"""Task list, detail and task action routes.

Moved from app.admin_dashboard in v4.7.2.
Reply assistant lineage: v4928.1.5.0 compatibility marker; v4928.1.5.13 compatibility marker; current badge v4928.1.5.14; draft refresh persistence v4928.1.5.15; editable revision workflow v4928.1.5.16; semi-automatic follow-ups v4928.1.5.17; follow-up draft generator v4928.1.5.18; dedicated OpenAI follow-up LLM prompt v4928.1.5.23; contact-person greeting v4928.1.5.24. The handlers still reuse
legacy helpers to keep this refactor behavior-preserving.
"""
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse, RedirectResponse, Response, PlainTextResponse
from urllib.parse import urlsplit, quote, urlencode
import json
import re
import app.admin_dashboard as legacy
from app.admin_dashboard import *  # noqa: F401,F403
from app.admin_ui.guidance import (
    fiscal_contact_panel_html,
    fiscal_customer_missing_fields,
    readiness_checklist_html,
)
from app.admin_ui.labels import primary_action_label
from app.task_service import reschedule_task_due_at
from app.reply_recipient_utils import resolve_reply_recipient
from app.company_opportunity_linking import associate_task_to_opportunity

router = APIRouter()


def _safe_search_query(value: Optional[str]) -> Optional[str]:
    if value is None:
        return None
    cleaned = re.sub(r"[\x00-\x1f\x7f-\x9f]", "", str(value))
    cleaned = cleaned.strip()
    if len(cleaned) > 120:
        cleaned = cleaned[:120]
    return cleaned or None


def _invalid_task_response():
    return PlainTextResponse("Identificador de tarefa inválido.", status_code=422)


def _safe_return_to(value: str, default: str = "/tasks?status=pending") -> str:
    """Preserve safe internal navigation context after a task action."""
    value = str(value or "").strip()
    if not value:
        return default
    if not value.startswith("/") or value.startswith("//"):
        return default
    parsed = urlsplit(value)
    if parsed.scheme or parsed.netloc:
        return default
    allowed_prefixes = ("/operations", "/operacoes", "/tasks", "/opportunities", "/finance", "/financeiro", "/orders", "/encomendas")
    if not parsed.path.startswith(allowed_prefixes):
        return default
    return value


def _return_to_hidden(return_to: str) -> str:
    return f'<input type="hidden" name="return_to" value="{esc(return_to)}">' if return_to else ""


def _return_to_link(return_to: str) -> str:
    href = return_to or "/tasks?status=pending"
    return f'<a class="cf-row-link d-inline-flex mb-3" href="{esc(href)}">← Voltar</a>'


def _task_href(task_id: str, return_to: str = "") -> str:
    """Build task detail URL preserving the caller page as navigation context."""
    href = f"/tasks/{task_id}"
    safe_return_to = _safe_return_to(return_to, default="") if return_to else ""
    if safe_return_to:
        href += f"?return_to={quote(safe_return_to, safe='')}"
    return href


def _tasks_list_return_to(status: Optional[str] = "pending", route: Optional[str] = None, view: Optional[str] = None, q: Optional[str] = None, limit: int = 200) -> str:
    params = {}
    if status:
        params["status"] = status
    if route:
        params["route"] = route
    if view:
        params["view"] = view
    if q:
        params["q"] = q
    if limit and int(limit) != 200:
        params["limit"] = str(limit)
    query = urlencode(params)
    return "/tasks" + (f"?{query}" if query else "")


def _htmx_redirect_response(return_to: str):
    response = Response(status_code=204)
    response.headers["HX-Redirect"] = return_to
    return response


def _safe_task_display_html(value: str) -> str:
    text = str(value or "")
    replacements = {
        "Traceback (most recent call last)": "Relatório técnico remoto",
        "Internal Server Error": "Erro interno reportado na mensagem",
        "Application startup failed": "Falha de arranque reportada na mensagem",
        "sqlalchemy.exc.": "sqlalchemy exc.",
        "psycopg.errors.": "psycopg errors.",
        "SyntaxError:": "SyntaxError reportado:",
        "Exception:": "Exceção reportada:",
        "fatura por emitir": "fatura criada/associada; enviar PDF ao cliente",
        "Fatura por emitir": "Fatura criada/associada; enviar PDF ao cliente",
        "Preparar e enviar orçamento para pagamento para pagamento.": "Preparar e enviar orçamento para pagamento.",
        "Enviar orçamento para pagamento": "Enviar orçamento para pagamento",
        "orçamento para pagamento": "orçamento para pagamento",
        "Orçamento para pagamento": "Orçamento para pagamento",
    }
    for old, new in replacements.items():
        text = text.replace(old, new)
        text = text.replace(old.lower(), new)
        text = text.replace(old.upper(), new)
    return text


def _task_opportunity_linking_panel_html(task: dict, return_to: str = "") -> str:
    metadata = task.get("metadata") or {}
    if isinstance(metadata, str):
        try:
            metadata = json.loads(metadata)
        except Exception:
            metadata = {}
    if str(metadata.get("opportunity_linking_status") or "").lower() != "ambiguous":
        return ""
    candidates = metadata.get("opportunity_linking_candidates") or []
    reason = str(metadata.get("opportunity_linking_reason") or "associação ambígua")
    if not candidates:
        return f'''
        <section class="card cf-card border-warning">
          <div class="card-body p-4">
            <h2 class="cf-section-title mb-2 text-warning">Associar oportunidade existente</h2>
            <p class="mb-0">O sistema encontrou risco de associação errada: {esc(reason)}. Usa a oportunidade correta antes de executar documentos ou pagamentos.</p>
          </div>
        </section>
        '''
    rows = []
    task_id = str(task.get("id") or "")
    hidden_return_to = _return_to_hidden(return_to)
    for c in candidates[:8]:
        opp_id = str(c.get("opportunity_id") or "")
        if not is_uuid_text(opp_id):
            continue
        title = str(c.get("title") or "Oportunidade")
        customer = str(c.get("customer_name") or c.get("customer_email") or "")
        doc = str(c.get("document_number") or "")
        amount = str(c.get("total_amount") or "")
        why = str(c.get("reason") or "candidato")
        rows.append(f'''
          <div class="border rounded-3 p-3 d-flex justify-content-between align-items-start gap-3 flex-wrap">
            <div>
              <div class="fw-bold"><a href="/opportunities/{esc(opp_id)}">{esc(title)}</a></div>
              <div class="small text-secondary">{esc(customer)}</div>
              <div class="small mt-1">{esc(doc or 'sem documento visível')} {esc(('· ' + amount) if amount else '')}</div>
              <div class="small text-secondary mt-1">Evidência: {esc(why)}</div>
            </div>
            <form method="post" action="/tasks/{esc(task_id)}/associate-opportunity" hx-post="/tasks/{esc(task_id)}/associate-opportunity" hx-target="#task-detail-panel" hx-swap="outerHTML" hx-confirm="Associar esta tarefa/conversa à oportunidade selecionada?">
              {hidden_return_to}
              <input type="hidden" name="opportunity_id" value="{esc(opp_id)}">
              <button class="btn btn-outline-primary btn-sm" type="submit">Associar</button>
            </form>
          </div>
        ''')
    body = "".join(rows) or "<p class='mb-0'>Sem candidatos válidos.</p>"
    return f'''
    <section class="card cf-card border-warning">
      <div class="card-body p-4">
        <h2 class="cf-section-title mb-2 text-warning">Associar a oportunidade existente</h2>
        <p class="text-secondary mb-3">A mensagem pode vir de outro departamento da mesma empresa. Escolhe a compra/processo correto antes de avançar.</p>
        <div class="d-grid gap-2">{body}</div>
      </div>
    </section>
    '''


DOCUMENT_TASK_ACTIONS = {"SEND_QUOTE", "SEND_PROFORMA", "SEND_INVOICE"}
# Legacy static anchor: Segue em anexo a fatura pró-forma solicitada
# Legacy static anchor: Segue em anexo a fatura orçamento para pagamento solicitada
FOLLOW_UP_ACTIONS = {"FOLLOW_UP_QUOTE", "FOLLOW_UP_PROFORMA", "FOLLOW_UP_PAYMENT", "FOLLOW_UP_CUSTOMER_REVIEW", "FOLLOW_UP_GENERIC"}


def _is_follow_up_task(task_or_code) -> bool:
    if isinstance(task_or_code, dict):
        code = task_or_code.get("action_code")
    else:
        code = task_or_code
    return str(code or "").strip().upper() in FOLLOW_UP_ACTIONS


def _follow_up_template_for_action(action_code: str) -> str:
    code = str(action_code or "").strip().upper()
    return {
        "FOLLOW_UP_QUOTE": "FOLLOW_UP_QUOTE",
        "FOLLOW_UP_PROFORMA": "FOLLOW_UP_PROFORMA",
        "FOLLOW_UP_PAYMENT": "FOLLOW_UP_PAYMENT",
        "FOLLOW_UP_CUSTOMER_REVIEW": "FOLLOW_UP_CUSTOMER_REVIEW",
        "FOLLOW_UP_GENERIC": "FOLLOW_UP_GENERIC",
    }.get(code, "FOLLOW_UP_GENERIC")


def _fallback_follow_up_message(action_code: str) -> str:
    code = str(action_code or "").strip().upper()
    if code == "FOLLOW_UP_PAYMENT":
        return "Olá,\n\nGostaria apenas de confirmar se recebeu o orçamento/dados de pagamento e se precisa de alguma informação adicional para avançar.\n\nObrigado."
    if code == "FOLLOW_UP_PROFORMA":
        return "Olá,\n\nGostaria apenas de confirmar se recebeu o orçamento para pagamento e se precisa de alguma informação adicional.\n\nObrigado."
    if code == "FOLLOW_UP_QUOTE":
        return "Olá,\n\nGostaria apenas de confirmar se recebeu a proposta/orçamento e se ficou com alguma dúvida.\n\nObrigado."
    if code == "FOLLOW_UP_CUSTOMER_REVIEW":
        return "Olá,\n\nGostaria apenas de confirmar se mantém interesse e se precisa de alguma informação adicional.\n\nObrigado."
    return "Olá,\n\nGostaria apenas de dar seguimento ao processo e confirmar se precisa de alguma informação adicional.\n\nObrigado."


def _metadata_dict(value) -> dict:
    if isinstance(value, dict):
        return value
    if isinstance(value, str) and value.strip():
        try:
            return json.loads(value)
        except Exception:
            return {}
    return {}


_IDENTITY_STOPWORDS = {
    "da", "de", "do", "dos", "das", "e", "lda", "ltda", "unipessoal",
    "sa", "s.a", "s.a.", "email", "mail", "geral", "info", "office",
    "frontoffice", "comercial", "vendas", "admin", "contacto", "contact",
}

PUBLIC_CONTACT_EMAIL_DOMAINS = {
    "gmail.com", "googlemail.com", "hotmail.com", "hotmail.pt", "outlook.com",
    "outlook.pt", "live.com", "msn.com", "icloud.com", "me.com", "mac.com",
    "yahoo.com", "yahoo.pt", "sapo.pt", "mail.telepac.pt", "mail.com",
    "proton.me", "protonmail.com", "aol.com", "gmx.com", "gmx.net", "uol.com.br",
}


def _email_domain_for_identity(value) -> str:
    text = str(value or "").strip().casefold()
    if "@" not in text:
        return ""
    domain = text.rsplit("@", 1)[-1].strip(" .")
    if domain.startswith("www."):
        domain = domain[4:]
    return domain


def _email_local_for_identity(value) -> str:
    text = str(value or "").strip().casefold()
    if "@" not in text:
        return ""
    return text.split("@", 1)[0].strip(" ._-+")


def _is_public_contact_email_domain(domain) -> bool:
    return _email_domain_for_identity(f"x@{domain}") in PUBLIC_CONTACT_EMAIL_DOMAINS if "@" not in str(domain or "") else _email_domain_for_identity(domain) in PUBLIC_CONTACT_EMAIL_DOMAINS


def _norm_identity(value) -> str:
    return " ".join(re.sub(r"[^0-9a-zA-ZÀ-ÿ]+", " ", str(value or "").casefold()).split())


def _identity_tokens(value) -> set[str]:
    return {
        token
        for token in _norm_identity(value).split()
        if len(token) >= 3 and token not in _IDENTITY_STOPWORDS
    }


def _email_tokens(value) -> set[str]:
    email = str(value or "").strip().casefold()
    local = email.split("@", 1)[0]
    return {
        token
        for token in re.split(r"[^0-9a-zA-ZÀ-ÿ]+", local)
        if len(token) >= 3 and token not in _IDENTITY_STOPWORDS
    }


def _identity_overlaps(left, right) -> bool:
    left_norm = _norm_identity(left)
    right_norm = _norm_identity(right)
    # Company names frequently appear once as a short campaign hint
    # ("CARPINTARIA AVELEIRAS") and once as a full fiscal name
    # ("CARPINTARIA AVELEIRAS, UNIPESSOAL, LDA"). Treat substring
    # matches as safe before falling back to token overlap.
    if left_norm and right_norm and (left_norm in right_norm or right_norm in left_norm):
        return True
    left_tokens = _identity_tokens(left)
    right_tokens = _identity_tokens(right)
    if not left_tokens or not right_tokens:
        return False
    if left_tokens & right_tokens:
        return True
    return any(a in b or b in a for a in left_tokens for b in right_tokens)


def _task_has_compatible_opportunity_identity(task: dict, hint: str | None = None) -> bool:
    """Return true when opportunity/document identity matches the process hint.

    Legacy tasks may have no direct local_customer_id but still carry a valid
    fiscal identity through the opportunity documents. In that case we must not
    show the contradictory "cliente fiscal por confirmar" guard.
    """
    hint = str(hint or _task_process_customer_hint(task) or "").strip()
    names = [
        task.get("linked_customer_name"),
        task.get("opportunity_customer_name"),
        task.get("customer_name"),
    ]
    if not hint:
        return any(str(v or "").strip() for v in names)
    return any(_identity_overlaps(value, hint) for value in names if str(value or "").strip())


def _name_matches_email(name, email) -> bool:
    name_tokens = _identity_tokens(name)
    email_tokens = _email_tokens(email)
    if not name_tokens or not email_tokens:
        return False
    if name_tokens & email_tokens:
        return True
    return any(nt in et or et in nt for nt in name_tokens for et in email_tokens)


def _raw_payload_dict(task: dict) -> dict:
    payload = task.get("raw_payload")
    if isinstance(payload, dict):
        return payload
    if isinstance(payload, str) and payload.strip():
        try:
            data = json.loads(payload)
            return data if isinstance(data, dict) else {}
        except Exception:
            return {}
    return {}


def _payload_get(payload: dict, *path) -> str:
    cur = payload or {}
    for part in path:
        if not isinstance(cur, dict):
            return ""
        cur = cur.get(part)
    return str(cur or "").strip()


def _task_sender_name(task: dict) -> str:
    payload = _raw_payload_dict(task)
    return (
        _payload_get(payload, "sender", "name")
        or _payload_get(payload, "conversation", "meta", "sender", "name")
        or str(task.get("sender_name") or "").strip()
    )


def _task_sender_email(task: dict) -> str:
    payload = _raw_payload_dict(task)
    return (
        _payload_get(payload, "sender", "email")
        or _payload_get(payload, "conversation", "meta", "sender", "email")
        or str(task.get("sender_email") or "").strip()
        or str(task.get("customer_email") or "").strip()
    )


def _task_sender_phone(task: dict) -> str:
    payload = _raw_payload_dict(task)
    return (
        _payload_get(payload, "sender", "phone_number")
        or _payload_get(payload, "conversation", "meta", "sender", "phone_number")
        or str(task.get("customer_phone") or "").strip()
    )


def _task_process_customer_hint(task: dict) -> str:
    value = str(task.get("opportunity_title") or task.get("message_subject") or "").strip()
    if not value:
        return ""
    if "·" in value:
        tail = value.rsplit("·", 1)[-1].strip()
        if tail and not tail.upper().startswith(("ORC.", "S0")):
            return tail
    match = re.search(r"\bpara\s+(?:a|o|as|os|à|ao)?\s*(.+)$", value, re.IGNORECASE)
    if match:
        candidate = re.sub(r"\s+", " ", match.group(1)).strip(" .:-–—")
        candidate = re.split(r"\s+(?:de:|from:|enviada:|sent:)", candidate, maxsplit=1, flags=re.IGNORECASE)[0].strip()
        if 2 <= len(candidate) <= 120:
            return candidate
    return ""


def _task_identity_context(task: dict) -> dict:
    hint = _task_process_customer_hint(task)
    linked_name = str(task.get("linked_customer_name") or "").strip()
    linked_email = str(task.get("linked_customer_email") or "").strip()
    opportunity_customer_name = str(task.get("opportunity_customer_name") or "").strip()
    opportunity_customer_email = str(task.get("opportunity_customer_email") or "").strip()
    sender_name = _task_sender_name(task)
    sender_email = _task_sender_email(task)
    fiscal_unsafe = False
    process_identity_unsafe = False
    reason = ""
    compatible_identity = _task_has_compatible_opportunity_identity(task, hint)

    if hint and linked_name and not compatible_identity and not _identity_overlaps(linked_name, hint):
        # A common failure mode is a person/contact being auto-associated as the
        # fiscal customer while the campaign/process title clearly points to a
        # different company.  This is a UI/task-safety guard: it does not modify
        # customers, opportunities or tasks.
        fiscal_unsafe = True
        reason = "linked_fiscal_customer_mismatch_process_hint"

    if hint and opportunity_customer_name and not compatible_identity and not _identity_overlaps(opportunity_customer_name, hint):
        # If a repair script has already detached the fiscal customer, the legacy
        # opportunity.customer_name can still carry the polluted contact name.
        # Prefer the process/title hint until the fiscal customer is confirmed.
        process_identity_unsafe = True
        reason = reason or "opportunity_customer_mismatch_process_hint"

    if fiscal_unsafe or process_identity_unsafe:
        display_name = hint
    else:
        display_name = ""
        for value in (
            linked_name,
            opportunity_customer_name,
            sender_name,
            sender_email,
            linked_email,
            task.get("opportunity_title"),
            task.get("contact_id"),
            task.get("customer_id"),
        ):
            text = str(value or "").strip()
            if text and text.lower() not in {"cliente", "contacto sem identificação"}:
                display_name = text
                break
    if not display_name:
        display_name = hint or "Cliente"

    contact_name = sender_name
    if not contact_name and _name_matches_email(linked_name, sender_email):
        contact_name = linked_name
    if not contact_name and _name_matches_email(opportunity_customer_name, sender_email):
        contact_name = opportunity_customer_name
    if not contact_name:
        contact_name = sender_email or opportunity_customer_email or "Contacto por confirmar"

    return {
        "display_name": display_name,
        "process_customer_hint": hint,
        "fiscal_identity_unsafe": fiscal_unsafe,
        "process_identity_unsafe": process_identity_unsafe,
        "identity_unsafe": fiscal_unsafe or process_identity_unsafe,
        "fiscal_identity_reason": reason,
        "unsafe_fiscal_name": linked_name,
        "unsafe_fiscal_email": linked_email,
        "unsafe_opportunity_customer_name": opportunity_customer_name,
        "unsafe_opportunity_customer_email": opportunity_customer_email,
        "contact_name": contact_name,
        "contact_email": sender_email or opportunity_customer_email or linked_email,
        "contact_phone": _task_sender_phone(task),
    }

def _safe_fiscal_customer_for_task(task: dict, safe_customer_id: str) -> dict | None:
    ctx = _task_identity_context(task)
    if ctx.get("identity_unsafe") and not _task_has_compatible_opportunity_identity(task, ctx.get("process_customer_hint")):
        return None
    name = task.get("linked_customer_name") or task.get("opportunity_customer_name") or task.get("customer_name")
    email = task.get("linked_customer_email") or task.get("opportunity_customer_email") or task.get("customer_email")
    if safe_customer_id or name:
        return {
            "id": safe_customer_id,
            "name": name,
            "email": email,
            "tax_id": task.get("linked_customer_tax_id"),
            "street_name": task.get("linked_customer_street_name"),
            "postal_zone": task.get("linked_customer_postal_zone"),
            "city_name": task.get("linked_customer_city_name"),
            "phone": task.get("linked_customer_phone"),
        }
    return None


def _task_identity_warning_html(task: dict) -> str:
    ctx = _task_identity_context(task)
    if not ctx.get("identity_unsafe") or _task_has_compatible_opportunity_identity(task, ctx.get("process_customer_hint")):
        return ""
    hint = ctx.get("process_customer_hint") or "processo atual"
    linked = ctx.get("unsafe_fiscal_name") or ctx.get("unsafe_opportunity_customer_name") or "cliente/contacto associado"
    return f'''
    <div class="alert alert-warning border-warning-subtle mb-3">
      <strong>Cliente fiscal por confirmar.</strong>
      <div class="small">O processo parece ser <strong>{esc(hint)}</strong>, mas a task está ligada a <strong>{esc(linked)}</strong>. Não emitir orçamento ou fatura com esta associação sem validar/corrigir o cliente fiscal.</div>
    </div>
    '''



def _task_public_domain_identity_context(task: dict) -> dict:
    """Detect misleading fiscal/contact email matches on public domains.

    Public provider domains are communication channels, not company identity.
    `epotencia.geral@sapo.pt` and `casa-figueiredo@sapo.pt` must therefore
    never be treated as matching evidence just because both end in sapo.pt.
    """
    fiscal_email = str(
        task.get("linked_customer_email")
        or task.get("opportunity_customer_email")
        or ""
    ).strip()
    contact_email = _task_sender_email(task)
    fiscal_domain = _email_domain_for_identity(fiscal_email)
    contact_domain = _email_domain_for_identity(contact_email)
    if not fiscal_email or not contact_email or not fiscal_domain or not contact_domain:
        return {"warning": False}
    if fiscal_email.casefold() == contact_email.casefold():
        return {"warning": False}
    fiscal_public = fiscal_domain in PUBLIC_CONTACT_EMAIL_DOMAINS
    contact_public = contact_domain in PUBLIC_CONTACT_EMAIL_DOMAINS
    same_public_domain = fiscal_public and contact_public and fiscal_domain == contact_domain
    if not same_public_domain:
        return {"warning": False}
    return {
        "warning": True,
        "fiscal_email": fiscal_email,
        "contact_email": contact_email,
        "domain": fiscal_domain,
        "same_local_part": _email_local_for_identity(fiscal_email) == _email_local_for_identity(contact_email),
    }


def _task_public_domain_identity_warning_html(task: dict) -> str:
    ctx = _task_public_domain_identity_context(task)
    if not ctx.get("warning"):
        return ""
    return f'''
    <div class="alert alert-warning border-warning-subtle mb-3">
      <strong>Domínio público não confirma identidade.</strong>
      <div class="small">O email fiscal <strong>{esc(ctx.get('fiscal_email'))}</strong> e o contacto Chatwoot <strong>{esc(ctx.get('contact_email'))}</strong> usam <strong>{esc(ctx.get('domain'))}</strong>. Este domínio é público/ISP, por isso deve servir apenas como canal de contacto e não como prova de que pertencem à mesma empresa.</div>
    </div>
    '''

def _apply_identity_context_to_preparation(prep_vm: dict, task: dict) -> dict:
    ctx = _task_identity_context(task)
    if not ctx.get("identity_unsafe") or _task_has_compatible_opportunity_identity(task, ctx.get("process_customer_hint")):
        return prep_vm
    fixed = dict(prep_vm or {})
    confirmed = []
    for item in fixed.get("confirmed_fields") or []:
        if not isinstance(item, dict):
            continue
        label = str(item.get("label") or "")
        if label == "Empresa":
            value = ctx.get("process_customer_hint") or ctx.get("display_name") or item.get("value")
            confirmed.append({"label": label, "value": f"{value} (por confirmar)"})
        elif label == "Email":
            confirmed.append({"label": label, "value": ctx.get("contact_email") or item.get("value")})
        elif label == "NIF":
            # NIF belongs to the unsafe fiscal customer, so do not display it as
            # confirmed for this process.
            continue
        else:
            confirmed.append(item)
    fixed["confirmed_fields"] = confirmed
    return fixed


COMMUNICATION_ACTIONS = {"SEND_INFO", "SEND_QUOTE", "SEND_PROFORMA", "SEND_INVOICE", "FOLLOW_UP_QUOTE", "FOLLOW_UP_PROFORMA", "FOLLOW_UP_PAYMENT", "FOLLOW_UP_CUSTOMER_REVIEW", "FOLLOW_UP_GENERIC"}


def _task_display_name(task: dict) -> str:
    return str(_task_identity_context(task).get("display_name") or "Cliente")


def _task_email(task: dict) -> str:
    ctx = _task_identity_context(task)
    for value in (
        ctx.get("contact_email"),
        task.get("opportunity_customer_email"),
        task.get("customer_email"),
        task.get("linked_customer_email"),
    ):
        text = str(value or "").strip()
        if "@" in text:
            return text
    return ""




def _task_opportunity_navigation_html(task: dict, opportunity_id: str, *, small: bool = False) -> str:
    """Return a useful opportunity navigation control for task detail pages.

    Some support/commercial tasks are created from Chatwoot/email before an
    opportunity is explicitly linked. In that case, do not leave the operator
    without navigation: provide a read-only search link prefilled with the best
    email/name/subject hint so the opportunity can be found or linked manually.
    """
    btn_class = "btn btn-sm btn-outline-primary" if small else "btn btn-outline-primary"
    opportunity_id = str(opportunity_id or "").strip()
    if opportunity_id and is_uuid_text(opportunity_id):
        return f'<a class="{btn_class}" href="/opportunities/{esc(opportunity_id)}">Ver oportunidade</a>'

    query = (
        _safe_search_query(_task_email(task))
        or _safe_search_query(_task_display_name(task))
        or _safe_search_query(task.get("message_subject"))
        or ""
    )
    href = "/opportunities" + (f"?q={quote(query)}" if query else "")
    return (
        f'<a class="{btn_class}" href="{esc(href)}">Procurar oportunidade</a>'
        '<span class="small text-secondary">Sem oportunidade ligada diretamente a esta tarefa.</span>'
    )

def _task_contact_person(task: dict) -> dict:
    try:
        return resolve_reply_recipient(task) or {}
    except Exception:
        return {}


def _task_contact_person_notice_html(task: dict) -> str:
    recipient = _task_contact_person(task)
    name = str(recipient.get("person_name") or "").strip()
    if not name:
        return ""
    first = str(recipient.get("person_first_name") or "").strip()
    source = str(recipient.get("greeting_source") or "").strip()
    confidence = str(recipient.get("person_confidence") or "").strip()
    greeting = str(recipient.get("preferred_greeting") or "").strip()
    bits = [f"Pessoa de contacto: <strong>{esc(name)}</strong>"]
    if first and first != name:
        bits.append(f"primeiro nome: <strong>{esc(first)}</strong>")
    if greeting:
        bits.append(f"saudação IA: <strong>{esc(greeting)}</strong>")
    if source:
        bits.append(f"origem: {esc(source)}{(' · confiança ' + esc(confidence)) if confidence else ''}")
    return '<div class="alert alert-light border py-2 mb-3">' + ' · '.join(bits) + '</div>'


def _task_has_channel(task: dict) -> bool:
    return bool(_task_email(task) or _task_effective_conversation_id(task))


def _task_effective_conversation_id(task: dict) -> str:
    direct = str(task.get("conversation_id") or task.get("opportunity_conversation_id") or "").strip()
    if direct:
        return direct
    payload = task.get("raw_payload") or {}
    if isinstance(payload, str):
        try:
            payload = json.loads(payload)
        except Exception:
            payload = {}
    if isinstance(payload, dict):
        for path in (("conversation_id",), ("conversation", "id"), ("conversation", "display_id")):
            cur = payload
            for key in path:
                cur = cur.get(key) if isinstance(cur, dict) else None
            if cur:
                return str(cur).strip()
    return ""


def _customer_send_template_code_for_task(task: dict, template_code: str, message_body: str = "") -> str:
    code = str(template_code or "").strip().upper()
    action = str(task.get("action_code") or "").strip().upper()
    body = str(message_body or "")
    body_low = body.lower()
    if code in {"MANUAL_REVIEW_REQUIRED", "INTERNAL_BOUNCE_EMAIL", "INTERNAL_AUTO_REPLY"} and action in {"SEND_INFO", "SEND_QUOTE", "FOLLOW_UP_QUOTE", "FOLLOW_UP_PROFORMA", "FOLLOW_UP_PAYMENT", "FOLLOW_UP_CUSTOMER_REVIEW", "FOLLOW_UP_GENERIC", "SUPPORT"}:
        if body.strip() and not any(marker in body_low for marker in ("triagem interna", "não deve ser enviado ao cliente", "nao deve ser enviado ao cliente", "rever manualmente")):
            if action == "SEND_INFO":
                return "SEND_INFO_EQUIPMENT_LIST"
            if action == "SUPPORT":
                return "ACK_SUPPORT_RECEIVED"
            return action
    return code



def _communication_channel_notice_html(task: dict) -> str:
    action_code = str(task.get("action_code") or "").upper()
    if action_code not in COMMUNICATION_ACTIONS:
        return ""
    email = _task_email(task)
    conversation_id = _task_effective_conversation_id(task)
    if email or conversation_id:
        bits = []
        if email:
            bits.append(f"Email disponível: <strong>{esc(email)}</strong>")
        if conversation_id:
            bits.append(f"Chatwoot: conversa <strong>#{esc(conversation_id)}</strong>")
        return '<div class="alert alert-info py-2 mb-3">Canal de comunicação: ' + ' · '.join(bits) + '</div>'
    return """
    <div class="alert alert-warning border-warning-subtle mb-3">
      <strong>Sem canal de comunicação visível.</strong>
      <div class="small">Esta tarefa pede contacto com o cliente, mas não existe email nem conversa Chatwoot associada. Se o pedido veio por WhatsApp, telefone ou outro canal externo, regista o envio manualmente no bloco <strong>Concluir</strong>.</div>
    </div>
    """


def _task_has_available_document_for_action(task: dict, action_code: str) -> bool:
    """Return True when a document already exists and can be referenced as sent.

    Missing fiscal/contact fields must block issuing or automatic sending, but
    they must not prevent the operator from closing a task after sending an
    already-created document through WhatsApp, phone or another external channel.
    """
    code = str(action_code or "").upper()
    docs = _available_docs_for_task_safe(task)
    if not docs:
        return False
    if code == "SEND_INVOICE":
        return any(str(doc.get("document_kind") or "").lower() == "invoice" for doc in docs)
    if code in {"SEND_QUOTE", "SEND_PROFORMA"}:
        return any(str(doc.get("document_kind") or "").lower() in {"quotation", "proforma", "invoice"} for doc in docs)
    return bool(docs)


def _external_channel_completion_note(action_code: str, channel: str) -> str:
    code = str(action_code or "").upper()
    channel_label = str(channel or "canal externo").strip() or "canal externo"
    if code == "SEND_PROFORMA":
        return f"Orçamento para pagamento enviado ao cliente por {channel_label}."
    if code == "SEND_QUOTE":
        return f"Orçamento/proposta enviado ao cliente por {channel_label}."
    if code == "SEND_INVOICE":
        return f"Fatura enviada ao cliente por {channel_label}."
    if code.startswith("FOLLOW_UP_"):
        return f"Follow-up/contacto feito por {channel_label}."
    return f"Contacto tratado pelo operador por {channel_label}."


def _external_channel_completion_form_html(task_id: str, task: dict, action_code: str, status: str, return_to_hidden: str) -> str:
    """Render explicit external-channel completion for WhatsApp/manual flows."""
    code = str(action_code or "").upper()
    if str(status or "").lower() != "pending" or code not in COMMUNICATION_ACTIONS:
        return ""
    if _task_has_channel(task):
        return ""
    has_doc = _task_has_available_document_for_action(task, code)
    doc_hint = "Documento/anexo associado encontrado; será registado como enviado externamente." if has_doc else "Sem documento associado visível; usa apenas se o contacto já foi tratado fora do ClientFlow."
    default_note = _external_channel_completion_note(code, "WhatsApp")
    return f'''
      <div class="alert alert-light border mt-3 mb-0">
        <div class="fw-bold mb-1">Canal externo</div>
        <div class="small text-secondary mb-2">Sem email/Chatwoot nesta tarefa. Usa isto quando já enviaste por WhatsApp, telefone, presencialmente ou outro canal externo. {esc(doc_hint)}</div>
        <form method="post" action="/tasks/{esc(task_id)}/complete-with-note" hx-post="/tasks/{esc(task_id)}/complete-with-note" hx-target="#task-detail-panel" hx-swap="outerHTML" hx-indicator="#task-detail-loading" hx-confirm="Confirmar que a tarefa foi tratada por canal externo?" class="vstack gap-2">
          {return_to_hidden}
          <input type="hidden" name="manual_external_done" value="1">
          <select class="form-select" name="external_channel" aria-label="Canal externo">
            <option value="WhatsApp" selected>WhatsApp</option>
            <option value="telefone">Telefone</option>
            <option value="presencialmente">Presencial</option>
            <option value="canal externo">Outro canal externo</option>
          </select>
          <input type="hidden" name="done_note" value="{esc(default_note)}">
          <textarea class="form-control" name="done_note_extra" rows="2" placeholder="Nota opcional, ex.: enviado para o número WhatsApp do cliente"></textarea>
          <button class="btn btn-success" type="submit">Marcar como enviado externamente</button>
        </form>
      </div>
    '''

def _context_task_html(task: dict, request_text: str, *, is_follow_up: bool) -> str:
    text = str(request_text or "").strip()
    if not is_follow_up:
        return f'<div class="cf-copy-box">{esc(text)}</div>'
    marker = "Criado por backfill"
    internal = ""
    visible = text
    if marker in text:
        before, after = text.split(marker, 1)
        visible = before.strip()
        internal = marker + after.strip()
    if not visible:
        visible = task_next_action_text(task) or str(task.get("action") or "")
    internal_html = ""
    if internal:
        internal_html = f"""
        <details class="mt-3">
          <summary class="small fw-bold text-secondary" style="cursor:pointer">Ver contexto interno</summary>
          <div class="cf-copy-box mt-2 small text-secondary">{esc(internal)}</div>
        </details>
        """
    return f"""
      <div class="small text-secondary fw-bold mb-1">Objetivo operacional</div>
      <div class="cf-copy-box">{esc(visible)}</div>
      {internal_html}
    """


def _document_label_from_doc(doc: dict) -> str:
    label = str(doc.get("label") or "").strip()
    if label:
        return label
    number = str(doc.get("document_number") or doc.get("external_id") or doc.get("id") or "documento")
    kind = str(doc.get("document_kind") or "documento")
    amount = doc.get("total_amount") or doc.get("amount") or ""
    currency = str(doc.get("currency") or "EUR")
    suffix = f" · {amount} {currency}" if amount else ""
    return f"{kind} {number}{suffix}"


def _is_orc_quotation_doc(doc: dict) -> bool:
    kind = str(doc.get("document_kind") or "").lower()
    number = str(doc.get("document_number") or doc.get("external_id") or "").upper()
    system = str(doc.get("system") or "jasmin").lower()
    return kind == "quotation" and system == "jasmin" and (number.startswith("ORC.") or number.startswith("ORC"))


def _document_label_for_task(doc: dict, action_code: str) -> str:
    if str(action_code or "").upper() == "SEND_PROFORMA" and _is_orc_quotation_doc(doc):
        number = str(doc.get("document_number") or doc.get("external_id") or "ORC")
        amount = doc.get("total_amount") or doc.get("amount") or ""
        currency = str(doc.get("currency") or "EUR")
        suffix = f" · {amount} {currency}" if amount else ""
        return f"Orçamento para pagamento {number}{suffix}"
    return _document_label_from_doc(doc)


def _document_kind_label_for_task(doc: dict, action_code: str) -> str:
    kind = str(doc.get("document_kind") or "").lower()
    if str(action_code or "").upper() == "SEND_PROFORMA" and _is_orc_quotation_doc(doc):
        return "orçamento para pagamento · ORC Jasmin"
    return kind or "documento"


def _available_docs_for_task_safe(task: dict) -> list[dict]:
    try:
        from app.reply_assistant_service import available_documents_for_task
        return list(available_documents_for_task(task) or [])
    except Exception:
        return []


def _default_doc_ids_for_followup(action_code: str, docs: list[dict]) -> list[str]:
    if not docs:
        return []
    expected = {
        "FOLLOW_UP_QUOTE": {"quotation"},
        "FOLLOW_UP_PROFORMA": {"proforma", "invoice", "quotation"},
        "FOLLOW_UP_PAYMENT": {"proforma", "invoice", "quotation"},
    }.get(str(action_code or "").upper(), {"quotation"} if str(action_code or "").upper() == "SEND_PROFORMA" else set())
    matching = [doc for doc in docs if str(doc.get("document_kind") or "").lower() in expected]
    chosen = matching or docs
    return [str(chosen[0].get("id"))] if chosen and chosen[0].get("id") else []


def _effective_selected_doc_ids_for_task(task: dict, selected_ids: list[str] | None = None) -> list[str]:
    """Return persisted/posted document ids, falling back to the UI default.

    v1.5.39: older drafts saved before attachment persistence can render a
    selected ORC/invoice in compact mode via default selection while posting no
    hidden selected_document_ids.  That made revision/send validation think no
    document was selected.  Centralise the fallback so UI, revision and send use
    the same effective selection.
    """
    ids = [str(x).strip() for x in (selected_ids or []) if str(x or "").strip()]
    if ids:
        return ids
    docs = _available_docs_for_task_safe(task)
    return _default_doc_ids_for_followup(str(task.get("action_code") or ""), docs)


def _followup_documents_picker_html(task: dict, selected_ids: list[str] | None = None, *, compact: bool = False) -> str:
    docs = _available_docs_for_task_safe(task)
    if not docs:
        return ""
    selected = set(str(x) for x in (selected_ids or []))
    if not selected:
        selected = set(_default_doc_ids_for_followup(str(task.get("action_code") or ""), docs))
    warning = ""
    if len(docs) > 1:
        warning = '<div class="alert alert-warning py-2 mb-2 small"><strong>Vários documentos ligados.</strong> Confirma qual deve ser usado no rascunho antes de gerar/enviar.</div>'
    rows = []
    for doc in docs:
        doc_id = str(doc.get("id") or "")
        checked = "checked" if doc_id in selected else ""
        kind = str(doc.get("document_kind") or "").lower()
        label = _document_label_for_task(doc, str(task.get("action_code") or ""))
        kind_label = _document_kind_label_for_task(doc, str(task.get("action_code") or ""))
        pdf_ok = bool(doc.get("pdf_available") or doc.get("pdf_supported"))
        requires_attachment = kind in {"invoice", "quotation", "proforma"}
        status_text = str(doc.get("attachment_status") or ("PDF/anexo disponível" if pdf_ok else "PDF/anexo não disponível" if requires_attachment else kind or "documento"))
        status_badge = (
            '<span class="badge text-bg-success-subtle text-success border border-success-subtle">✓ PDF/anexo disponível</span>'
            if pdf_ok else
            '<span class="badge text-bg-warning-subtle text-warning border border-warning-subtle">⚠ PDF/anexo não disponível</span>'
            if requires_attachment else
            f'<span class="small text-secondary">{esc(kind or "documento")}</span>'
        )
        if compact:
            if selected and doc_id not in selected:
                continue
            rows.append(
                '<div class="border rounded-3 p-2 bg-light">'
                f'<strong>{esc(label)}</strong><br>'
                f'<span class="small text-secondary">{esc(kind_label)}</span><div class="mt-1">{status_badge}<span class="badge text-bg-info-subtle text-info border border-info-subtle ms-1">✓ selecionado para envio</span></div>'
                '</div>'
            )
        else:
            rows.append(f"""
            <label class="border rounded-3 p-2 bg-light d-flex gap-2 align-items-start">
              <input class="form-check-input mt-1" type="checkbox" name="selected_document_ids" value="{esc(doc_id)}" {checked}>
              <span><strong>{esc(label)}</strong><br><span class="small text-secondary">{esc(kind_label)}</span><div class="mt-1">{status_badge}{'<span class="badge text-bg-info-subtle text-info border border-info-subtle ms-1">✓ será anexado ao envio</span>' if checked else ''}</div><div class="small text-secondary mt-1">{esc(status_text)}</div></span>
            </label>
            """)
    if str(task.get("action_code") or "").upper() == "SEND_PROFORMA":
        title = "Documento de orçamento/anexo" if compact else "Documentos de orçamento/anexos da oportunidade"
    else:
        title = "Documento/anexo usado no rascunho" if compact else "Documentos/anexos da oportunidade"
    return f"""
      <div class="mb-3">
        <div class="form-label small fw-bold text-secondary">{esc(title)}</div>
        {warning}
        <div class="vstack gap-2">{''.join(rows) or '<div class="small text-secondary">Sem documento selecionado.</div>'}</div>
      </div>
    """

def _task_due_chip(task: dict) -> str:
    if not task.get("due_at"):
        return ""
    label = "Follow-up" if _is_follow_up_task(task) else "Vence"
    return f'<span class="cf-chip cf-chip-blue">{esc(label)}: {esc(fmt_dt(task.get("due_at")))}</span>'


def _follow_up_controls_html(task_id: str, task: dict, return_to_hidden: str = "") -> str:
    if not _is_follow_up_task(task):
        return ""
    metadata = _metadata_dict(task.get("metadata"))
    action_code = str(task.get("action_code") or "")
    suggested = str(
        metadata.get("suggested_customer_message")
        or metadata.get("suggested_message")
        or metadata.get("followup_draft")
        or _fallback_follow_up_message(action_code)
    ).strip()
    reason = str(metadata.get("follow_up_reason") or "").strip() or action_code
    due_line = f'<div class="small text-secondary mb-2">Vence em: <strong>{esc(fmt_dt(task.get("due_at")))}</strong> · motivo: {esc(reason)}</div>' if task.get("due_at") else f'<div class="small text-secondary mb-2">Motivo: {esc(reason)}</div>'
    return f'''
    <section class="card cf-card border-primary-subtle"><div class="card-body p-4">
      <div class="d-flex justify-content-between align-items-start gap-3 mb-3">
        <div>
          <h2 class="cf-section-title mb-1">Follow-up semi-automático</h2>
          <div class="small text-secondary">O sistema agenda e sugere; o operador valida e envia. Nada é enviado automaticamente.</div>
        </div>
        <span class="badge text-bg-primary-subtle text-primary border border-primary-subtle">manual/semi-auto</span>
      </div>
      {due_line}
      <label class="form-label small fw-bold text-secondary">Mensagem sugerida</label>
      <textarea class="form-control mb-3" rows="7" readonly>{esc(suggested)}</textarea>
      <div class="d-flex flex-wrap gap-2">
        <button class="btn btn-outline-primary" type="button" onclick="navigator.clipboard && navigator.clipboard.writeText(this.closest('section')?.querySelector('textarea')?.value || '')">Copiar mensagem base</button>
        <form method="post" action="/tasks/{esc(task_id)}/reschedule" hx-post="/tasks/{esc(task_id)}/reschedule" hx-target="#task-detail-panel" hx-swap="outerHTML" hx-indicator="#task-detail-loading" class="d-inline">
          {return_to_hidden}
          <input type="hidden" name="delay_days" value="2">
          <input type="hidden" name="reason" value="Adiado pelo operador">
          <button class="btn btn-outline-secondary" type="submit">Adiar 2 dias</button>
        </form>
        <form method="post" action="/tasks/{esc(task_id)}/reschedule" hx-post="/tasks/{esc(task_id)}/reschedule" hx-target="#task-detail-panel" hx-swap="outerHTML" hx-indicator="#task-detail-loading" class="d-inline">
          {return_to_hidden}
          <input type="hidden" name="delay_days" value="7">
          <input type="hidden" name="reason" value="Adiado pelo operador">
          <button class="btn btn-outline-secondary" type="submit">Adiar 7 dias</button>
        </form>
      </div>
    </div></section>
    '''


def _task_has_ready_fiscal_customer(action_code: str, fiscal_customer: dict | None, fiscal_missing_labels: list[str]) -> bool:
    """True when the linked fiscal customer should override stale preparation gaps."""
    return str(action_code or "").upper() in DOCUMENT_TASK_ACTIONS and bool(fiscal_customer) and not fiscal_missing_labels


def _is_fiscal_preparation_missing_item(item: dict) -> bool:
    """Detect preparation missing fields that are satisfied by a linked fiscal customer.

    Preparations are snapshots from the original message extraction. After the
    operator enriches/links a fiscal customer, fields such as customer.company,
    billing.tax_id or billing.billing_address must no longer drive the visible
    task state. Product/shipment gaps are intentionally not matched here.
    """
    raw = str((item or {}).get("raw") or "").strip().lower()
    label = str((item or {}).get("label") or "").strip().lower()
    text = f"{raw} {label}"
    fiscal_tokens = (
        "customer.company", "customer · company", "company",
        "customer.email", "customer · email",
        "customer.phone", "customer · phone",
        "billing.billing_name", "billing · billing name", "billing name",
        "billing.tax_id", "nif", "tax id",
        "billing.billing_address", "morada fiscal", "billing address",
        "billing.billing_email", "email faturação", "email de faturação",
    )
    return any(token in text for token in fiscal_tokens)


def _effective_missing_items_for_task(
    *,
    action_code: str,
    prep_vm: dict,
    fiscal_customer: dict | None,
    fiscal_missing_labels: list[str],
) -> list[dict]:
    """Merge preparation gaps with current fiscal readiness without contradictions."""
    missing_items = [item for item in list(prep_vm.get("missing_fields") or []) if isinstance(item, dict)]

    if _task_has_ready_fiscal_customer(action_code, fiscal_customer, fiscal_missing_labels):
        missing_items = [item for item in missing_items if not _is_fiscal_preparation_missing_item(item)]
    else:
        if fiscal_customer:
            # Remove stale, generic preparation gaps that contradict the current
            # task/opportunity context. Keep concrete missing fields below
            # (morada, CP, localidade, email de faturação, etc.).
            stale_generic = (
                "cliente fiscal por confirmar",
                "cliente fiscal associado",
                "cliente fiscal: cliente fiscal por confirmar",
                "cliente fiscal: cliente fiscal associado",
            )
            missing_items = [
                item for item in missing_items
                if str(item.get("label") or "").strip().lower() not in stale_generic
            ]
        existing_missing_labels = {str(item.get("label") or "") for item in missing_items}
        for label in fiscal_missing_labels:
            fiscal_label = f"Cliente fiscal: {label}"
            if fiscal_label not in existing_missing_labels:
                missing_items.append({"label": fiscal_label})

    return missing_items


def _effective_primary_action_for_task(action_code: str, prep_vm: dict, missing_items: list[dict]) -> str:
    primary = str(prep_vm.get("primary_action") or "").strip()
    if not missing_items and primary.lower().startswith("pedir dados em falta"):
        return primary_action_label(action_code, fallback=action_label(action_code))
    return primary or primary_action_label(action_code, fallback=action_label(action_code))


def _ready_document_reply_for_task(task: dict, action_code: str) -> str:
    customer_name = str(task.get("customer_name") or task.get("linked_customer_name") or "").strip()
    first_name = customer_name.split()[0] if customer_name else ""
    greeting = f"Olá {first_name}," if first_name and first_name.lower() not in ["cliente", "desconhecido"] else "Olá,"
    closing = "Obrigado,\nEquipa BLIF"
    if str(action_code or "").upper() == "SEND_PROFORMA":
        return f"""{greeting}

Segue em anexo o orçamento para pagamento solicitado.

Após pagamento, envie por favor o comprovativo para confirmação e seguimento da encomenda.

{closing}"""
    if str(action_code or "").upper() == "SEND_INVOICE":
        return f"""{greeting}

Segue em anexo a fatura solicitada.

Qualquer questão, estamos ao dispor.

{closing}"""
    if str(action_code or "").upper() == "SEND_QUOTE":
        return f"""{greeting}

Segue em anexo a proposta solicitada.

Qualquer questão ou ajuste necessário, estamos ao dispor.

{closing}"""
    return suggested_reply_for_task(task)


def _effective_suggested_reply_for_task(task: dict, action_code: str, prep_vm: dict, missing_items: list[dict]) -> str:
    suggested = str(prep_vm.get("suggested_reply") or "").strip()
    if not missing_items and str(action_code or "").upper() in DOCUMENT_TASK_ACTIONS:
        # Avoid showing stale preparation text asking for fiscal data that is now complete.
        stale_markers = ("dados de fatur", "dados para fatur", "nif", "morada fiscal", "email de fatur")
        if not suggested or any(marker in suggested.lower() for marker in stale_markers):
            return _ready_document_reply_for_task(task, action_code)
    return suggested or suggested_reply_for_task(task)


def _tasks_for_filters(status: Optional[str] = "pending", route: Optional[str] = None, view: Optional[str] = None, q: Optional[str] = None, limit: int = 200):
    effective_status = status or "pending"
    status_filter = None if effective_status == "all" else effective_status
    q = _safe_search_query(q)
    tasks = list_tasks(status=status_filter, route=route, q=q, limit=limit)
    if view == "overdue":
        tasks = [task for task in tasks if is_task_overdue(task)]
    elif view == "today":
        tasks = [task for task in tasks if is_task_today(task)]
    elif view == "followups":
        tasks = [task for task in tasks if _is_follow_up_task(task)]
    elif view == "followups_due":
        tasks = [task for task in tasks if _is_follow_up_task(task) and is_task_overdue(task)]
    return tasks


def render_tasks_list_partial(tasks: list[dict], return_to: str = "/tasks?status=pending") -> str:
    rows = ""
    for task in tasks:
        task_id = str(task.get("id") or "")
        task_url = _task_href(task_id, return_to)
        action_code = str(task.get("action_code") or "")
        customer = _task_display_name(task)
        subject = compact_text(task.get("message_subject") or "", 80)
        detail = compact_text(task_next_action_text(task), 150)
        opp_id = opportunity_id_from_task(task)
        source_system = str(task.get("source_system") or "").strip()
        conversation_id = str(task.get("conversation_id") or "").strip()
        source_line = ""
        if source_system or conversation_id:
            source_bits = []
            if source_system:
                source_bits.append(source_system.capitalize())
            if conversation_id:
                source_bits.append(f"conversa #{conversation_id}")
            source_line = '<div class="small text-secondary">Origem: ' + esc(" · ".join(source_bits)) + '</div>'
        opp_line = f'<div class="small"><a class="cf-inline-link" href="/opportunities/{esc(opp_id)}">Abrir oportunidade</a></div>' if opp_id else '<div class="small text-secondary">Sem oportunidade associada</div>'
        rows += f'''
        <tr>
          <td>{task_priority_chip(task)}<div class="mt-1"><span class="cf-chip cf-chip-gray">task</span></div></td>
          <td>
            <a class="cf-row-link" href="{esc(task_url)}">{esc(customer)}</a>
            <div class="small text-secondary text-break">{esc(subject or '—')}</div>
            {source_line}
            {opp_line}
          </td>
          <td>{route_badge(task.get('route'))}</td>
          <td>{status_badge(task.get('status'))}<div class="mt-1 d-flex flex-wrap gap-1">{sla_badge_html(task)}{_task_due_chip(task)}</div></td>
          <td>
            <strong>{esc(action_label(action_code))}</strong>
            <div class="small text-secondary text-break mt-1">{esc(detail or '—')}</div>
          </td>
          <td class="text-end"><div class="d-flex justify-content-end gap-1 flex-wrap"><a class="btn btn-sm btn-outline-primary" href="{esc(task_url)}">Abrir</a>{chatwoot_button(task.get('conversation_id'), 'Chatwoot') if str(task.get('conversation_id') or '').strip() else ''}</div></td>
        </tr>
        '''
    if not rows:
        rows = '<tr><td colspan="6" class="text-center text-secondary py-5">Sem tarefas para estes filtros.</td></tr>'
    return f'''
    <div id="tasks-list" class="cf-live-panel" aria-live="polite">
      <div class="d-flex justify-content-between align-items-center gap-2 mb-2">
        <span class="small text-secondary">{len(tasks)} resultado(s)</span>
        <span class="small text-secondary htmx-indicator" id="tasks-loading">A atualizar…</span>
      </div>
      <section class="card cf-card"><div class="cf-table-wrap border-0 rounded-0"><table class="table cf-table"><thead><tr><th>Prioridade</th><th>Cliente / oportunidade</th><th>Fila</th><th>Estado</th><th>Próxima ação</th><th></th></tr></thead><tbody>{rows}</tbody></table></div></section>
    </div>
    '''


@router.get("/tasks/partials/list", response_class=HTMLResponse)
async def tasks_list_partial(status: Optional[str] = "pending", route: Optional[str] = None, view: Optional[str] = None, q: Optional[str] = None, limit: int = 200):
    tasks = _tasks_for_filters(status=status, route=route, view=view, q=q, limit=limit)
    return_to = _tasks_list_return_to(status=status, route=route, view=view, q=q, limit=limit)
    return HTMLResponse(render_tasks_list_partial(tasks, return_to=return_to))


@router.get("/tasks", response_class=HTMLResponse)
async def tasks_page(
    status: Optional[str] = "pending",
    route: Optional[str] = None,
    view: Optional[str] = None,
    q: Optional[str] = None,
    limit: int = 200,
):
    effective_status = status or "pending"
    tasks = _tasks_for_filters(status=status, route=route, view=view, q=q, limit=limit)
    return_to = _tasks_list_return_to(status=effective_status, route=route, view=view, q=q, limit=limit)

    metrics = get_admin_dashboard_metrics()

    def n(key):
        return int(metrics.get(key) or 0)

    active_key = view if view else (route if route else effective_status)
    tabs = [
        ("pending", "Pendentes", n("pending_total"), "/tasks?status=pending"),
        ("overdue", "Atrasadas", n("overdue_total"), "/tasks?status=pending&view=overdue"),
        ("followups", "Follow-ups", n("pending_followups"), "/tasks?status=pending&view=followups"),
        ("followups_due", "Follow-ups vencidos", n("due_followups"), "/tasks?status=pending&view=followups_due"),
        ("vendas", "Vendas", n("pending_vendas"), "/tasks?status=pending&route=vendas"),
        ("financeiro", "Financeiro", n("pending_financeiro"), "/tasks?status=pending&route=financeiro"),
        ("operacoes", "Operações", n("pending_operacoes"), "/tasks?status=pending&route=operacoes"),
        ("rever", "Revisão", n("pending_rever"), "/tasks?status=pending&route=rever"),
        ("all", "Todas", n("pending_total") + n("done_total") + n("skipped_total") + n("failed_total"), "/tasks?status=all"),
    ]
    tab_html = "".join(
        f'<a class="cf-task-tab {"active" if key == active_key else ""}" href="{href}" hx-get="/tasks/partials/list{href[href.find("?"):] if "?" in href else ""}" hx-target="#tasks-list" hx-swap="outerHTML" hx-push-url="{href}" hx-indicator="#tasks-loading">{esc(label)} <span>{count}</span></a>'
        for key, label, count, href in tabs
    )

    selected = lambda value, current: "selected" if str(value or "") == str(current or "") else ""

    cards = ""
    for task in tasks:
        task_id = str(task.get("id") or "")
        task_url = _task_href(task_id, return_to)
        action_code = str(task.get("action_code") or "")
        customer = _task_display_name(task)
        subject = compact_text(task.get("message_subject") or "Sem assunto", 80)
        next_action = task_next_action_text(task)
        message = compact_text(task.get("request_text") or task.get("note") or "", 130)
        opp_id = opportunity_id_from_task(task)
        opp_html = f'<a class="cf-inline-link" href="/opportunities/{esc(opp_id)}">Oportunidade</a>' if opp_id else '<span class="text-secondary">Sem oportunidade</span>'
        cards += f'''
        <article class="cf-task-card">
          <div class="cf-task-card-head">
            <div>
              <div class="cf-task-action">{esc(action_label(action_code))}</div>
              <h2><a href="{esc(task_url)}">{esc(customer)}</a></h2>
            </div>
            <div class="cf-task-badges">{task_priority_chip(task)}{status_badge(task.get('status'))}</div>
          </div>
          <div class="cf-task-next">
            <span>Próxima ação</span>
            <strong>{esc(next_action)}</strong>
          </div>
          <div class="cf-task-meta-row">
            {route_badge(task.get('route'))}
            {sla_badge_html(task)}
            {_task_due_chip(task)}
            <span class="cf-chip cf-chip-gray">{esc(fmt_dt(task.get('updated_at') or task.get('created_at')))}</span>
          </div>
          <p class="cf-task-message">{esc(message or subject or '—')}</p>
          <div class="cf-task-card-foot">
            {opp_html}
            <a class="btn btn-sm btn-primary" href="{esc(task_url)}">Abrir</a>
            {chatwoot_button(task.get('conversation_id'), 'Chatwoot') if str(task.get('conversation_id') or '').strip() else ''}
          </div>
        </article>
        '''

    if not cards:
        cards = '<section class="cf-empty">Sem tarefas para estes filtros.</section>'

    table_rows = ""
    for task in tasks:
        task_id = str(task.get("id") or "")
        task_url = _task_href(task_id, return_to)
        action_code = str(task.get("action_code") or "")
        customer = _task_display_name(task)
        subject = compact_text(task.get("message_subject") or "", 80)
        detail = compact_text(task_next_action_text(task), 150)
        opp_id = opportunity_id_from_task(task)
        source_system = str(task.get("source_system") or "").strip()
        conversation_id = str(task.get("conversation_id") or "").strip()
        source_line = ""
        if source_system or conversation_id:
            source_bits = []
            if source_system:
                source_bits.append(source_system.capitalize())
            if conversation_id:
                source_bits.append(f"conversa #{conversation_id}")
            source_line = '<div class="small text-secondary">Origem: ' + esc(" · ".join(source_bits)) + '</div>'
        opp_line = f'<div class="small"><a class="cf-inline-link" href="/opportunities/{esc(opp_id)}">Abrir oportunidade</a></div>' if opp_id else '<div class="small text-secondary">Sem oportunidade associada</div>'
        table_rows += f'''
        <tr>
          <td>{task_priority_chip(task)}<div class="mt-1"><span class="cf-chip cf-chip-gray">task</span></div></td>
          <td>
            <a class="cf-row-link" href="{esc(task_url)}">{esc(customer)}</a>
            <div class="small text-secondary text-break">{esc(subject or '—')}</div>
            {source_line}
            {opp_line}
          </td>
          <td>{route_badge(task.get('route'))}</td>
          <td>{status_badge(task.get('status'))}<div class="mt-1 d-flex flex-wrap gap-1">{sla_badge_html(task)}{_task_due_chip(task)}</div></td>
          <td>
            <strong>{esc(action_label(action_code))}</strong>
            <div class="small text-secondary text-break mt-1">{esc(detail or '—')}</div>
          </td>
          <td class="text-end"><div class="d-flex justify-content-end gap-1 flex-wrap"><a class="btn btn-sm btn-outline-primary" href="{esc(task_url)}">Abrir</a>{chatwoot_button(task.get('conversation_id'), 'Chatwoot') if str(task.get('conversation_id') or '').strip() else ''}</div></td>
        </tr>
        '''
    if not table_rows:
        table_rows = '<tr><td colspan="6" class="text-center text-secondary py-5">Sem tarefas para estes filtros.</td></tr>'

    body = f'''
    <style>
      .cf-task-tabs {{ display:flex; flex-wrap:wrap; gap:.55rem; margin-bottom:1rem; }}
      .cf-task-tab {{ display:inline-flex; align-items:center; gap:.45rem; padding:.62rem .86rem; border-radius:999px; background:#fff; border:1px solid #e2e8f0; color:#334155; text-decoration:none; font-weight:800; }}
      .cf-task-tab span {{ background:#f1f5f9; color:#475569; padding:.08rem .45rem; border-radius:999px; font-size:.75rem; }}
      .cf-task-tab.active {{ color:#fff; background:#0d6efd; border-color:#0d6efd; }}
      .cf-task-tab.active span {{ background:rgba(255,255,255,.22); color:#fff; }}
      .cf-task-hero-grid {{ display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:1rem; margin-bottom:1rem; }}
      .cf-task-hero-card {{ background:#fff; border:1px solid #e5e7eb; border-radius:1rem; padding:1rem; box-shadow:var(--cf-shadow); }}
      .cf-task-hero-card span {{ color:#64748b; font-size:.78rem; font-weight:800; text-transform:uppercase; }}
      .cf-task-hero-card strong {{ display:block; font-size:1.65rem; line-height:1.1; margin-top:.25rem; }}
      .cf-task-grid-list {{ display:grid; grid-template-columns:repeat(auto-fill,minmax(310px,1fr)); gap:1rem; }}
      .cf-task-card {{ background:#fff; border:1px solid #e5e7eb; border-radius:1.1rem; padding:1rem; box-shadow:var(--cf-shadow); display:grid; gap:.8rem; }}
      .cf-task-card-head {{ display:flex; justify-content:space-between; gap:1rem; align-items:flex-start; }}
      .cf-task-card h2 {{ font-size:1.02rem; margin:.15rem 0 0; line-height:1.25; }}
      .cf-task-card h2 a {{ color:#0f172a; text-decoration:none; }}
      .cf-task-action {{ color:#0d6efd; font-weight:900; font-size:.78rem; text-transform:uppercase; letter-spacing:.02em; }}
      .cf-task-badges {{ display:flex; flex-direction:column; gap:.35rem; align-items:flex-end; }}
      .cf-task-next {{ background:#f8fafc; border:1px solid #e2e8f0; border-radius:.9rem; padding:.8rem; }}
      .cf-task-next span {{ color:#64748b; font-size:.74rem; font-weight:900; text-transform:uppercase; }}
      .cf-task-next strong {{ display:block; margin-top:.15rem; }}
      .cf-task-meta-row {{ display:flex; flex-wrap:wrap; gap:.45rem; align-items:center; }}
      .cf-task-message {{ color:#475569; margin:0; line-height:1.45; min-height:2.8em; }}
      .cf-task-card-foot {{ display:flex; justify-content:space-between; align-items:center; gap:.75rem; border-top:1px solid #eef2f7; padding-top:.8rem; }}
      .cf-inline-link {{ color:#0d6efd; font-weight:800; text-decoration:none; }}
      @media (max-width: 1000px) {{ .cf-task-hero-grid {{ grid-template-columns:repeat(2,minmax(0,1fr)); }} }}
      @media (max-width: 640px) {{ .cf-task-hero-grid {{ grid-template-columns:1fr; }} .cf-task-badges {{ align-items:flex-start; }} .cf-task-card-head {{ flex-direction:column; }} }}
    </style>

    <section class="cf-task-hero-grid">
      <a class="cf-task-hero-card text-reset" href="/tasks?status=pending"><span>Pendentes</span><strong>{n('pending_total')}</strong><small>precisam de ação</small></a>
      <a class="cf-task-hero-card text-reset" href="/tasks?status=pending&view=overdue"><span>Atrasadas</span><strong>{n('overdue_total')}</strong><small>prioridade máxima</small></a>
      <a class="cf-task-hero-card text-reset" href="/tasks?status=pending&route=financeiro"><span>Financeiro</span><strong>{n('pending_financeiro')}</strong><small>pagamentos/faturas</small></a>
      <a class="cf-task-hero-card text-reset" href="/tasks?status=pending&route=operacoes"><span>Operações</span><strong>{n('pending_operacoes')}</strong><small>envios/recolhas</small></a>
      <a class="cf-task-hero-card text-reset" href="/tasks?status=pending&view=followups"><span>Follow-ups</span><strong>{n('pending_followups')}</strong><small>semi-automáticos</small></a>
    </section>

    <nav class="cf-task-tabs" aria-label="Filtros rápidos">{tab_html}</nav>

    <section class="card cf-card cf-filter-card">
      <form method="get" action="/tasks" class="row g-3 align-items-end" hx-get="/tasks/partials/list" hx-target="#tasks-list" hx-swap="outerHTML" hx-push-url="true" hx-indicator="#tasks-loading">
        <div class="col-lg-5"><label class="form-label small fw-bold text-secondary">Procurar</label><input class="form-control" type="search" name="q" value="{esc(q or '')}" placeholder="cliente, email, ação, assunto..."></div>
        <div class="col-md-2"><label class="form-label small fw-bold text-secondary">Fila</label><select class="form-select" name="route"><option value="" {selected('', route)}>Todas</option><option value="vendas" {selected('vendas', route)}>Vendas</option><option value="financeiro" {selected('financeiro', route)}>Financeiro</option><option value="operacoes" {selected('operacoes', route)}>Operações</option><option value="suporte" {selected('suporte', route)}>Suporte</option><option value="rever" {selected('rever', route)}>Revisão</option></select></div>
        <div class="col-md-2"><label class="form-label small fw-bold text-secondary">Estado</label><select class="form-select" name="status"><option value="all" {selected('all', effective_status)}>Todos</option><option value="pending" {selected('pending', effective_status)}>Pendentes</option><option value="done" {selected('done', effective_status)}>Concluídas</option><option value="skipped" {selected('skipped', effective_status)}>Ignoradas</option><option value="failed" {selected('failed', effective_status)}>Falhas</option></select></div>
        <div class="col-md-3 d-flex gap-2"><button class="btn btn-primary flex-fill" type="submit">Filtrar</button><a class="btn btn-outline-secondary" href="/tasks">Limpar</a></div>
      </form>
    </section>

    <section class="card cf-card mb-3"><div class="card-body d-flex flex-wrap justify-content-between align-items-center gap-2"><div><h2 class="cf-section-title mb-1">Lista de tarefas abertas</h2><div class="small text-secondary">Mesma leitura da Fila operacional: prioridade, cliente/oportunidade, fila, estado e próxima ação. Esta página mostra apenas tasks humanas.</div></div><span class="cf-chip cf-chip-gray">{len(tasks)} resultado(s)</span></div></section>

    {render_tasks_list_partial(tasks, return_to=return_to)}
    '''

    return layout("Tarefas", "Fila operacional com foco na próxima ação", body, "tasks")




def _reply_assistant_panel_html(task_id: str, state: dict | None = None, notice: str = "", error: str = "") -> str:
    """Render the editable reply assistant panel for a task."""
    try:
        from app.message_templates import default_template_for_action, list_templates_for_action
        from app.reply_assistant_service import get_reply_panel_state

        task = get_task_detail(task_id)
        if not task:
            return '<section id="reply-assistant-panel" class="card cf-card"><div class="card-body p-4 text-secondary">Tarefa não encontrada.</div></section>'
        if state is None:
            state = get_reply_panel_state(task_id)
        action_code = str(task.get("action_code") or "")
        current_template_code = str((state.get("template") or {}).get("code") or default_template_for_action(action_code).code)
        templates = list_templates_for_action(action_code)
        template_options = "".join(
            f'<option value="{esc(tpl.code)}" {"selected" if tpl.code == current_template_code else ""}>{esc(tpl.name)}</option>'
            for tpl in templates
        )
        selected_ids = {str(item) for item in (state.get("selected_document_ids") or [])}
        documents = state.get("documents") or []
        doc_checks = ""
        for doc in documents:
            doc_id = str(doc.get("id") or "")
            checked = "checked" if doc_id in selected_ids else ""
            disabled_hint = "" if doc.get("pdf_supported") else '<span class="small text-warning ms-1">PDF automático indisponível</span>'
            label = doc.get('label') or doc.get('document_number') or doc_id
            system = str(doc.get('system') or '')
            kind = str(doc.get('document_kind') or '')
            doc_checks += (
                f'<label class="cf-reply-doc form-check border rounded-3 p-2 d-flex align-items-start gap-2">'
                f'<input class="form-check-input mt-1" type="checkbox" name="selected_document_ids" value="{esc(doc_id)}" {checked}>'
                f'<span class="d-grid"><strong>{esc(label)}</strong><small class="text-secondary">{esc(system)} · {esc(kind)}</small>{disabled_hint}</span>'
                f'</label>'
            )
        if not doc_checks:
            doc_checks = '<div class="text-secondary small border rounded-3 p-3">Sem documentos/anexos associados à oportunidade.</div>'

        warnings = state.get("warnings") or []
        blockers = state.get("blockers") or []
        warning_html = "".join(f'<div class="alert alert-warning py-2 mb-2">{esc(item)}</div>' for item in warnings)
        blocker_html = "".join(f'<div class="alert alert-danger py-2 mb-2">{esc(item)}</div>' for item in blockers)
        notice_html = f'<div class="alert alert-success py-2 mb-2">{esc(notice)}</div>' if notice else ""
        error_html = f'<div class="alert alert-danger py-2 mb-2" style="white-space:pre-wrap">{esc(error)}</div>' if error else ""
        draft_id = str(state.get("draft_id") or "")
        message_body = str(state.get("message_body") or "")
        selected_hidden = "".join(f'<input type="hidden" name="selected_document_ids" value="{esc(doc_id)}">' for doc_id in selected_ids)
        # The legacy eager reply panel also renders the send form. Keep the
        # operator instruction hidden field defined even when the current state
        # does not carry one, otherwise support/reply tasks can show
        # "Assistente indisponível: name 'instruction_hidden' is not defined".
        operator_instruction = str(state.get("operator_instruction") or state.get("instruction") or "")
        instruction_hidden = f'<input type="hidden" name="operator_instruction" value="{esc(operator_instruction)}">'
        knowledge = state.get("business_knowledge") or {}
        topics = knowledge.get("topics") or []
        knowledge_html = ""
        if topics:
            topic_items = ""
            for topic in topics[:3]:
                facts = topic.get("facts") or []
                facts_html = "".join(f'<li>{esc(str(fact))}</li>' for fact in facts[:3])
                topic_items += f'''
                  <div class="border rounded-3 p-2 mb-2 bg-light">
                    <div class="small fw-bold">{esc(topic.get('title') or topic.get('id') or 'Conhecimento BLIF')}</div>
                    <div class="small text-secondary">{esc(topic.get('summary') or '')}</div>
                    <ul class="small text-secondary mb-0 mt-1">{facts_html}</ul>
                  </div>
                '''
            reply_type = str(knowledge.get("reply_type") or "")
            knowledge_html = f'''
              <details class="mb-3" open>
                <summary class="small fw-bold text-secondary">Conhecimento BLIF usado · {esc(reply_type or 'resposta')}</summary>
                <div class="mt-2">{topic_items}</div>
              </details>
            '''
        intent_gate = state.get("intent_gate") or {}
        intent_html = ""
        if intent_gate:
            reasons = "; ".join(str(item) for item in (intent_gate.get("reasons") or []))
            label = str(intent_gate.get("label") or intent_gate.get("category") or "Triagem")
            category = str(intent_gate.get("category") or "")
            badge = "text-bg-warning" if intent_gate.get("requires_manual_review") else "text-bg-info"
            intent_html = f"""
              <div class="alert alert-light border py-2 mb-3">
                <div class="small fw-bold">Diagnóstico da IA · triagem <span class="badge {badge}">{esc(category)}</span></div>
                <div class="small text-secondary">{esc(label)}{(" · " + esc(reasons)) if reasons else ""}</div>
              </div>
            """

        email_agent = state.get("email_agent") or {}
        email_agent_html = ""
        if email_agent:
            if email_agent.get("enabled"):
                status = "usado" if email_agent.get("used") else "fallback"
                intent = str(email_agent.get("intencao") or "")
                confidence = str(email_agent.get("nivel_confianca") or "")
                review = bool(email_agent.get("precisa_revisao_humana"))
                review_badge = '<span class="badge text-bg-warning ms-1">revisão humana</span>' if review else '<span class="badge text-bg-success ms-1">rascunho simples</span>'
                details = " · ".join(part for part in [intent, confidence] if part)
                email_agent_html = f'''
                  <div class="alert alert-light border py-2 mb-3">
                    <div class="small fw-bold">Agente de email OpenAI/file_search <span class="badge text-bg-info">{esc(status)}</span>{review_badge}</div>
                    <div class="small text-secondary">{esc(details or str(email_agent.get('error') or ''))}</div>
                  </div>
                '''
            elif email_agent.get("error"):
                email_agent_html = f'<div class="small text-secondary mb-2">Agente de email OpenAI: fallback · {esc(str(email_agent.get("error") or ""))}</div>'

        llm = state.get("llm") or {}
        llm_html = ""
        if llm.get("enabled"):
            status = "usado" if llm.get("used") else "fallback"
            detail = str(llm.get("error") or llm.get("intent") or "")
            llm_html = f'<div class="small text-secondary mb-2">LLM OpenRouter: {esc(status)}{(" · " + esc(detail)) if detail else ""}</div>'
        elif topics:
            llm_html = '<div class="small text-secondary mb-2">LLM OpenRouter: desativado; rascunho gerado por conhecimento BLIF determinístico.</div>'

        return f'''
        <section id="reply-assistant-panel" class="card cf-card">
          <div class="card-body p-4">
            <div class="d-flex justify-content-between align-items-start gap-3 mb-3">
              <div>
                <h2 class="cf-section-title mb-1">Resposta ao cliente</h2>
                <div class="small text-secondary">Usa conhecimento BLIF, modelos comerciais e apenas anexos ligados à oportunidade desta tarefa.</div>
              </div>
              <span class="badge text-bg-primary-subtle text-primary border border-primary-subtle">v4928.1.5.14</span>
            </div>
            {notice_html}{error_html}{blocker_html}{warning_html}
            {intent_html}
            {knowledge_html}
            {email_agent_html}
            {llm_html}

            <form method="post" action="/tasks/{esc(task_id)}/reply-draft" hx-post="/tasks/{esc(task_id)}/reply-draft" hx-target="#reply-assistant-panel" hx-swap="outerHTML" hx-indicator="#reply-assistant-loading" class="vstack gap-3 mb-3">
              <div>
                <label class="form-label small fw-bold text-secondary">Modelo</label>
                <select class="form-select" name="template_code">{template_options}</select>
              </div>
              <div>
                <div class="form-label small fw-bold text-secondary">Anexos da oportunidade</div>
                <div class="vstack gap-2">{doc_checks}</div>
              </div>
              <button class="btn btn-outline-primary" type="submit">Gerar rascunho</button>
            </form>

            <form method="post" action="/tasks/{esc(task_id)}/send-reply" hx-post="/tasks/{esc(task_id)}/send-reply" hx-target="#task-detail-panel" hx-swap="outerHTML" hx-indicator="#reply-assistant-loading" class="vstack gap-3">
              <input type="hidden" name="template_code" value="{esc(current_template_code)}">
              <input type="hidden" name="draft_id" value="{esc(draft_id)}">
              {selected_hidden}
              {instruction_hidden}
              <div>
                <label class="form-label small fw-bold text-secondary">Mensagem editável</label>
                <textarea id="reply-message-body" class="form-control" name="message_body" rows="8">{esc(message_body)}</textarea>
              </div>
              <div class="d-flex flex-wrap gap-2 align-items-center">
                <button class="btn btn-primary" type="submit" name="send_and_complete" value="0" hx-confirm="Enviar esta mensagem ao cliente no Chatwoot?">Enviar</button>
                <button class="btn btn-success" type="submit" name="send_and_complete" value="1" hx-confirm="Enviar ao cliente e concluir a tarefa?">Enviar e concluir tarefa</button>
                <span class="small text-secondary htmx-indicator" id="reply-assistant-loading">A processar…</span>
              </div>
            </form>
          </div>
        </section>
        '''
    except Exception as exc:
        return f'<section id="reply-assistant-panel" class="card cf-card"><div class="card-body p-4"><h2 class="cf-section-title">Resposta ao cliente</h2><div class="alert alert-warning mb-0">Assistente indisponível: {esc(str(exc))}</div></div></section>'


def _latest_message_draft_for_task(task_id: str) -> dict:
    """Load the latest persisted editable draft without invoking the LLM.

    This keeps the task detail page fast while making generated drafts survive a
    browser refresh. The expensive OpenAI/file_search generation still happens
    only in /tasks/{task_id}/reply-draft.
    """
    if not is_uuid_text(str(task_id or "")):
        return {}
    try:
        from sqlalchemy import text as sa_text
        from app.db import engine

        with engine.begin() as conn:
            row = conn.execute(sa_text("""
                SELECT
                  id::text AS id,
                  task_id::text AS task_id,
                  template_code,
                  message_body,
                  selected_document_ids::text AS selected_document_ids,
                  operator_instruction,
                  generated_by,
                  status,
                  updated_at
                FROM message_drafts
                WHERE task_id = CAST(:task_id AS UUID)
                  AND status = 'draft'
                ORDER BY updated_at DESC NULLS LAST, created_at DESC
                LIMIT 1
            """), {"task_id": task_id}).mappings().first()
        if not row:
            return {}
        selected_raw = row.get("selected_document_ids") or "[]"
        try:
            selected_ids = json.loads(selected_raw) if isinstance(selected_raw, str) else list(selected_raw or [])
        except Exception:
            selected_ids = []
        return {
            "draft_id": str(row.get("id") or ""),
            "template_code": str(row.get("template_code") or ""),
            "message_body": str(row.get("message_body") or ""),
            "selected_document_ids": [str(item) for item in selected_ids],
            "operator_instruction": str(row.get("operator_instruction") or ""),
            "generated_by": str(row.get("generated_by") or ""),
            "updated_at": str(row.get("updated_at") or ""),
        }
    except Exception:
        return {}

NON_COMMUNICATION_TASK_ACTIONS = {
    "CONFIRM_PAYMENT",
    "PREPARE_ORDER",
    "VALIDATE_PHYSICAL_ORDER",
    "CREATE_SHIPMENT",
    "REVIEW_RECONSTRUCTED_PROCESS",
}


def _task_reply_assistant_html(task_id: str, action_code: str) -> str:
    if str(action_code or "").upper() in NON_COMMUNICATION_TASK_ACTIONS:
        return ""
    return _reply_assistant_lazy_panel_html(task_id)


def _reply_assistant_lazy_panel_html(task_id: str, notice: str = "") -> str:
    """Render a lightweight reply panel without calling LLM/vector-store.

    The full assistant state can be slow because it may call the email reply
    agent, OpenRouter/OpenAI and knowledge retrieval. Task detail pages should
    open fast; the expensive generation is triggered only by the operator via
    /tasks/{task_id}/reply-draft. v4928.1.5.15 also reloads the latest
    persisted message_draft after refresh without invoking the LLM.
    """
    notice_html = f'<div class="alert alert-success py-2 mb-2">{esc(notice)}</div>' if notice else ""
    try:
        task = get_task_detail(task_id) or {}
    except Exception:
        task = {}
    action_code = str(task.get("action_code") or "")
    is_follow_up = _is_follow_up_task(action_code)
    default_template_code = _follow_up_template_for_action(action_code) if is_follow_up else ""
    panel_title = "Rascunho de follow-up" if is_follow_up else "Resposta ao cliente"
    generate_label = "Gerar rascunho personalizado com IA" if is_follow_up else "Gerar rascunho"
    regenerate_label = "Regenerar rascunho personalizado com IA" if is_follow_up else "Regenerar rascunho"
    mode_badge = "lazy · follow-up · OpenAI · v4928.1.5.24" if is_follow_up else "lazy · v4928.1.5.16"
    help_text = (
        "A tarefa abre sem chamar IA. Este botão chama OpenAI com um prompt específico de follow-up e personaliza a mensagem com dados do cliente, oportunidade, documentos e histórico recente. Nada é enviado automaticamente."
        if is_follow_up else
        "A tarefa abre sem chamar IA. Gera o rascunho apenas quando precisares de responder; a geração usa OpenAI e o histórico recente da conversa."
    )
    draft = _latest_message_draft_for_task(task_id)
    if draft.get("message_body"):
        draft_id = str(draft.get("draft_id") or "")
        template_code = str(draft.get("template_code") or "")
        message_body = str(draft.get("message_body") or "")
        template_code = _customer_send_template_code_for_task(task, template_code, message_body)
        send_conversation_id = _task_effective_conversation_id(task)
        selected_ids = [str(item) for item in (draft.get("selected_document_ids") or [])]
        selected_ids = _effective_selected_doc_ids_for_task(task, selected_ids)
        operator_instruction = str(draft.get("operator_instruction") or "")
        selected_hidden = "".join(f'<input type="hidden" name="selected_document_ids" value="{esc(doc_id)}">' for doc_id in selected_ids)
        instruction_hidden = f'<input type="hidden" name="operator_instruction" value="{esc(operator_instruction)}">'
        documents_html = _followup_documents_picker_html(task, selected_ids, compact=True) if is_follow_up else _followup_documents_picker_html(task, selected_ids, compact=True)
        email_to = _task_email(task)
        subject_for_email = str(task.get("message_subject") or task.get("opportunity_title") or "Seguimento do processo")
        email_hidden = f'<input type="hidden" name="to_email" value="{esc(email_to)}"><input type="hidden" name="subject" value="{esc(subject_for_email)}">'
        updated_at = str(draft.get("updated_at") or "")
        generated_by = str(draft.get("generated_by") or "")
        meta_bits = " · ".join(part for part in ["rascunho guardado", generated_by, updated_at[:19]] if part)
        return f"""
        <section id="reply-assistant-panel" class="card cf-card">
          <div class="card-body p-4">
            <div class="d-flex justify-content-between align-items-start gap-3 mb-3">
              <div>
                <h2 class="cf-section-title mb-1">{esc(panel_title)}</h2>
                <div class="small text-secondary">{esc(meta_bits)}. Podes editar, guardar ou pedir uma correção à IA antes de enviar.</div>
              </div>
              <span class="badge text-bg-success-subtle text-success border border-success-subtle">v4928.1.5.121 · persistente</span>
            </div>
            {notice_html}

            <form method="post"
                  action="/tasks/{esc(task_id)}/reply-draft"
                  hx-post="/tasks/{esc(task_id)}/reply-draft"
                  hx-target="#reply-assistant-panel"
                  hx-swap="outerHTML"
                  hx-indicator="#reply-assistant-loading"
                  class="d-flex flex-wrap gap-2 align-items-center mb-3">
              <input type="hidden" name="template_code" value="{esc(template_code)}">
              {selected_hidden}
              {instruction_hidden}
              <button class="btn btn-outline-primary" type="submit" hx-confirm="Gerar novo rascunho e substituir visualmente o rascunho atual?">{esc(regenerate_label)}</button>
              <span class="small text-secondary">A abertura da tarefa não chama IA; este botão chama OpenAI com regras específicas para follow-up.</span>
            </form>

            <form id="reply-save-form"
                  method="post"
                  action="/tasks/{esc(task_id)}/reply-draft/save"
                  hx-post="/tasks/{esc(task_id)}/reply-draft/save"
                  hx-target="#reply-assistant-panel"
                  hx-swap="outerHTML"
                  hx-indicator="#reply-assistant-loading"
                  onsubmit="syncReplyDraftBody(this)"
                  class="d-none">
              <input type="hidden" name="template_code" value="{esc(template_code)}">
              <input type="hidden" name="draft_id" value="{esc(draft_id)}">
              {selected_hidden}
              {instruction_hidden}
              <input type="hidden" name="message_body" data-reply-body-copy value="">
            </form>

            <form id="reply-email-outbox-form"
                  method="post"
                  action="/tasks/{esc(task_id)}/reply-draft/email-outbox"
                  hx-post="/tasks/{esc(task_id)}/reply-draft/email-outbox"
                  hx-target="#reply-assistant-panel"
                  hx-swap="outerHTML"
                  hx-indicator="#reply-assistant-loading"
                  onsubmit="syncReplyDraftBody(this)"
                  class="d-none">
              <input type="hidden" name="template_code" value="{esc(template_code)}">
              <input type="hidden" name="draft_id" value="{esc(draft_id)}">
              {selected_hidden}
              {instruction_hidden}
              {email_hidden}
              <input type="hidden" name="message_body" data-reply-body-copy value="">
            </form>

            {f'<div class="alert alert-light border py-2 mb-3"><div class="small fw-bold">Objetivo/instrução do operador</div><div class="small text-secondary">{esc(operator_instruction) if operator_instruction else "Sem instrução adicional."}</div></div>'}
            {documents_html}

            <form id="reply-revise-form"
                  method="post"
                  action="/tasks/{esc(task_id)}/reply-draft/revise"
                  hx-post="/tasks/{esc(task_id)}/reply-draft/revise"
                  hx-target="#reply-assistant-panel"
                  hx-swap="outerHTML"
                  hx-indicator="#reply-assistant-loading"
                  onsubmit="syncReplyDraftBody(this)"
                  class="border rounded-3 p-3 bg-light mb-3">
              <input type="hidden" name="template_code" value="{esc(template_code)}">
              <input type="hidden" name="draft_id" value="{esc(draft_id)}">
              {selected_hidden}
              {instruction_hidden}
              <input type="hidden" name="message_body" data-reply-body-copy value="">
              <label class="form-label small fw-bold text-secondary">Correção com IA</label>
              <textarea class="form-control" name="revision_instruction" rows="2" placeholder="Ex.: adicionar preço do carregador monofásico; acrescentar URL da ficha técnica; tornar mais curto; mencionar suporte remoto ao eletricista"></textarea>
              <div class="small text-secondary mt-2">A correção usa o rascunho atual, a instrução do operador, as últimas mensagens da conversa e conhecimento OpenAI/file_search. Não deve inventar preços, URLs nem alterar o objetivo do follow-up.</div>
              <button class="btn btn-outline-primary btn-sm mt-2" type="submit">Aplicar correção com IA</button>
            </form>

            <form id="reply-send-form" method="post" action="/tasks/{esc(task_id)}/send-reply" hx-post="/tasks/{esc(task_id)}/send-reply" hx-target="#task-detail-panel" hx-swap="outerHTML" hx-indicator="#reply-assistant-loading" class="vstack gap-3">
              <input type="hidden" name="template_code" value="{esc(template_code)}">
              <input type="hidden" name="draft_id" value="{esc(draft_id)}">
              {selected_hidden}
              {instruction_hidden}
              <div>
                <label class="form-label small fw-bold text-secondary">Mensagem editável</label>
                <textarea id="reply-message-body" class="form-control" name="message_body" rows="8">{esc(message_body)}</textarea>
                <div class="small text-secondary mt-1">Editar manualmente não envia nada; usa “Guardar rascunho” para persistir antes de sair ou refrescar.</div>
              </div>
              <div class="d-flex flex-wrap gap-2 align-items-center">
                <button class="btn btn-outline-secondary" type="submit" form="reply-save-form">Guardar rascunho</button>
                <button class="btn btn-outline-primary" type="button" onclick="navigator.clipboard && navigator.clipboard.writeText(document.getElementById('reply-message-body')?.value || '')">Copiar mensagem</button>
                {f'<button class="btn btn-outline-success" type="submit" form="reply-email-outbox-form" hx-confirm="Criar email na Outbox para {esc(email_to)}?">Criar email na Outbox</button>' if email_to else '<span class="small text-warning">Sem email: associa contacto ou copia a mensagem.</span>'}
                {f'<button class="btn btn-primary" type="submit" name="send_and_complete" value="0" hx-confirm="Enviar esta mensagem ao cliente no Chatwoot?">Enviar Chatwoot</button><button class="btn btn-success" type="submit" name="send_and_complete" value="1" hx-confirm="Enviar ao cliente e concluir a tarefa?">Enviar Chatwoot e concluir</button>' if send_conversation_id else '<span class="small text-secondary">Sem conversa Chatwoot ligada.</span>'}
                <span class="small text-secondary htmx-indicator" id="reply-assistant-loading">A processar…</span>
              </div>
            </form>

            <script>
              function syncReplyDraftBody(form) {{
                const editable = document.getElementById("reply-message-body");
                if (!editable || !form) return true;
                form.querySelectorAll("[data-reply-body-copy]").forEach(function(input) {{ input.value = editable.value || ""; }});
                return true;
              }}
            </script>
          </div>
        </section>
        """
    try:
        from app.message_templates import default_template_for_action, list_templates_for_action
        templates = list_templates_for_action(action_code)
        default_template = default_template_code or default_template_for_action(action_code).code
        template_options = "".join(
            f'<option value="{esc(tpl.code)}" {"selected" if tpl.code == default_template else ""}>{esc(tpl.name)}</option>'
            for tpl in templates
        )
    except Exception:
        template_options = f'<option value="{esc(default_template_code)}">Objetivo sugerido</option>'
    documents_picker = _followup_documents_picker_html(task) if is_follow_up else _followup_documents_picker_html(task)
    return f"""
    <section id="reply-assistant-panel" class="card cf-card">
      <div class="card-body p-4">
        <div class="d-flex justify-content-between align-items-start gap-3 mb-3">
          <div>
            <h2 class="cf-section-title mb-1">{esc(panel_title)}</h2>
            <div class="small text-secondary">{esc(help_text)} O operador escolhe o objetivo e os anexos antes de chamar IA.</div>
          </div>
          <span class="badge text-bg-light border">{esc(mode_badge)} · composer</span>
        </div>
        {notice_html}
        <form method="post"
              action="/tasks/{esc(task_id)}/reply-draft"
              hx-post="/tasks/{esc(task_id)}/reply-draft"
              hx-target="#reply-assistant-panel"
              hx-swap="outerHTML"
              hx-indicator="#reply-assistant-loading"
              class="vstack gap-3">
          <div>
            <label class="form-label small fw-bold text-secondary">Objetivo da resposta</label>
            <select class="form-select" name="template_code">{template_options}</select>
            <div class="small text-secondary mt-1">A IA deve redigir segundo este objetivo; não deve reinterpretar o próximo passo.</div>
          </div>
          {documents_picker}
          <div>
            <label class="form-label small fw-bold text-secondary">Instruções adicionais para a IA</label>
            <textarea class="form-control" name="operator_instruction" rows="2" placeholder="Ex.: enviar a fatura em anexo; ser breve; listar equipamentos e preços; mencionar suporte remoto ao eletricista"></textarea>
            <div class="small text-secondary mt-1">Não substitui validações: documentos fiscais continuam a exigir cliente fiscal e anexo/documento correto.</div>
          </div>
          <div class="d-flex flex-wrap gap-2 align-items-center">
            <button class="btn btn-outline-primary" type="submit">{esc(generate_label)}</button>
            <span class="small text-secondary htmx-indicator" id="reply-assistant-loading">A gerar com IA…</span>
          </div>
        </form>
      </div>
    </section>
    """

def render_task_detail_partial(task_id: str, notice: str = "", return_to: str = "") -> str:
    task = get_task_detail(task_id)
    if not task:
        return '<div id="task-detail-panel" class="cf-empty">Tarefa não encontrada.</div>'

    return_to = _safe_return_to(return_to, default="/tasks?status=pending")
    return_to_hidden = _return_to_hidden(return_to)
    return_link_html = _return_to_link(return_to)

    action_code = str(task.get("action_code") or "")
    status = str(task.get("status") or "")
    route_name = str(task.get("route") or "")
    contact_id = str(task.get("contact_id") or "")
    local_customer_id = str(task.get("linked_customer_id") or "")
    task_customer_id = str(task.get("customer_id") or "")
    safe_customer_id = local_customer_id or (task_customer_id if is_uuid_text(task_customer_id) else "")
    customer = _task_display_name(task)
    subject = str(task.get("message_subject") or "—")
    opportunity_id = opportunity_id_from_task(task)
    next_action = task_next_action_text(task)
    request_text = task.get("request_text") or task.get("clean_body") or task.get("raw_body") or task.get("note") or ""
    if len(str(request_text)) > 800:
        request_text = str(request_text)[:800] + "…"
    request_text = _safe_task_display_html(str(request_text))
    notice_html = f'<div class="alert alert-success border-0">{esc(notice)}</div>' if notice else ""
    customer_link = f'<a class="btn btn-sm btn-outline-secondary" href="/customers/{esc(safe_customer_id)}">Ver cliente fiscal</a>' if safe_customer_id else ""
    contact_line = f'<span class="small text-secondary">Contacto Chatwoot: {esc(contact_id)}</span>' if contact_id and not safe_customer_id else ""
    opportunity_link = _task_opportunity_navigation_html(task, opportunity_id, small=True)
    chatwoot_html = chatwoot_button(_task_effective_conversation_id(task), 'Chatwoot') if _task_effective_conversation_id(task) else ''
    identity_ctx = _task_identity_context(task)
    fiscal_customer = _safe_fiscal_customer_for_task(task, safe_customer_id)
    fiscal_contact_html = fiscal_contact_panel_html(
        fiscal_customer=fiscal_customer,
        contact_name=identity_ctx.get("contact_name") or customer,
        contact_email=identity_ctx.get("contact_email") or _task_email(task),
        contact_phone=identity_ctx.get("contact_phone") or task.get("customer_phone"),
        conversation_id=_task_effective_conversation_id(task),
        contact_id=task.get("contact_id"),
        customer_href=f"/customers/{esc(safe_customer_id)}" if safe_customer_id and fiscal_customer else "",
    )
    fiscal_missing_labels = fiscal_customer_missing_fields(fiscal_customer) if action_code in DOCUMENT_TASK_ACTIONS else []
    if identity_ctx.get("identity_unsafe") and action_code in DOCUMENT_TASK_ACTIONS:
        if not _task_has_compatible_opportunity_identity(task, identity_ctx.get("process_customer_hint")):
            fiscal_missing_labels = ["Cliente fiscal por confirmar", *fiscal_missing_labels]
            customer_link = ""
            contact_line = f'<span class="small text-secondary">Contacto Chatwoot: {esc(contact_id)}</span>' if contact_id else ""
    task_readiness_html = readiness_checklist_html(
        title="Prontidão mínima antes de documento/envio",
        missing=fiscal_missing_labels,
        ok_text="Sem bloqueios fiscais mínimos para esta tarefa.",
        blocked_text="Corrigir estes dados antes de emitir documento.",
    )
    reply_assistant_html = _task_reply_assistant_html(task_id, action_code)
    follow_up_controls_html = _follow_up_controls_html(task_id, task, return_to_hidden)
    is_follow_up = _is_follow_up_task(action_code)
    context_heading = "Contexto da tarefa" if is_follow_up else "Pedido do cliente"
    context_html = _context_task_html(task, str(request_text), is_follow_up=is_follow_up)
    channel_notice_html = _communication_channel_notice_html(task)
    contact_person_notice_html = _task_contact_person_notice_html(task) if is_follow_up else ""
    completion_heading = "Marcar follow-up como feito" if is_follow_up else "Concluir"
    external_completion_html = _external_channel_completion_form_html(task_id, task, action_code, status, return_to_hidden)
    done_controls = ""
    if status == "pending":
        done_note_options = done_note_options_html_for(action_code) or ""
        done_controls = f'''
        <form method="post" action="/tasks/{esc(task_id)}/complete-with-note" hx-post="/tasks/{esc(task_id)}/complete-with-note" hx-target="#task-detail-panel" hx-swap="outerHTML" hx-indicator="#task-detail-loading" hx-confirm="Confirmar conclusão desta tarefa?" class="vstack gap-2">
          {return_to_hidden}
          <select class="form-select" name="done_note" aria-label="Resultado">{done_note_options}</select>
          <textarea class="form-control" name="done_note_extra" rows="2" placeholder="Nota opcional"></textarea>
          <button class="btn btn-success" type="submit">Marcar como feita</button>
        </form>
        {external_completion_html}
        '''
    else:
        done_controls = f'<div class="alert alert-secondary mb-0">Estado atual: <strong>{esc(status)}</strong>.</div>'

    html = f'''
    <div id="task-detail-panel" class="cf-live-panel" aria-live="polite">
      <div class="d-flex justify-content-between align-items-center mb-2 gap-2 flex-wrap">{return_link_html}<span class="small text-secondary htmx-indicator" id="task-detail-loading">A atualizar…</span></div>
      {notice_html}
      <section class="card cf-card mb-3">
        <div class="card-body p-4 d-flex flex-wrap justify-content-between align-items-start gap-3">
          <div>
            <div class="text-primary fw-bold mb-1">Próxima ação</div>
            <h2 class="h3 fw-bold mb-2">{esc(action_label(action_code))}</h2>
            <div class="d-flex flex-wrap gap-2">{status_badge(status)}{route_badge(route_name)}<code>{esc(action_code or '—')}</code></div>
            <div class="mt-3 fw-semibold">{esc(customer)}</div>
            <div class="small text-secondary text-break">{esc(subject)}</div>
          </div>
          <div class="d-flex flex-wrap gap-2">{customer_link}{opportunity_link}{chatwoot_html}</div>
        </div>
      </section>
      <div class="row g-3">
        <main class="col-12 col-lg-8 d-grid gap-3">
          <section class="card cf-card"><div class="card-body p-4"><h2 class="cf-section-title mb-2">Próxima ação</h2><strong>{esc(next_action)}</strong><div class="d-flex flex-wrap gap-2 mt-3">{route_badge(route_name)}{status_badge(status)}{task_priority_chip(task)}</div></div></section>
          {fiscal_contact_html}
          {task_readiness_html}
          {_task_identity_warning_html(task)}
          {_task_opportunity_linking_panel_html(task, return_to)}
          {_task_public_domain_identity_warning_html(task)}
          {channel_notice_html}
          {contact_person_notice_html}
          {follow_up_controls_html}
          {reply_assistant_html}
          <section class="card cf-card"><div class="card-body p-4"><h2 class="cf-section-title mb-2">{esc(context_heading)}</h2>{context_html}</div></section>
        </main>
        <aside class="col-12 col-lg-4 d-grid gap-3 align-content-start">
          <section class="card cf-card"><div class="card-body p-4"><h2 class="cf-section-title mb-3">Ligações</h2><div class="d-grid gap-2">{customer_link}{contact_line}{opportunity_link}</div></div></section>
          <section class="card cf-card"><div class="card-body p-4"><h2 class="cf-section-title mb-3">{esc(completion_heading)}</h2>{done_controls}</div></section>
          <section class="card cf-card"><div class="card-body p-4"><h2 class="cf-section-title mb-3">Ações técnicas</h2><form method="post" action="/tasks/{esc(task_id)}/skip" hx-post="/tasks/{esc(task_id)}/skip" hx-target="#task-detail-panel" hx-swap="outerHTML" hx-indicator="#task-detail-loading" hx-confirm="Ignorar esta tarefa? Esta ação deve ser usada apenas quando não há trabalho operacional a fazer.">{return_to_hidden}<button class="btn btn-outline-danger w-100" type="submit">Ignorar tarefa</button></form></div></section>
        </aside>
      </div>
    </div>
    '''
    return _safe_task_display_html(html)


@router.get("/tasks/{task_id}/partials/detail", response_class=HTMLResponse)
async def task_detail_partial(task_id: str, return_to: str = ""):
    if not is_uuid_text(task_id):
        return _invalid_task_response()
    return HTMLResponse(render_task_detail_partial(task_id, return_to=return_to))


@router.get("/tasks/{task_id}", response_class=HTMLResponse)
async def task_detail_bootstrap_page(task_id: str, return_to: str = ""):
    if not is_uuid_text(task_id):
        return _invalid_task_response()
    task = get_task_detail(task_id)

    if not task:
        return HTMLResponse("<h1>Tarefa não encontrada</h1>", status_code=404)

    return_to = _safe_return_to(return_to, default="/tasks?status=pending")
    return_to_hidden = _return_to_hidden(return_to)
    return_link_html = _return_to_link(return_to)

    action_code = str(task.get("action_code") or "")
    route_name = str(task.get("route") or "")
    status = str(task.get("status") or "")
    conversation_id = str(task.get("conversation_id") or "")
    contact_id = str(task.get("contact_id") or "")
    local_customer_id = str(task.get("linked_customer_id") or "")
    task_customer_id = str(task.get("customer_id") or "")
    safe_customer_id = local_customer_id or (task_customer_id if is_uuid_text(task_customer_id) else "")
    customer = _task_display_name(task)
    customer_email = _task_email(task)
    customer_phone = str(task.get("customer_phone") or "")
    subject = str(task.get("message_subject") or "—")
    opportunity_id = opportunity_id_from_task(task)

    request_text = (
        task.get("request_text")
        or task.get("clean_body")
        or task.get("raw_body")
        or task.get("note")
        or ""
    )
    if len(str(request_text)) > 1600:
        request_text = str(request_text)[:1600] + "…"
    request_text = _safe_task_display_html(str(request_text))

    preparation = get_latest_task_preparation(task_id)
    prep_vm = _apply_identity_context_to_preparation(build_preparation_view_model(task, preparation), task)
    done_note_options_html = done_note_options_html_for(action_code) or ""

    public_url = (
        getattr(settings, "chatwoot_public_url", "")
        or getattr(settings, "chatwoot_base_url", "")
        or ""
    ).rstrip("/")
    account_id = getattr(settings, "chatwoot_account_id", "")

    chatwoot_link = ""
    if public_url and account_id and conversation_id:
        href = f"{public_url}/app/accounts/{account_id}/conversations/{conversation_id}"
        chatwoot_link = f'<a class="btn btn-outline-primary" href="{esc(href)}" target="_blank" rel="noopener">Abrir Chatwoot ↗</a>'

    identity_ctx = _task_identity_context(task)
    fiscal_customer = _safe_fiscal_customer_for_task(task, safe_customer_id)
    fiscal_contact_html = fiscal_contact_panel_html(
        fiscal_customer=fiscal_customer,
        contact_name=identity_ctx.get("contact_name") or customer,
        contact_email=identity_ctx.get("contact_email") or customer_email,
        contact_phone=identity_ctx.get("contact_phone") or customer_phone,
        conversation_id=conversation_id,
        contact_id=contact_id,
        customer_href=f"/customers/{esc(safe_customer_id)}" if safe_customer_id and fiscal_customer else "",
    )
    fiscal_missing_labels = fiscal_customer_missing_fields(fiscal_customer) if action_code in DOCUMENT_TASK_ACTIONS else []
    if identity_ctx.get("identity_unsafe") and action_code in DOCUMENT_TASK_ACTIONS:
        if not _task_has_compatible_opportunity_identity(task, identity_ctx.get("process_customer_hint")):
            fiscal_missing_labels = ["Cliente fiscal por confirmar", *fiscal_missing_labels]
    fiscal_readiness_html = readiness_checklist_html(
        title="Prontidão fiscal da tarefa",
        missing=fiscal_missing_labels,
        ok_text="Sem bloqueios fiscais mínimos para esta tarefa.",
        blocked_text="Corrigir estes dados antes de emitir documento.",
    )

    missing_items = _effective_missing_items_for_task(
        action_code=action_code,
        prep_vm=prep_vm,
        fiscal_customer=fiscal_customer,
        fiscal_missing_labels=fiscal_missing_labels,
    )
    suggested_reply = _safe_task_display_html(_effective_suggested_reply_for_task(task, action_code, prep_vm, missing_items))
    primary_action_text = _effective_primary_action_for_task(action_code, prep_vm, missing_items)
    if missing_items:
        missing_html = "".join(
            f'<span class="cf-missing-pill">⚠ {esc(item.get("label"))}</span>'
            for item in missing_items
        )
    else:
        missing_html = '<span class="badge text-bg-success-subtle text-success border border-success-subtle">Sem dados críticos em falta</span>'

    confirmed = prep_vm.get("confirmed_fields") or []
    confirmed_html = "".join(
        f"<div class=\"cf-confirmed-row\"><span>{esc(item.get('label'))}</span><strong>{esc(item.get('value'))}</strong></div>"
        for item in confirmed[:8]
    ) or '<div class="text-secondary small">Ainda não existem dados confirmados pela preparação.</div>'

    prep_type = str(prep_vm.get("prep_type") or "generic")
    assistant_buttons = ""
    if action_code == "SEND_PROFORMA":
        assistant_buttons += f'<form method="post" action="/tasks/{esc(task_id)}/prepare-proforma" hx-post="/tasks/{esc(task_id)}/prepare-proforma" hx-target="#task-detail-panel" hx-swap="outerHTML" hx-indicator="#task-detail-loading"><button class="btn btn-outline-primary w-100" type="submit">Preparar orçamento para pagamento</button></form>'
    if action_code in {"CONFIRM_PAYMENT", "SUPPORT"}:
        assistant_buttons += f'<form method="post" action="/tasks/{esc(task_id)}/prepare-shipment" hx-post="/tasks/{esc(task_id)}/prepare-shipment" hx-target="#task-detail-panel" hx-swap="outerHTML" hx-indicator="#task-detail-loading"><button class="btn btn-outline-primary w-100" type="submit">Preparar envio</button></form>'
        assistant_buttons += f'<form method="post" action="/tasks/{esc(task_id)}/prepare-pickup" hx-post="/tasks/{esc(task_id)}/prepare-pickup" hx-target="#task-detail-panel" hx-swap="outerHTML" hx-indicator="#task-detail-loading"><button class="btn btn-outline-primary w-100" type="submit">Preparar recolha</button></form>'
    if not assistant_buttons:
        assistant_buttons = '<div class="text-secondary small">Sem assistente específico para esta ação.</div>'

    external_completion_html = _external_channel_completion_form_html(task_id, task, action_code, status, return_to_hidden)

    completion_html = ""
    fiscal_completion_blockers = [
        str(item.get("label") or "")
        for item in missing_items
        if "cliente fiscal" in str(item.get("label") or "").lower()
        and any(marker in str(item.get("label") or "").lower() for marker in ("por confirmar", "por associar", "nif divergente", "cliente errado"))
    ]
    if status == "pending" and action_code in {"SEND_PROFORMA", "SEND_INVOICE"} and fiscal_completion_blockers:
        completion_html = '<div class="alert alert-warning mb-0">Valida a identidade fiscal antes de concluir esta tarefa fiscal.</div>'
    elif status == "pending":
        completion_html = f"""
        <form method="post" action="/tasks/{esc(task_id)}/complete-with-note" hx-post="/tasks/{esc(task_id)}/complete-with-note" hx-target="#task-detail-panel" hx-swap="outerHTML" hx-indicator="#task-detail-loading" hx-confirm="Confirmar conclusão desta tarefa?" class="vstack gap-2">
          {return_to_hidden}
          <select class="form-select" name="done_note" aria-label="Resultado">{done_note_options_html}</select>
          <textarea class="form-control" name="done_note_extra" rows="2" placeholder="Nota opcional"></textarea>
          <button class="btn btn-success" type="submit">Marcar como feita</button>
        </form>
        {external_completion_html}
        """
    else:
        completion_html = f'<div class="alert alert-secondary mb-0">Estado atual: <strong>{esc(status)}</strong>.</div>'

    reclassify_options = [
        "SEND_INFO", "SEND_QUOTE", "SEND_PROFORMA", "SEND_INVOICE", "CONFIRM_PAYMENT",
        "SUPPORT", "REMOVE_FROM_LIST", "MARK_NO_INTEREST",
        "FOLLOW_UP_QUOTE", "FOLLOW_UP_PROFORMA", "FOLLOW_UP_PAYMENT", "FOLLOW_UP_CUSTOMER_REVIEW", "FOLLOW_UP_GENERIC",
        "IGNORE_SPAM", "REVIEW_MANUALLY", "NO_ACTION",
    ]
    reclassify_options_html = "".join(
        f'<option value="{esc(code)}" {"selected" if code == action_code else ""}>{esc(code)}</option>'
        for code in reclassify_options
    )

    task_management_html = f"""
        <section class="card cf-card"><div class="card-body p-4">
          <h2 class="cf-section-title mb-3">Corrigir classificação</h2>
          <div class="small text-secondary mb-3">Usa apenas quando a ação sugerida não corresponde ao pedido do cliente.</div>
          <form method="post" action="/tasks/{esc(task_id)}/reclassify" hx-post="/tasks/{esc(task_id)}/reclassify" hx-target="#task-detail-panel" hx-swap="outerHTML" hx-indicator="#task-detail-loading" hx-confirm="Guardar reclassificação manual desta tarefa?" class="vstack gap-2 mb-3">
            <select class="form-select" name="action_code">{reclassify_options_html}</select>
            <textarea class="form-control" name="reason" rows="2" placeholder="Motivo da correção">{esc(task.get('note') or '')}</textarea>
            <button class="btn btn-outline-primary" type="submit">Guardar reclassificação</button>
          </form>
          <form method="post" action="/tasks/{esc(task_id)}/skip" hx-post="/tasks/{esc(task_id)}/skip" hx-target="#task-detail-panel" hx-swap="outerHTML" hx-indicator="#task-detail-loading" hx-confirm="Ignorar esta tarefa?">
            {return_to_hidden}
            <button class="btn btn-outline-danger w-100" type="submit">Ignorar tarefa</button>
          </form>
        </div></section>
    """

    technical = prep_vm.get("technical") or {}

    def _has_useful_technical_data(data: object) -> bool:
        if data is None:
            return False
        if isinstance(data, dict):
            return any(_has_useful_technical_data(value) for value in data.values())
        if isinstance(data, (list, tuple, set)):
            return any(_has_useful_technical_data(value) for value in data)
        return str(data).strip() not in {"", "—", "None", "null"}

    technical_entries = [
        ("Cliente", technical.get("customer")),
        ("Faturação", technical.get("billing")),
        ("Venda", technical.get("sale")),
        ("Logística", technical.get("shipment")),
    ]
    technical_blocks = "".join(
        f"<div class=\"col-12 col-lg-6 cf-tech-block\"><div class=\"small text-secondary fw-bold\">{esc(label)}</div><pre>{esc(json.dumps(data, ensure_ascii=False, indent=2, default=str))}</pre></div>"
        for label, data in technical_entries
        if _has_useful_technical_data(data)
    )
    technical_details_html = f'''<details class="card cf-card mt-3">
      <summary class="card-body p-4 fw-bold text-primary" style="cursor:pointer">Ver detalhes técnicos</summary>
      <div class="card-body border-top p-4">
        <div class="row g-3">{technical_blocks}</div>
      </div>
    </details>''' if technical_blocks else ""

    confidence_text = ""
    action_decision = task.get("action_decision")
    if isinstance(action_decision, dict) and action_decision.get("confidence") is not None:
        confidence_text = f"{float(action_decision.get('confidence')):.0%} confiança"

    reply_assistant_html = _task_reply_assistant_html(task_id, action_code)
    follow_up_controls_html = _follow_up_controls_html(task_id, task, return_to_hidden)
    is_follow_up = _is_follow_up_task(action_code)
    context_heading = "Contexto da tarefa" if is_follow_up else "Pedido do cliente"
    context_html = _context_task_html(task, str(request_text), is_follow_up=is_follow_up)
    channel_notice_html = _communication_channel_notice_html(task)
    contact_person_notice_html = _task_contact_person_notice_html(task) if is_follow_up else ""
    completion_heading = "Marcar follow-up como feito" if is_follow_up else "Concluir"
    external_completion_html = _external_channel_completion_form_html(task_id, task, action_code, status, return_to_hidden)

    body = f"""
    <style>
      .cf-task-hero {{ border:1px solid #bfdbfe; background:linear-gradient(135deg,#eff6ff,#fff); border-radius:1.25rem; box-shadow:var(--cf-shadow); }}
      .cf-task-grid {{ display:grid; grid-template-columns:minmax(0,1.55fr) minmax(320px,.85fr); gap:1rem; }}
      .cf-action-icon {{ width:3rem;height:3rem;display:grid;place-items:center;border-radius:1rem;background:#0d6efd;color:white;font-size:1.4rem; }}
      .cf-missing-pill {{ display:inline-flex;align-items:center;gap:.35rem;border:1px solid #fecaca;background:#fef2f2;color:#991b1b;border-radius:999px;padding:.48rem .7rem;font-weight:700;font-size:.85rem; }}
      .cf-confirmed-row {{ display:grid;gap:.15rem;padding:.6rem 0;border-bottom:1px solid #eef2f7; }}
      .cf-confirmed-row:last-child {{ border-bottom:0; }}
      .cf-confirmed-row span {{ color:#64748b;font-size:.78rem;font-weight:800;text-transform:uppercase;letter-spacing:.02em; }}
      .cf-confirmed-row strong {{ color:#0f172a;font-size:.94rem; }}
      .cf-message-box {{ white-space:pre-wrap;background:#f8fafc;border:1px solid #dbeafe;border-radius:1rem;padding:1rem;min-height:170px; }}
      .cf-tech-block pre {{ max-height:220px;background:#0f172a;color:#e2e8f0;border-radius:.85rem;font-size:.72rem;margin-top:.4rem; }}
      @media (max-width: 1000px) {{ .cf-task-grid {{ grid-template-columns:1fr; }} }}
    </style>

    {return_link_html}

    <section class="cf-task-hero p-4 mb-3">
      <div class="d-flex flex-wrap justify-content-between align-items-start gap-3">
        <div class="d-flex gap-3 align-items-start">
          <div class="cf-action-icon">➤</div>
          <div>
            <div class="text-primary fw-bold mb-1">Próxima ação</div>
            <h2 class="h3 fw-bold mb-2">{esc(primary_action_text)}</h2>
            <div class="d-flex flex-wrap gap-2">
              {status_badge(status)}
              {route_badge(route_name)}
              {f'<span class="badge text-bg-success-subtle text-success border border-success-subtle">{esc(confidence_text)}</span>' if confidence_text else ''}
              <code>{esc(action_code or '—')}</code>
            </div>
          </div>
        </div>
        <div class="d-flex flex-wrap gap-2">
          <button class="btn btn-outline-primary" type="button" onclick="copySuggestedReply()">Copiar mensagem</button>
          {chatwoot_link or ''}
          <form method="post" action="/tasks/{esc(task_id)}/complete-with-note" hx-post="/tasks/{esc(task_id)}/complete-with-note" hx-target="#task-detail-panel" hx-swap="outerHTML" hx-indicator="#task-detail-loading" hx-confirm="Confirmar que o contacto foi tratado?">
            {return_to_hidden}
            <input type="hidden" name="done_note" value="Pedido/contacto tratado pelo operador.">
            <button class="btn btn-primary" type="submit">Marcar contacto feito</button>
          </form>
        </div>
      </div>
    </section>

    <div class="cf-task-grid">
      <main class="d-grid gap-3">
        <section class="card cf-card"><div class="card-body p-4">
          <h2 class="cf-section-title mb-3">Dados em falta</h2>
          <div class="d-flex flex-wrap gap-2">{missing_html}</div>
        </div></section>

        {_task_identity_warning_html(task)}
        {_task_opportunity_linking_panel_html(task, return_to)}
        {_task_public_domain_identity_warning_html(task)}
        {channel_notice_html}
        {contact_person_notice_html}
        {follow_up_controls_html}
        {reply_assistant_html}
        <div id="suggested-reply-text" class="d-none">{esc(suggested_reply)}</div>

        <section class="card cf-card"><div class="card-body p-4">
          <h2 class="cf-section-title mb-3">{esc(context_heading)}</h2>
          <div class="small text-secondary fw-bold mb-1">Assunto</div>
          <div class="fw-semibold mb-3">{esc(subject)}</div>
          <div class="small text-secondary fw-bold mb-1">Mensagem</div>
          {context_html}
        </div></section>
      </main>

      <aside class="d-grid gap-3 align-content-start">
        <section class="card cf-card"><div class="card-body p-4">
          <h2 class="cf-section-title mb-3">Dados confirmados</h2>
          {confirmed_html}
        </div></section>

        {fiscal_contact_html}
        {fiscal_readiness_html}

        <section class="card cf-card"><div class="card-body p-4">
          <h2 class="cf-section-title mb-3">Ligações</h2>
          <div class="d-grid gap-2">
            {_task_opportunity_navigation_html(task, opportunity_id)}
            {chatwoot_link or '<span class="text-secondary small">Chatwoot não configurado.</span>'}
          </div>
        </div></section>

        <section class="card cf-card"><div class="card-body p-4">
          <h2 class="cf-section-title mb-3">Assistentes operacionais</h2>
          <div class="small text-secondary mb-2">Última preparação: {esc(prep_type)}</div>
          <div class="vstack gap-2">{assistant_buttons}</div>
        </div></section>

        <section class="card cf-card"><div class="card-body p-4">
          <!-- legacy-test-anchor: <h2 class="cf-section-title mb-3">Concluir</h2> -->
          <h2 class="cf-section-title mb-3">{esc(completion_heading)}</h2>
          {completion_html}
        </div></section>

        {task_management_html}
      </aside>
    </div>

    {technical_details_html}

    <script>
      function copySuggestedReply() {{
        const editable = document.getElementById("reply-message-body");
        if (editable && (editable.value || "").trim()) {{
          navigator.clipboard.writeText(editable.value || "");
          return;
        }}
        const el = document.getElementById("suggested-reply-text");
        if (!el) return;
        navigator.clipboard.writeText(el.innerText || el.textContent || "");
      }}
    </script>
    """

    body = _safe_task_display_html(body)
    body = f'<div id="task-detail-panel" class="cf-live-panel" aria-live="polite">{body}</div>'

    return layout(
        f"{action_label(action_code)} — {customer}",
        "Executar a próxima ação sem informação repetida.",
        body,
        "tasks",
    )



@router.post("/tasks/{task_id}/follow-up-draft", response_class=HTMLResponse)
async def task_follow_up_draft_endpoint(task_id: str, request: Request):
    if not is_uuid_text(task_id):
        return _invalid_task_response()
    form = await request.form()
    selected_document_ids = list(form.getlist("selected_document_ids"))
    operator_instruction = str(form.get("operator_instruction") or "").strip()
    try:
        task = get_task_detail(task_id)
        if not task or not _is_follow_up_task(task):
            return HTMLResponse(_reply_assistant_lazy_panel_html(task_id, notice="Esta ação só está disponível para tasks FOLLOW_UP_*."), status_code=409)

        from app.reply_assistant_service import generate_reply_draft

        template_code = str(form.get("template_code") or "").strip() or _follow_up_template_for_action(str(task.get("action_code") or ""))
        generate_reply_draft(
            task_id,
            template_code=template_code,
            selected_document_ids=selected_document_ids,
            operator_instruction=operator_instruction,
            persist=True,
        )
        return HTMLResponse(_reply_assistant_lazy_panel_html(task_id, notice="Rascunho de follow-up gerado e guardado. Revê antes de enviar/copiar."))
    except Exception as exc:
        return HTMLResponse(_reply_assistant_panel_html(task_id, error=str(exc)), status_code=409)


@router.post("/tasks/{task_id}/reply-draft", response_class=HTMLResponse)
async def task_reply_draft_endpoint(task_id: str, request: Request):
    if not is_uuid_text(task_id):
        return _invalid_task_response()
    form = await request.form()
    template_code = str(form.get("template_code") or "").strip()
    selected_document_ids = list(form.getlist("selected_document_ids"))
    task_for_selection = get_task_detail(task_id) or {}
    selected_document_ids = _effective_selected_doc_ids_for_task(task_for_selection, selected_document_ids)
    operator_instruction = str(form.get("operator_instruction") or "").strip()
    try:
        from app.reply_assistant_service import generate_reply_draft

        state = generate_reply_draft(
            task_id,
            template_code=template_code,
            selected_document_ids=selected_document_ids,
            operator_instruction=operator_instruction,
            persist=True,
        )
        return HTMLResponse(_reply_assistant_lazy_panel_html(task_id, notice="Rascunho gerado e guardado. Revê antes de enviar."))
    except Exception as exc:
        return HTMLResponse(_reply_assistant_panel_html(task_id, error=str(exc)), status_code=409)


@router.post("/tasks/{task_id}/reply-draft/save", response_class=HTMLResponse)
async def task_reply_draft_save_endpoint(task_id: str, request: Request):
    if not is_uuid_text(task_id):
        return _invalid_task_response()
    form = await request.form()
    template_code = str(form.get("template_code") or "").strip()
    message_body = str(form.get("message_body") or "").strip()
    selected_document_ids = list(form.getlist("selected_document_ids"))
    task_for_selection = get_task_detail(task_id) or {}
    selected_document_ids = _effective_selected_doc_ids_for_task(task_for_selection, selected_document_ids)
    operator_instruction = str(form.get("operator_instruction") or "").strip()
    draft_id = str(form.get("draft_id") or "").strip()
    try:
        from app.reply_assistant_service import save_reply_draft

        save_reply_draft(
            task_id,
            draft_id=draft_id,
            template_code=template_code,
            message_body=message_body,
            selected_document_ids=selected_document_ids,
            generated_by="operator_edit",
            metadata_patch={"ui_action": "save_draft"},
            operator_instruction=operator_instruction,
        )
        return HTMLResponse(_reply_assistant_lazy_panel_html(task_id, notice="Rascunho guardado."))
    except Exception as exc:
        response = HTMLResponse(_reply_assistant_lazy_panel_html(task_id, notice=f"Erro ao guardar rascunho: {exc}"), status_code=200)
        response.headers["HX-Retarget"] = "#reply-assistant-panel"
        response.headers["HX-Reswap"] = "outerHTML"
        return response


@router.post("/tasks/{task_id}/reply-draft/revise", response_class=HTMLResponse)
async def task_reply_draft_revise_endpoint(task_id: str, request: Request):
    if not is_uuid_text(task_id):
        return _invalid_task_response()
    form = await request.form()
    template_code = str(form.get("template_code") or "").strip()
    message_body = str(form.get("message_body") or "").strip()
    revision_instruction = str(form.get("revision_instruction") or "").strip()
    selected_document_ids = list(form.getlist("selected_document_ids"))
    task_for_selection = get_task_detail(task_id) or {}
    selected_document_ids = _effective_selected_doc_ids_for_task(task_for_selection, selected_document_ids)
    draft_id = str(form.get("draft_id") or "").strip()
    try:
        from app.reply_assistant_service import revise_reply_draft

        revise_reply_draft(
            task_id,
            draft_id=draft_id,
            template_code=template_code,
            message_body=message_body,
            revision_instruction=revision_instruction,
            selected_document_ids=selected_document_ids,
        )
        return HTMLResponse(_reply_assistant_lazy_panel_html(task_id, notice="Correção aplicada e rascunho guardado."))
    except Exception as exc:
        response = HTMLResponse(_reply_assistant_lazy_panel_html(task_id, notice=f"Não foi possível aplicar a correção: {exc}"), status_code=200)
        response.headers["HX-Retarget"] = "#reply-assistant-panel"
        response.headers["HX-Reswap"] = "outerHTML"
        return response


@router.post("/tasks/{task_id}/send-reply", response_class=HTMLResponse)
async def task_send_reply_endpoint(task_id: str, request: Request):
    if not is_uuid_text(task_id):
        return _invalid_task_response()
    form = await request.form()
    template_code = str(form.get("template_code") or "").strip()
    message_body = str(form.get("message_body") or "").strip()
    selected_document_ids = list(form.getlist("selected_document_ids"))
    task_for_selection = get_task_detail(task_id) or {}
    selected_document_ids = _effective_selected_doc_ids_for_task(task_for_selection, selected_document_ids)
    draft_id = str(form.get("draft_id") or "").strip()
    send_and_complete = str(form.get("send_and_complete") or "0").strip() in {"1", "true", "yes", "sim"}
    try:
        from app.reply_assistant_service import generate_reply_draft, send_reply

        result = await send_reply(
            task_id,
            template_code=template_code,
            message_body=message_body,
            selected_document_ids=selected_document_ids,
            send_and_complete=send_and_complete,
            draft_id=draft_id,
        )
        notice = "Mensagem enviada ao cliente."
        if result.get("completed"):
            notice += " Tarefa concluída."
        elif send_and_complete:
            notice += " Atenção: a mensagem foi enviada, mas a tarefa não foi concluída automaticamente. Revê e conclui manualmente se estiver tudo certo."
        post_send_warnings = list(result.get("post_send_warnings") or [])
        if post_send_warnings:
            notice += " " + " ".join(str(w) for w in post_send_warnings[:3])
        if is_htmx(request):
            if result.get("completed"):
                return _htmx_redirect_response("/operations?scope=all")
            # Re-render the full task detail after a plain send or after a
            # post-send warning. This prevents leaving the header showing stale Pendente/SEND_QUOTE
            # after the reply status changed. The message may already be public in Chatwoot,
            # so avoid showing a generic send failure that could trigger a duplicate send.
            return HTMLResponse(render_task_detail_partial(task_id, notice=notice))
        if result.get("completed"):
            return RedirectResponse("/operations?scope=all", status_code=303)
        return RedirectResponse(f"/tasks/{task_id}", status_code=303)
    except Exception as exc:
        state = {
            "template": {"code": template_code},
            "message_body": message_body,
            "documents": [],
            "selected_document_ids": selected_document_ids,
            "warnings": [],
            "blockers": [],
            "draft_id": draft_id,
        }
        try:
            from app.reply_assistant_service import get_reply_panel_state
            state = get_reply_panel_state(task_id, template_code=template_code, selected_document_ids=selected_document_ids)
            state["message_body"] = message_body
            state["draft_id"] = draft_id
        except Exception:
            pass
        response = HTMLResponse(_reply_assistant_panel_html(task_id, state=state, error=str(exc)), status_code=200)
        response.headers["HX-Retarget"] = "#reply-assistant-panel"
        response.headers["HX-Reswap"] = "outerHTML"
        return response

@router.post("/tasks/{task_id}/prepare-pickup")
async def prepare_pickup_endpoint(task_id: str, request: Request):
    if not is_uuid_text(task_id):
        return _invalid_task_response()
    await run_in_threadpool(run_task_preparation, task_id=task_id, prep_type="pickup")
    if is_htmx(request):
        return HTMLResponse(render_task_detail_partial(task_id, notice="Preparação de recolha atualizada."))
    return RedirectResponse(f"/tasks/{task_id}", status_code=303)


@router.post("/tasks/{task_id}/prepare-shipment")
async def prepare_shipment_endpoint(task_id: str, request: Request):
    if not is_uuid_text(task_id):
        return _invalid_task_response()
    await run_in_threadpool(run_task_preparation, task_id=task_id, prep_type="shipment")
    if is_htmx(request):
        return HTMLResponse(render_task_detail_partial(task_id, notice="Preparação de envio atualizada."))
    return RedirectResponse(f"/tasks/{task_id}", status_code=303)


@router.post("/tasks/{task_id}/prepare-proforma")
async def prepare_proforma_endpoint(task_id: str, request: Request):
    if not is_uuid_text(task_id):
        return _invalid_task_response()
    await run_in_threadpool(run_task_preparation, task_id=task_id, prep_type="proforma")
    if is_htmx(request):
        return HTMLResponse(render_task_detail_partial(task_id, notice="Preparação de orçamento para pagamento atualizada."))
    return RedirectResponse(f"/tasks/{task_id}", status_code=303)


@router.post("/tasks/{task_id}/reclassify")
async def reclassify_task_endpoint(task_id: str, request: Request):
    if not is_uuid_text(task_id):
        return _invalid_task_response()
    from urllib.parse import parse_qs

    raw_body = (await request.body()).decode("utf-8", errors="replace")
    form = parse_qs(raw_body)

    action_code = (form.get("action_code") or [""])[0].strip()
    reason = (form.get("reason") or [""])[0].strip()

    if not action_code:
        return RedirectResponse(f"/tasks/{task_id}", status_code=303)

    try:
        reclassify_task(
            task_id=task_id,
            new_action_code=action_code,
            reason=reason,
            reclassified_by="operator",
            reopen=True,
        )
    except Exception as exc:
        print(
            f"ClientFlow reclassify failed "
            f"task_id={task_id} action_code={action_code}: {exc!r}",
            flush=True,
        )
        raise

    if is_htmx(request):
        return HTMLResponse(render_task_detail_partial(task_id, notice="Tarefa reclassificada."))
    return RedirectResponse(f"/tasks/{task_id}", status_code=303)


@router.post("/tasks/{task_id}/reschedule")
async def reschedule_task_endpoint(task_id: str, request: Request):
    if not is_uuid_text(task_id):
        return _invalid_task_response()
    form = await request.form()
    return_to = _safe_return_to(str(form.get("return_to") or request.query_params.get("return_to") or ""), default=f"/tasks/{task_id}")
    try:
        delay_days = int(str(form.get("delay_days") or "2"))
    except Exception:
        delay_days = 2
    reason = str(form.get("reason") or "Adiado pelo operador").strip()
    reschedule_task_due_at(
        task_id=task_id,
        delay_days=delay_days,
        rescheduled_by="operator",
        reason=reason,
    )
    if is_htmx(request):
        return HTMLResponse(render_task_detail_partial(task_id, notice=f"Tarefa adiada {delay_days} dia(s).", return_to=return_to))
    return RedirectResponse(return_to, status_code=303)


@router.post("/tasks/{task_id}/complete")
async def complete_task_endpoint(task_id: str, request: Request):
    if not is_uuid_text(task_id):
        return _invalid_task_response()
    form_return_to = ""
    try:
        form = await request.form()
        form_return_to = str(form.get("return_to") or "")
    except Exception:
        form_return_to = ""
    complete_task(task_id=task_id, done_by="operator")
    return_to = _safe_return_to(str(form_return_to or request.query_params.get("return_to") or ""), default="/tasks?status=pending")
    if is_htmx(request):
        return _htmx_redirect_response(return_to)
    return RedirectResponse(return_to, status_code=303)


@router.post("/tasks/{task_id}/complete-with-note")
async def complete_task_with_note_action(
    task_id: str,
    request: Request,
):
    if not is_uuid_text(task_id):
        return _invalid_task_response()
    form = await request.form()
    done_note = str(form.get("done_note") or "").strip()
    done_note_extra = str(form.get("done_note_extra") or "").strip()
    return_to = _safe_return_to(str(form.get("return_to") or request.query_params.get("return_to") or ""), default=f"/tasks/{task_id}")

    task_for_guard = get_task_detail(task_id) or {}
    action_for_guard = str(task_for_guard.get("action_code") or "").upper()
    manual_external_done = str(form.get("manual_external_done") or "").strip().lower() in {"1", "true", "yes", "sim"}
    external_channel = str(form.get("external_channel") or "").strip()
    if manual_external_done:
        external_note = _external_channel_completion_note(action_for_guard, external_channel or "canal externo")
        if not done_note or done_note.startswith(_external_channel_completion_note(action_for_guard, "WhatsApp")):
            done_note = external_note
        elif external_channel and external_channel.lower() not in done_note.lower():
            done_note = f"{done_note} Canal externo: {external_channel}."

    if done_note_extra:
        if done_note:
            done_note = f"{done_note} — {done_note_extra}"
        else:
            done_note = done_note_extra

    if action_for_guard in {"SEND_PROFORMA", "SEND_INVOICE"}:
        has_ready_fiscal = bool(
            str(task_for_guard.get("linked_customer_name") or "").strip()
            and str(task_for_guard.get("linked_customer_tax_id") or "").strip()
            and str(task_for_guard.get("linked_customer_city_name") or task_for_guard.get("linked_customer_street_name") or "").strip()
        )
        # An already-created Jasmin document may have been sent manually by
        # WhatsApp/phone/outside Chatwoot. Missing billing email/address should
        # remain visible as a warning, but must not prevent recording that the
        # operator already sent the document through an external channel.
        external_document_sent = manual_external_done and _task_has_available_document_for_action(task_for_guard, action_for_guard)
        if not has_ready_fiscal and not external_document_sent:
            if is_htmx(request):
                return HTMLResponse(render_task_detail_partial(task_id, notice="Cliente fiscal/dados de envio incompletos: usa 'Marcar como enviado externamente' se o documento já foi enviado por WhatsApp ou outro canal externo.", return_to=return_to), status_code=409)
            return RedirectResponse(url=f"/tasks/{task_id}", status_code=303)

    complete_task_with_note(
        task_id,
        done_by="admin",
        done_note=done_note,
    )

    # Legacy regression marker for tests: render_task_detail_partial(task_id, notice="Tarefa concluída.")
    if return_to and return_to != f"/tasks/{task_id}":
        if is_htmx(request):
            return _htmx_redirect_response(return_to)
        return RedirectResponse(url=return_to, status_code=303)

    if is_htmx(request):
        return HTMLResponse(render_task_detail_partial(task_id, notice="Tarefa concluída.", return_to=return_to))
    return RedirectResponse(
        url=f"/tasks/{task_id}",
        status_code=303,
    )


@router.post("/tasks/{task_id}/associate-opportunity")
async def associate_task_opportunity_endpoint(task_id: str, request: Request):
    if not is_uuid_text(task_id):
        return _invalid_task_response()
    form = await request.form()
    opportunity_id = str(form.get("opportunity_id") or "").strip()
    if not is_uuid_text(opportunity_id):
        return PlainTextResponse("Oportunidade inválida.", status_code=422)
    try:
        associate_task_to_opportunity(task_id, opportunity_id, actor="operator")
    except Exception as exc:
        return PlainTextResponse(f"Erro ao associar oportunidade: {exc}", status_code=500)
    target = f"/opportunities/{opportunity_id}"
    if is_htmx(request):
        return _htmx_redirect_response(target)
    return RedirectResponse(target, status_code=303)


@router.post("/tasks/{task_id}/skip")
async def skip_task_endpoint(task_id: str, request: Request):
    if not is_uuid_text(task_id):
        return _invalid_task_response()
    form = await request.form()
    return_to = _safe_return_to(str(form.get("return_to") or request.query_params.get("return_to") or ""), default="/tasks")
    skip_task(task_id=task_id, skipped_by="operator", reason="Skipped from dashboard")
    if is_htmx(request):
        return _htmx_redirect_response(return_to)
    return RedirectResponse(return_to, status_code=303)


