"""Operational reconciliation and external intake UI.

This page is a staging area for information found outside ClientFlow. It does
not replace Operations; it prepares loose Jasmin/Odoo/payment/manual evidence so
an operator can link, create or ignore it deliberately.
"""
from __future__ import annotations

import logging
from typing import Optional

from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse, RedirectResponse

from app.admin_ui.components import esc, fmt_dt, kpi_card, money, status_chip
from app.admin_ui.layout import layout
from app.fiscal_enrichment_service import enrich_open_opportunities, fiscal_enrichment_summary
from app.external_reconciliation_sync import (
    sync_all_external_reconciliation_candidates,
    sync_jasmin_reconciliation_candidates,
    sync_odoo_reconciliation_candidates,
    sync_packlink_reconciliation_candidates,
)
from app.reconciliation_decision_service import (
    classify_reconciliation_item,
    classify_reconciliation_process,
    reconciliation_decision_summary,
    sort_items_for_operator,
)
from app.reconciliation_service import (
    cleanup_reconciliation_outside_window,
    create_external_request,
    create_opportunity_from_reconciliation,
    create_payment_proof,
    link_reconciliation_to_opportunity,
    list_reconciliation_items,
    list_reconciliation_process_candidates,
    create_opportunity_from_reconciliation_process,
    link_reconciliation_process_to_opportunity,
    reconciliation_summary,
    recent_window_start,
    reset_generated_reconciliation_items,
    set_reconciliation_status,
    sync_local_documents_without_opportunity,
    upsert_reconciliation_item,
)

logger = logging.getLogger(__name__)

router = APIRouter()


TYPE_LABELS = {
    "jasmin_quotation": "Orçamento Jasmin",
    "jasmin_proforma": "Orçamento Jasmin legado",
    "jasmin_invoice": "Fatura Jasmin",
    "odoo_sale_order": "Venda Odoo",
    "payment_proof": "Comprovativo",
    "packlink_shipment": "Envio Packlink",
    "manual_request": "Pedido externo",
    "external_record": "Registo externo",
}


def _type_label(value: str | None) -> str:
    return TYPE_LABELS.get(str(value or ""), str(value or "—"))


def _safe_days(value: object, default: int = 3) -> int:
    try:
        return min(max(int(value or default), 1), 90)
    except Exception:
        return default


def _filter_url(status: str = "open", external_type: str = "all", days: int = 3) -> str:
    return f"/reconciliation?status={esc(status)}&external_type={esc(external_type)}&days={int(days or 3)}"



def _operation_label(value: str | None) -> str:
    raw = str(value or "")
    if not raw:
        return ""
    labels = {
        "odoo_sale_order": "Venda Odoo",
        "jasmin_quotation": "Orçamento Jasmin",
        "jasmin_proforma": "Orçamento Jasmin legado",
        "jasmin_invoice": "Fatura Jasmin",
        "document": "Documento",
    }
    if ":" in raw:
        key, ref = raw.split(":", 1)
        return f"{labels.get(key, key)} {ref}"
    return raw

def _render_item_rows(items: list[dict]) -> str:
    rows = ""
    for item in items:
        item_id = str(item.get("id") or "")
        opportunity_link = "—"
        if item.get("opportunity_id"):
            opportunity_label = item.get("opportunity_title") or "Ver oportunidade"
            opportunity_link = f'<a href="/opportunities/{esc(item.get("opportunity_id"))}">{esc(opportunity_label)}</a>'

        amount = "—"
        if item.get("amount") is not None:
            amount = money(item.get("amount"), item.get("currency") or "EUR")

        actions = ""
        if str(item.get("status") or "") in {"open", "needs_review"}:
            suggestions = item.get("operation_suggestions") or []
            suggestion_html = ""
            if suggestions:
                suggestion_cards = ""
                for suggestion in suggestions[:2]:
                    suggested_opp_id = str(suggestion.get("opportunity_id") or "")
                    suggested_title = suggestion.get("opportunity_title") or "Oportunidade aberta"
                    suggested_customer = suggestion.get("customer_name") or ""
                    suggested_action = suggestion.get("action") or suggestion.get("action_code") or "operação aberta"
                    suggested_reason = suggestion.get("reason") or "possível correspondência"
                    suggestion_cards += f"""
                    <form method="post" action="/reconciliation/{esc(item_id)}/link" hx-confirm="Ligar este item à operação/oportunidade sugerida?" class="cf-reconcile-suggestion">
                      <input type="hidden" name="opportunity_id" value="{esc(suggested_opp_id)}">
                      <div class="small text-secondary">Sugestão encontrada em Operations</div>
                      <div class="fw-semibold text-break">{esc(suggested_title)}</div>
                      <div class="small text-secondary text-break">{esc(suggested_customer)}</div>
                      <div class="small text-secondary text-break">{esc(suggested_action)} · {esc(suggested_reason)}</div>
                      <button class="btn btn-sm btn-primary mt-2" type="submit">Ligar a esta operação</button>
                    </form>
                    """
                suggestion_html = f'<div class="d-grid gap-2 mb-2">{suggestion_cards}</div>'

            create_button_class = "btn-outline-primary" if suggestions else "btn-primary"
            search_query = item.get("customer_tax_id") or item.get("customer_email") or item.get("customer_name") or item.get("document_number") or ""
            search_href = f"/opportunities?q={esc(str(search_query))}" if search_query else "/opportunities"
            manual_link = f"""
              <details class="cf-reconcile-manual-link small text-secondary mt-1">
                <summary>Escolher outra oportunidade</summary>
                <form class="d-flex gap-1 mt-2" method="post" action="/reconciliation/{esc(item_id)}/link" hx-confirm="Ligar este item à oportunidade indicada?">
                  <input class="form-control form-control-sm" name="opportunity_id" placeholder="ID oportunidade" style="max-width:170px">
                  <button class="btn btn-sm btn-outline-primary" type="submit">Ligar</button>
                </form>
              </details>
            """
            actions = f"""
            <div class="d-grid gap-2 justify-content-end">
              {suggestion_html}
              <div class="d-flex flex-wrap justify-content-end gap-2">
                <a class="btn btn-sm btn-outline-secondary" href="{search_href}">Pesquisar oportunidade</a>
                <form method="post" action="/reconciliation/{esc(item_id)}/create-opportunity" hx-confirm="Criar nova oportunidade a partir deste item? Use isto apenas se não houver operação aberta correspondente.">
                  <button class="btn btn-sm {create_button_class}" type="submit">Criar oportunidade</button>
                </form>
                <form method="post" action="/reconciliation/{esc(item_id)}/ignore" hx-confirm="Ignorar este item de reconciliação?">
                  <button class="btn btn-sm btn-outline-secondary" type="submit">Ignorar</button>
                </form>
                <form method="post" action="/reconciliation/{esc(item_id)}/needs-review" hx-confirm="Enviar este item para revisão manual?">
                  <button class="btn btn-sm btn-outline-warning" type="submit">Rever</button>
                </form>
                <form method="post" action="/reconciliation/{esc(item_id)}/historical" hx-confirm="Marcar este item como histórico sem ação operacional?">
                  <button class="btn btn-sm btn-outline-secondary" type="submit">Histórico</button>
                </form>
              </div>
              {manual_link}
            </div>
            """
        else:
            actions = '<span class="text-secondary small">Sem ações pendentes</span>'

        decision = classify_reconciliation_item(item)
        decision_chip = f'<span class="cf-chip {esc(decision.get("chip_class") or "cf-chip-gray")}">{esc(decision.get("label") or "Decisão")}</span>'
        rows += f"""
        <tr>
          <td>
            <div class="fw-bold">{esc(item.get('title'))}</div>
            <div class="small text-secondary text-break">{esc(item.get('description') or '')}</div>
            <div class="small text-secondary mt-1">{esc(item.get('source_system'))} · {esc(item.get('external_id') or 'sem external_id')}</div>
          </td>
          <td><span class="cf-chip cf-chip-blue">{esc(_type_label(item.get('external_type')))}</span><div class="small mt-1">{esc(item.get('document_number') or '')}</div></td>
          <td>{status_chip(item.get('status'))}<div class="small text-secondary mt-1">{esc(item.get('priority') or 'normal')}</div><div class="mt-1">{decision_chip}</div><div class="small text-secondary mt-1">{esc(decision.get('primary_decision') or '')}</div></td>
          <td><div>{esc(item.get('customer_name') or item.get('linked_customer_name') or '—')}</div><div class="small text-secondary">{esc(item.get('customer_email') or '')}</div>{f'<div class="small text-secondary">NIF {esc(item.get("customer_tax_id"))}</div>' if item.get('customer_tax_id') else ''}</td>
          <td>{amount}<div class="small text-secondary">{esc(fmt_dt(item.get('document_date')))}</div></td>
          <td>{opportunity_link}</td>
          <td class="text-end">{actions}</td>
        </tr>
        """
    if not rows:
        rows = '<tr><td colspan="7" class="text-center text-secondary py-5">Sem itens de reconciliação para este filtro.</td></tr>'
    return rows



def _render_process_candidates(candidates: list[dict], *, days: int = 3) -> str:
    """Render grouped timeline reconstruction proposals.

    These cards are intentionally more useful than isolated rows: they show the
    probable sequence found across Jasmin/Odoo/Packlink/payment evidence and let
    the operator create or link one process deliberately.
    """
    if not candidates:
        return """
        <section class="card cf-card mb-4">
          <div class="card-body p-4 text-secondary">
            <h2 class="cf-section-title mb-2">Processos candidatos</h2>
            <div>Sem processos reconstruíveis na janela ativa. Itens isolados continuam disponíveis abaixo.</div>
          </div>
        </section>
        """

    cards = ""
    for candidate in candidates[:8]:
        item_ids = ",".join(str(x) for x in candidate.get("item_ids") or [])
        tax_line = f'<div class="small text-secondary">NIF {esc(candidate.get("customer_tax_id"))}</div>' if candidate.get("customer_tax_id") else ""
        email_line = f'<div class="small text-secondary text-break">{esc(candidate.get("customer_email"))}</div>' if candidate.get("customer_email") else ""
        amount = ""
        if candidate.get("amount") is not None:
            amount = f'<span class="cf-chip cf-chip-gray">{money(candidate.get("amount"), candidate.get("currency") or "EUR")}</span>'
        identity_line = f'<div class="small text-secondary">Cliente fiscal: {esc(candidate.get("identity_reason") or candidate.get("match_key") or "—")}</div>'
        operation_label = _operation_label(candidate.get("operation_key"))
        operation_line = f'<div class="small text-secondary">Compra/processo: {esc(operation_label)}</div>' if operation_label else ""
        review_status = str(candidate.get("review_status") or "needs_review")
        review_label = {"ready": "pronto", "needs_review": "rever", "conflict": "conflito"}.get(review_status, review_status)
        review_class = "green" if review_status == "ready" else "orange" if review_status == "needs_review" else "red"
        review_chip = f'<span class="cf-chip cf-chip-{review_class}">{esc(review_label)}</span>'
        decision = classify_reconciliation_process(candidate)
        decision_chip = f'<span class="cf-chip {esc(decision.get("chip_class") or "cf-chip-gray")}">{esc(decision.get("label") or "Decisão")}</span>'
        reasons = "".join(f'<li>{esc(reason)}</li>' for reason in (candidate.get("reasons") or [])[:5])
        risks = "".join(f'<li>{esc(risk)}</li>' for risk in (candidate.get("risks") or [])[:5])
        explanation_html = (
            '<div class="cf-reconcile-explain">'
            f'<div><strong>Motivos</strong><ul>{reasons or "<li>candidato reconstruído por identidade fiscal/processo</li>"}</ul></div>'
            f'<div><strong>Riscos</strong><ul>{risks or "<li>sem riscos relevantes detetados</li>"}</ul></div>'
            '</div>'
        )
        timeline = ""
        for step in candidate.get("timeline") or []:
            timeline += f"""
            <div class="cf-reconcile-timeline-step">
              <div class="small text-secondary">{esc(fmt_dt(step.get('document_date')))}</div>
              <div><strong>{esc(step.get('label'))}</strong></div>
              <div class="small text-secondary text-break">{esc(step.get('document_number') or step.get('title') or '')}</div>
            </div>
            """
        suggestion_html = ""
        suggestions = candidate.get("suggestions") or []
        if suggestions:
            suggestion = suggestions[0]
            suggestion_html = f"""
            <form method="post" action="/reconciliation/processes/link" hx-confirm="Ligar todos os itens deste processo à oportunidade sugerida?" class="cf-reconcile-suggestion">
              <input type="hidden" name="item_ids" value="{esc(item_ids)}">
              <input type="hidden" name="opportunity_id" value="{esc(suggestion.get('opportunity_id') or '')}">
              <div class="small text-secondary">Sugestão de oportunidade existente</div>
              <div class="fw-semibold text-break">{esc(suggestion.get('opportunity_title') or 'Oportunidade aberta')}</div>
              <div class="small text-secondary text-break">{esc(suggestion.get('customer_name') or '')} · {esc(suggestion.get('reason') or 'possível correspondência')}</div>
              <button class="btn btn-sm btn-primary mt-2" type="submit">Ligar processo à sugestão</button>
            </form>
            """
        cards += f"""
        <article class="cf-reconcile-process-card">
          <div class="d-flex justify-content-between gap-3 align-items-start flex-wrap">
            <div>
              <h3 class="h6 mb-1">{esc(candidate.get('customer_name') or 'Processo externo')}</h3>
              {email_line}{tax_line}
            </div>
            <div class="d-flex gap-2 flex-wrap justify-content-end">
              <span class="cf-chip cf-chip-blue">{len(candidate.get('items') or [])} evidência(s)</span>
              <span class="cf-chip cf-chip-orange">confiança {esc(candidate.get('confidence') or 'média')}</span>
              {review_chip}
              {decision_chip}
              {amount}
            </div>
          </div>
          {identity_line}{operation_line}
          <div class="cf-reconcile-decision-note"><strong>{esc(decision.get('primary_decision') or 'Decisão pendente')}</strong><span>{esc(decision.get('description') or '')}</span></div>
          {explanation_html}
          <div class="cf-reconcile-process-state">
            <div><span>Estado sugerido</span><strong>{esc(candidate.get('suggested_stage') or 'REVIEW')}</strong></div>
            <div><span>Próxima ação</span><strong>{esc(candidate.get('suggested_action') or 'REVIEW_MANUALLY')}</strong></div>
          </div>
          <div class="cf-reconcile-timeline">{timeline}</div>
          <div class="d-flex flex-wrap gap-2 align-items-start">
            {suggestion_html}
            <form method="post" action="/reconciliation/processes/create-opportunity" hx-confirm="Criar uma oportunidade reconstruída com esta timeline?">
              <input type="hidden" name="item_ids" value="{esc(item_ids)}">
              <button class="btn btn-sm btn-outline-primary" type="submit">Criar oportunidade reconstruída</button>
            </form>
            <form method="post" action="/reconciliation/processes/needs-review" hx-confirm="Enviar este processo para revisão manual?">
              <input type="hidden" name="item_ids" value="{esc(item_ids)}">
              <button class="btn btn-sm btn-outline-warning" type="submit">Rever</button>
            </form>
            <form method="post" action="/reconciliation/processes/historical" hx-confirm="Marcar este processo como histórico sem ação operacional?">
              <input type="hidden" name="item_ids" value="{esc(item_ids)}">
              <button class="btn btn-sm btn-outline-secondary" type="submit">Histórico</button>
            </form>
            <form method="post" action="/reconciliation/processes/ignore" hx-confirm="Ignorar este processo? Use apenas quando pertence a histórico externo ou não deve criar ação operacional.">
              <input type="hidden" name="item_ids" value="{esc(item_ids)}">
              <button class="btn btn-sm btn-outline-secondary" type="submit">Ignorar</button>
            </form>
            <details class="small text-secondary cf-reconcile-manual-link">
              <summary>Ligar a outra oportunidade</summary>
              <form method="post" action="/reconciliation/processes/link" class="d-flex gap-1 mt-2" hx-confirm="Ligar todos os itens deste processo à oportunidade indicada?">
                <input type="hidden" name="item_ids" value="{esc(item_ids)}">
                <input class="form-control form-control-sm" name="opportunity_id" placeholder="ID oportunidade" style="max-width:180px">
                <button class="btn btn-sm btn-outline-primary" type="submit">Ligar</button>
              </form>
            </details>
          </div>
        </article>
        """
    return f"""
    <section class="card cf-card mb-4">
      <div class="card-body p-3 border-bottom">
        <h2 class="cf-section-title mb-1">Processos candidatos</h2>
        <div class="small text-secondary">O sistema agrupa primeiro por cliente fiscal e depois separa por compra/processo antes de criar ou ligar a oportunidade.</div>
      </div>
      <div class="card-body p-3 d-grid gap-3">{cards}</div>
    </section>
    """

def render_reconciliation_decision_board(items: list[dict], candidates: list[dict], *, status: str, external_type: str, days: int) -> str:
    summary = reconciliation_decision_summary(items, candidates)
    cards = [
        ("actionable", "Ação recomendada", "Ligar/criar com evidência suficiente", "open"),
        ("review", "Requer revisão", "Conflitos ou baixa confiança", "needs_review"),
        ("historical", "Histórico", "Sem ação operacional", "historical"),
        ("ignored", "Ignorados", "Fora da fila", "ignored"),
        ("resolved", "Resolvidos", "Ligados ou fechados", "linked"),
    ]
    html = ""
    for key, label, description, target_status in cards:
        active = " active" if ((key == "actionable" and status == "open") or status == target_status) else ""
        href = _filter_url(target_status, external_type, days)
        html += f'''
        <a class="cf-decision-card{active}" href="{href}">
          <span>{esc(label)}</span>
          <strong>{int(summary.get(key, 0))}</strong>
          <small>{esc(description)}</small>
        </a>
        '''
    return f'''
    <section class="cf-reconcile-decision-grid mb-3" aria-label="Resumo decisional de reconciliação">
      {html}
    </section>
    '''


def render_reconciliation_table(items: list[dict]) -> str:
    return f"""
    <div class="cf-table-wrap border-0 rounded-0">
      <table class="table cf-table align-middle">
        <thead><tr><th>Item</th><th>Tipo</th><th>Estado</th><th>Cliente</th><th>Valor/data</th><th>Oportunidade</th><th class="text-end">Ações</th></tr></thead>
        <tbody>{_render_item_rows(items)}</tbody>
      </table>
    </div>
    """


@router.get("/reconciliation", response_class=HTMLResponse)
@router.get("/reconciliacao", response_class=HTMLResponse)
async def reconciliation_page(status: Optional[str] = "open", external_type: Optional[str] = "all", notice: Optional[str] = None, days: Optional[int] = 3):
    status = status or "open"
    external_type = external_type or "all"
    item_type = None if external_type == "all" else external_type
    recent_days = min(max(int(days or 3), 1), 90)
    items = sort_items_for_operator(list_reconciliation_items(status=status, external_type=item_type, limit=100, days=recent_days))
    process_candidates = list_reconciliation_process_candidates(status="open", days=recent_days, limit=8)
    summary = reconciliation_summary(days=recent_days)
    enrichment_summary = fiscal_enrichment_summary()

    notice_html = f'<div class="alert alert-info border-0">{esc(notice)}</div>' if notice else ""
    status_filters = "".join(
        f'<a class="btn btn-sm {"btn-primary" if status == value else "btn-outline-secondary"}" href="{_filter_url(value, external_type, recent_days)}">{esc(label)}</a>'
        for value, label in [("open", "Abertos"), ("needs_review", "Revisão"), ("conflict", "Conflitos"), ("historical", "Histórico"), ("linked", "Ligados"), ("resolved", "Resolvidos"), ("ignored", "Ignorados"), ("all", "Todos")]
    )
    type_filters = "".join(
        f'<a class="btn btn-sm {"btn-primary" if external_type == value else "btn-outline-secondary"}" href="{_filter_url(status, value, recent_days)}">{esc(label)}</a>'
        for value, label in [("all", "Todos"), ("jasmin_quotation", "Orçamentos"), ("jasmin_invoice", "Faturas"), ("payment_proof", "Comprovativos"), ("odoo_sale_order", "Odoo"), ("packlink_shipment", "Packlink"), ("manual_request", "Externos")]
    )

    body = f"""
    {notice_html}
    <section class="alert alert-primary border-0 shadow-sm">
      <strong>Reconciliação = informação solta para organizar.</strong>
      O sistema cria candidatos por cliente fiscal e por compra/processo; o operador decide ligar, criar oportunidade ou ignorar. Janela ativa: últimos {recent_days} dias operacionais, desde {esc(recent_window_start(recent_days))}. Comprovativo recebido não confirma pagamento.
    </section>

    <section class="cf-kpi-grid">
      {kpi_card('Itens abertos', summary.get('open', 0), '/reconciliation', 'pendências para ligar/criar/ignorar', 'bi-diagram-3')}
      {kpi_card('Conflitos', summary.get('conflict', 0), '/reconciliation?status=conflict', 'cliente/processo com risco', 'bi-exclamation-triangle')}
      {kpi_card('Histórico', summary.get('historical', 0), '/reconciliation?status=historical', 'fora da operação diária', 'bi-archive')}
      {kpi_card('Resolvidos', summary.get('resolved', 0), '/reconciliation?status=linked', 'ligados ou fechados', 'bi-check2-circle')}
      {kpi_card('Sugestões fiscais', enrichment_summary.get('pending_suggestions', 0), '/opportunities?scope=blocked', 'pendentes antes da reconciliação', 'bi-person-vcard')}
    </section>

    {render_reconciliation_decision_board(items, process_candidates, status=status, external_type=external_type, days=recent_days)}

    <section class="card cf-card mb-3"><div class="card-body p-3 d-grid gap-3">
      <div><strong class="me-2">Estado:</strong><span class="d-inline-flex flex-wrap gap-2">{status_filters}</span></div>
      <div><strong class="me-2">Tipo:</strong><span class="d-inline-flex flex-wrap gap-2">{type_filters}</span></div>
      <div><strong class="me-2">Janela:</strong><span class="d-inline-flex flex-wrap gap-2">
        {''.join(f'<a class="btn btn-sm {"btn-primary" if recent_days == value else "btn-outline-secondary"}" href="/reconciliation?status={esc(status)}&external_type={esc(external_type)}&days={value}">{label}</a>' for value, label in [(1, 'Hoje'), (3, '3 dias'), (7, '7 dias'), (30, '30 dias')])}
      </span></div>
      <div class="d-flex flex-wrap gap-2 align-items-center">
        <form class="d-inline-block" method="post" action="/reconciliation/sync-local-documents" hx-confirm="Procurar documentos locais sem oportunidade?">
          <button class="btn btn-sm btn-outline-primary" type="submit">Procurar documentos locais</button>
        </form>
        <form class="d-inline-block" method="post" action="/reconciliation/sync-jasmin" hx-confirm="Consultar API Jasmin e criar candidatos de reconciliação?">
          <input type="hidden" name="days" value="{recent_days}"><button class="btn btn-sm btn-outline-primary" type="submit">Sincronizar Jasmin</button>
        </form>
        <form class="d-inline-block" method="post" action="/reconciliation/sync-odoo" hx-confirm="Consultar API Odoo e criar candidatos de reconciliação?">
          <input type="hidden" name="days" value="{recent_days}"><button class="btn btn-sm btn-outline-primary" type="submit">Sincronizar Odoo</button>
        </form>
        <form class="d-inline-block" method="post" action="/reconciliation/sync-packlink" hx-confirm="Consultar API Packlink e criar candidatos de reconciliação?">
          <input type="hidden" name="days" value="{recent_days}"><button class="btn btn-sm btn-outline-primary" type="submit">Sincronizar Packlink</button>
        </form>
        <form class="d-inline-block" method="post" action="/reconciliation/enrich-fiscal" hx-confirm="Enriquecer oportunidades abertas sem cliente fiscal antes da reconciliação? Auto-associa apenas matches muito fortes.">
          <button class="btn btn-sm btn-outline-success" type="submit">Enriquecer oportunidades</button>
        </form>
        <form class="d-inline-block" method="post" action="/reconciliation/sync-external" hx-confirm="Consultar APIs externas ativas na janela ativa? A sequência é: enriquecimento fiscal → clientes fiscais → documentos Jasmin → vendas Odoo → envios Packlink. Não cria oportunidades automaticamente.">
          <input type="hidden" name="days" value="{recent_days}"><button class="btn btn-sm btn-primary" type="submit">Sincronizar APIs externas</button>
        </form>
        <form class="d-inline-block" method="post" action="/reconciliation/rebuild" hx-confirm="Reconstruir a reconciliação da janela ativa? Vai apagar apenas candidatos gerados por Jasmin/Odoo/Packlink sem oportunidade ligada, criar backup e correr a sequência enriquecimento fiscal → clientes fiscais → Jasmin → Odoo → faturas/envios. Não apaga clientes, oportunidades nem documentos reais.">
          <input type="hidden" name="days" value="{recent_days}"><button class="btn btn-sm btn-danger" type="submit">Apagar e correr novamente</button>
        </form>
        <form class="d-inline-block" method="post" action="/reconciliation/cleanup-window" hx-confirm="Limpar da fila aberta os candidatos fora da janela ativa? Não apaga documentos externos.">
          <input type="hidden" name="days" value="{recent_days}"><button class="btn btn-sm btn-outline-danger" type="submit">Limpar fora dos 3 dias/janela</button>
        </form>
      </div>
    </div></section>

    {_render_process_candidates(process_candidates, days=recent_days)}

    <section class="card cf-card mb-4">
      <div class="card-body p-0">
        <div class="p-3 border-bottom"><h2 class="cf-section-title mb-1">Itens soltos</h2><div class="small text-secondary">Itens individuais que ainda precisam de decisão ou que não formaram um processo candidato.</div></div>
        {render_reconciliation_table(items)}
      </div>
    </section>

    <div class="row g-3">
      <div class="col-xl-6">
        <section class="card cf-card h-100"><div class="card-body p-4">
          <h2 class="cf-section-title mb-3">Registar pedido externo</h2>
          <form method="post" action="/reconciliation/manual-request" class="d-grid gap-3">
            <div class="row g-2"><div class="col-md-4"><label class="form-label small fw-bold">Origem</label><select class="form-select" name="source_channel"><option>WhatsApp</option><option>Telefone</option><option>Email direto</option><option>Presencial</option><option>Outro</option></select></div><div class="col-md-8"><label class="form-label small fw-bold">Nome/contacto</label><input class="form-control" name="customer_name" required></div></div>
            <div class="row g-2"><div class="col-md-6"><label class="form-label small fw-bold">Email</label><input class="form-control" name="customer_email" type="email" autocomplete="off"></div><div class="col-md-6"><label class="form-label small fw-bold">Telefone</label><input class="form-control" name="customer_phone" type="tel" autocomplete="off"></div></div>
            <div class="row g-2"><div class="col-md-6"><label class="form-label small fw-bold">Produto/interesse</label><input class="form-control" name="product_interest" placeholder="Carregador EV, Cabo..."></div><div class="col-md-6"><label class="form-label small fw-bold">Próxima ação</label><select class="form-select" name="action_code"><option value="SEND_QUOTE">Preparar orçamento</option><option value="SEND_INFO">Preparar resposta</option><option value="SEND_PROFORMA">Enviar orçamento para pagamento</option><option value="SEND_INVOICE">Emitir fatura</option><option value="REVIEW_MANUALLY">Rever manualmente</option></select></div></div>
            <div><label class="form-label small fw-bold">Mensagem/pedido</label><textarea class="form-control" rows="3" name="request_text" placeholder="Colar texto do WhatsApp, telefone ou email direto..."></textarea></div>
            <div><button class="btn btn-primary" type="submit">Guardar e criar oportunidade</button></div>
          </form>
        </div></section>
      </div>

      <div class="col-xl-6">
        <section class="card cf-card h-100"><div class="card-body p-4">
          <h2 class="cf-section-title mb-3">Adicionar comprovativo</h2>
          <form method="post" action="/reconciliation/payment-proof" class="d-grid gap-3">
            <div class="alert alert-warning border-0 py-2 mb-0"><strong>Regra:</strong> comprovativo recebido abre validação financeira; não confirma pagamento.</div>
            <div class="row g-2"><div class="col-md-7"><label class="form-label small fw-bold">ID oportunidade, se conhecido</label><input class="form-control" name="opportunity_id" placeholder="opcional"></div><div class="col-md-5"><label class="form-label small fw-bold">Origem</label><select class="form-select" name="source_system"><option>WhatsApp</option><option>Chatwoot</option><option>Email</option><option>Manual</option></select></div></div>
            <div class="row g-2"><div class="col-md-6"><label class="form-label small fw-bold">Valor indicado</label><input class="form-control" name="amount" placeholder="190,00"></div><div class="col-md-6"><label class="form-label small fw-bold">Ficheiro/ref.</label><input class="form-control" name="filename" placeholder="comprovativo.jpg"></div></div>
            <div><label class="form-label small fw-bold">Nota</label><textarea class="form-control" rows="3" name="note" placeholder="Referência, banco, MB Way, observações..."></textarea></div>
            <div><button class="btn btn-primary" type="submit">Guardar comprovativo</button></div>
          </form>
        </div></section>
      </div>
    </div>

    <section class="card cf-card mt-3"><div class="card-body p-4">
      <h2 class="cf-section-title mb-3">Criar candidato manual de reconciliação</h2>
      <form method="post" action="/reconciliation/item" class="row g-2 align-items-end">
        <div class="col-md-2"><label class="form-label small fw-bold">Sistema</label><select class="form-select" name="source_system"><option>jasmin</option><option>odoo</option><option>manual</option></select></div>
        <div class="col-md-2"><label class="form-label small fw-bold">Tipo</label><select class="form-select" name="external_type"><option value="jasmin_quotation">Orçamento</option><option value="jasmin_invoice">Fatura</option><option value="odoo_sale_order">Venda Odoo</option><option value="external_record">Outro</option></select></div>
        <div class="col-md-2"><label class="form-label small fw-bold">Nº documento</label><input class="form-control" name="document_number"></div>
        <div class="col-md-2"><label class="form-label small fw-bold">Cliente</label><input class="form-control" name="customer_name"></div>
        <div class="col-md-2"><label class="form-label small fw-bold">Valor</label><input class="form-control" name="amount"></div>
        <div class="col-md-2"><button class="btn btn-outline-primary w-100" type="submit">Criar candidato</button></div>
      </form>
    </div></section>
    """
    return layout("Reconciliação", "Organizar documentos, vendas e comprovativos fora do ClientFlow", body, "reconciliation")


@router.post("/reconciliation/sync-local-documents")
async def reconciliation_sync_local_documents():
    result = sync_local_documents_without_opportunity(limit=200)
    notice = f"Documentos analisados: {result.get('seen', 0)} · candidatos criados/atualizados: {result.get('created_or_updated', 0)}"
    return RedirectResponse(f"/reconciliation?notice={esc(notice)}", status_code=303)


@router.post("/reconciliation/sync-jasmin")
async def reconciliation_sync_jasmin(request: Request):
    form = await request.form()
    days = _safe_days(form.get("days"), 3)
    result = await sync_jasmin_reconciliation_candidates(limit=50, days=days)
    notice = f"Jasmin últimos {result.get('days', days)} dias: analisados {result.get('seen', 0)} · candidatos criados/atualizados {result.get('created_or_updated', 0)}"
    if result.get("errors"):
        notice += " · erros: " + "; ".join(str(x) for x in result.get("errors", [])[:2])
    if result.get("skipped"):
        notice += " · " + str(result.get("skipped"))
    return RedirectResponse(f"/reconciliation?days={days}&notice={esc(notice)}", status_code=303)


@router.post("/reconciliation/sync-odoo")
async def reconciliation_sync_odoo(request: Request):
    form = await request.form()
    days = _safe_days(form.get("days"), 3)
    result = sync_odoo_reconciliation_candidates(limit=50, days=days)
    notice = f"Odoo últimos {result.get('days', days)} dias: analisadas {result.get('seen', 0)} vendas · candidatos criados/atualizados {result.get('created_or_updated', 0)}"
    if result.get("already_linked"):
        notice += f" · já ligadas {result.get('already_linked', 0)}"
    if result.get("resolved_existing"):
        notice += f" · candidatos obsoletos resolvidos {result.get('resolved_existing', 0)}"
    if result.get("link_conflicts"):
        notice += f" · conflitos de ligação {result.get('link_conflicts', 0)}"
    if result.get("skipped"):
        notice += " · " + str(result.get("skipped"))
    return RedirectResponse(f"/reconciliation?days={days}&notice={esc(notice)}", status_code=303)


@router.post("/reconciliation/sync-packlink")
async def reconciliation_sync_packlink(request: Request):
    form = await request.form()
    days = _safe_days(form.get("days"), 3)
    result = await sync_packlink_reconciliation_candidates(limit=50, days=days)
    notice = f"Packlink últimos {result.get('days', days)} dias: analisados {result.get('seen', 0)} envios · candidatos criados/atualizados {result.get('created_or_updated', 0)}"
    if result.get("errors"):
        notice += " · erros: " + "; ".join(str(x) for x in result.get("errors", [])[:2])
    if result.get("skipped"):
        notice += " · " + str(result.get("skipped"))
    return RedirectResponse(f"/reconciliation?days={days}&notice={esc(notice)}", status_code=303)


@router.post("/reconciliation/enrich-fiscal")
async def reconciliation_enrich_fiscal(request: Request):
    result = enrich_open_opportunities(limit=200, apply_safe=True, mode="operator_ui")
    notice = (
        f"Enriquecimento fiscal: analisadas {result.get('seen', 0)} oportunidades"
        f" · sugestões {result.get('suggested', 0)}"
        f" · auto-associadas {result.get('auto_applied', 0)}"
        f" · ignoradas {result.get('skipped', 0)}"
    )
    if result.get("errors"):
        notice += " · erros: " + "; ".join(str(x) for x in result.get("errors", [])[:2])
    return RedirectResponse(f"/reconciliation?notice={esc(notice)}", status_code=303)


@router.post("/reconciliation/sync-external")
async def reconciliation_sync_external(request: Request):
    form = await request.form()
    days = _safe_days(form.get("days"), 3)
    result = await sync_all_external_reconciliation_candidates(limit=50, days=days)
    notice = (
        f"APIs externas últimos {days} dias: enriquecidas {result.get('enrichment_auto_applied', 0)} oportunidades"
        f" · sugestões fiscais {result.get('enrichment_suggested', 0)}"
        f" · clientes fiscais analisados {result.get('customer_seen', 0)}"
        f" · clientes criados/atualizados {result.get('customers_created_or_updated', 0)}"
        f" · documentos/vendas analisados {result.get('seen', 0)}"
        f" · candidatos criados/atualizados {result.get('created_or_updated', 0)}"
    )
    return RedirectResponse(f"/reconciliation?days={days}&notice={esc(notice)}", status_code=303)


@router.post("/reconciliation/rebuild")
async def reconciliation_rebuild(request: Request):
    form = await request.form()
    days = _safe_days(form.get("days"), 3)
    reset = reset_generated_reconciliation_items(days=days, limit=2000, actor="operator_ui", apply=True)
    result = await sync_all_external_reconciliation_candidates(limit=200, days=days)
    notice = (
        f"Reconciliação reconstruída para {days} dias: apagados {reset.get('deleted', 0)} candidatos gerados"
        f" · backup {reset.get('backup_table') or 'sem alterações'}"
        f" · enriquecidas {result.get('enrichment_auto_applied', 0)} oportunidades"
        f" · sugestões fiscais {result.get('enrichment_suggested', 0)}"
        f" · clientes fiscais analisados {result.get('customer_seen', 0)}"
        f" · clientes criados/atualizados {result.get('customers_created_or_updated', 0)}"
        f" · documentos/vendas analisados {result.get('seen', 0)}"
        f" · candidatos criados/atualizados {result.get('created_or_updated', 0)}"
    )
    errors = []
    for source_result in result.get("results", []) or []:
        errors.extend(str(err) for err in source_result.get("errors", []) or [])
    if errors:
        notice += " · erros: " + "; ".join(errors[:2])
    return RedirectResponse(f"/reconciliation?days={days}&notice={esc(notice)}", status_code=303)


@router.post("/reconciliation/cleanup-window")
async def reconciliation_cleanup_window(request: Request):
    form = await request.form()
    days = _safe_days(form.get("days"), 3)
    result = cleanup_reconciliation_outside_window(days=days, limit=2000, actor="operator_ui")
    notice = f"Limpeza aplicada: {result.get('ignored', 0)} itens fora dos últimos {result.get('days', days)} dias foram marcados como ignorados."
    return RedirectResponse(f"/reconciliation?days={days}&notice={esc(notice)}", status_code=303)


@router.post("/reconciliation/manual-request")
async def reconciliation_manual_request(request: Request):
    form = await request.form()
    result = create_external_request(
        source_channel=str(form.get("source_channel") or "manual"),
        customer_name=str(form.get("customer_name") or ""),
        customer_email=str(form.get("customer_email") or ""),
        customer_phone=str(form.get("customer_phone") or ""),
        product_interest=str(form.get("product_interest") or ""),
        request_text=str(form.get("request_text") or ""),
        action_code=str(form.get("action_code") or "SEND_QUOTE"),
    )
    opportunity_id = result.get("opportunity_id")
    return RedirectResponse(f"/opportunities/{opportunity_id}?notice=Pedido%20externo%20registado", status_code=303)


@router.post("/reconciliation/payment-proof")
async def reconciliation_payment_proof(request: Request):
    form = await request.form()
    proof = create_payment_proof(
        opportunity_id=str(form.get("opportunity_id") or "").strip() or None,
        source_system=str(form.get("source_system") or "manual"),
        source_ref=str(form.get("source_ref") or ""),
        filename=str(form.get("filename") or ""),
        amount=str(form.get("amount") or ""),
        note=str(form.get("note") or ""),
    )
    if proof.get("task_id") and proof.get("opportunity_id"):
        return RedirectResponse(f"/opportunities/{proof.get('opportunity_id')}?notice=Comprovativo%20registado%20para%20validação", status_code=303)
    return RedirectResponse("/reconciliation?external_type=payment_proof&notice=Comprovativo%20por%20associar%20registado", status_code=303)


@router.post("/reconciliation/item")
async def reconciliation_create_item(request: Request):
    form = await request.form()
    source_system = str(form.get("source_system") or "manual")
    external_type = str(form.get("external_type") or "external_record")
    document_number = str(form.get("document_number") or "").strip()
    customer_name = str(form.get("customer_name") or "").strip()
    title = f"{_type_label(external_type)} sem ligação"
    if document_number:
        title += f" · {document_number}"
    upsert_reconciliation_item(
        source_system=source_system,
        external_type=external_type,
        external_id=document_number or None,
        title=title,
        description="Candidato criado manualmente para ligação/criação de oportunidade.",
        customer_name=customer_name,
        document_number=document_number,
        amount=str(form.get("amount") or ""),
        suggested_action="CONFIRM_PAYMENT" if external_type == "jasmin_invoice" else "SEND_PROFORMA",
    )
    return RedirectResponse("/reconciliation?notice=Candidato%20criado", status_code=303)



@router.post("/reconciliation/processes/create-opportunity")
async def reconciliation_create_process_opportunity(request: Request):
    form = await request.form()
    item_ids = [part.strip() for part in str(form.get("item_ids") or "").split(",") if part.strip()]
    opportunity_id = create_opportunity_from_reconciliation_process(item_ids)
    if opportunity_id:
        return RedirectResponse(f"/opportunities/{opportunity_id}?notice=Oportunidade%20reconstruída%20a%20partir%20da%20reconciliação", status_code=303)
    return RedirectResponse("/reconciliation?notice=Não%20foi%20possível%20reconstruir%20oportunidade", status_code=303)


@router.post("/reconciliation/processes/link")
async def reconciliation_link_process(request: Request):
    form = await request.form()
    item_ids = [part.strip() for part in str(form.get("item_ids") or "").split(",") if part.strip()]
    opportunity_id = str(form.get("opportunity_id") or "").strip()
    if opportunity_id and item_ids:
        try:
            count = link_reconciliation_process_to_opportunity(item_ids, opportunity_id)
            return RedirectResponse(f"/opportunities/{opportunity_id}?notice={count}%20itens%20de%20processo%20ligados", status_code=303)
        except Exception as exc:  # pragma: no cover - production guard
            logger.exception("failed to link reconstructed reconciliation process to opportunity %s", opportunity_id)
            return RedirectResponse(
                "/reconciliation?notice=Erro%20ao%20ligar%20processo.%20Ver%20journal%20do%20servi%C3%A7o.",
                status_code=303,
            )
    return RedirectResponse("/reconciliation?notice=Indica%20o%20ID%20da%20oportunidade", status_code=303)


async def _set_reconciliation_process_status(request: Request, *, status: str, note: str, notice: str):
    form = await request.form()
    item_ids = [part.strip() for part in str(form.get("item_ids") or "").split(",") if part.strip()]
    for item_id in item_ids:
        set_reconciliation_status(item_id, status=status, note=note)
    return RedirectResponse(f"/reconciliation?status={status}&notice={notice}", status_code=303)


@router.post("/reconciliation/processes/needs-review")
async def reconciliation_process_needs_review(request: Request):
    return await _set_reconciliation_process_status(
        request,
        status="needs_review",
        note="Processo enviado para revisão manual.",
        notice="Processo%20enviado%20para%20revis%C3%A3o",
    )


@router.post("/reconciliation/processes/historical")
async def reconciliation_process_historical(request: Request):
    return await _set_reconciliation_process_status(
        request,
        status="historical",
        note="Processo marcado como histórico sem ação operacional.",
        notice="Processo%20marcado%20como%20hist%C3%B3rico",
    )


@router.post("/reconciliation/processes/ignore")
async def reconciliation_process_ignore(request: Request):
    return await _set_reconciliation_process_status(
        request,
        status="ignored",
        note="Processo ignorado manualmente pelo operador.",
        notice="Processo%20ignorado",
    )


@router.post("/reconciliation/{item_id}/needs-review")
async def reconciliation_needs_review(item_id: str):
    set_reconciliation_status(item_id, status="needs_review", note="Enviado para revisão manual.")
    return RedirectResponse("/reconciliation?status=needs_review&notice=Item%20enviado%20para%20revis%C3%A3o", status_code=303)


@router.post("/reconciliation/{item_id}/ignore")
async def reconciliation_ignore(item_id: str):
    set_reconciliation_status(item_id, status="ignored", note="Ignorado manualmente.")
    return RedirectResponse("/reconciliation?notice=Item%20ignorado", status_code=303)


@router.post("/reconciliation/{item_id}/historical")
async def reconciliation_historical(item_id: str):
    set_reconciliation_status(item_id, status="historical", note="Marcado como histórico sem ação operacional.")
    return RedirectResponse("/reconciliation?status=historical&notice=Item%20marcado%20como%20hist%C3%B3rico", status_code=303)


@router.post("/reconciliation/{item_id}/create-opportunity")
async def reconciliation_create_opportunity(item_id: str):
    opportunity_id = create_opportunity_from_reconciliation(item_id)
    if opportunity_id:
        return RedirectResponse(f"/opportunities/{opportunity_id}?notice=Oportunidade%20criada%20a%20partir%20de%20reconciliação", status_code=303)
    return RedirectResponse("/reconciliation?notice=Não%20foi%20possível%20criar%20oportunidade", status_code=303)


@router.post("/reconciliation/{item_id}/link")
async def reconciliation_link(item_id: str, request: Request):
    form = await request.form()
    opportunity_id = str(form.get("opportunity_id") or "").strip()
    if opportunity_id:
        link_reconciliation_to_opportunity(item_id, opportunity_id)
        return RedirectResponse(f"/opportunities/{opportunity_id}?notice=Item%20de%20reconciliação%20ligado", status_code=303)
    return RedirectResponse("/reconciliation?notice=Indica%20o%20ID%20da%20oportunidade", status_code=303)
