from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any

from .types import PAYMENT_BEFORE_SHIPPING, DELIVERY_CARRIER, TERMINAL_STAGES


QUOTE_KINDS = {"quotation", "quote", "orc", "orcamento", "jasmin_quotation", "proforma", "jasmin_proforma"}  # proforma is legacy alias only.
INVOICE_KINDS = {"invoice", "fatura", "fa", "ft", "jasmin_invoice"}


def _s(value: Any) -> str:
    return str(value or "").strip()


def _upper(value: Any) -> str:
    return _s(value).upper()


def _doc_number(doc: dict[str, Any] | None) -> str:
    if not doc:
        return ""
    return _s(doc.get("document_number") or doc.get("external_id") or doc.get("number") or doc.get("id"))


@dataclass(frozen=True)
class OpportunityEvidence:
    """Normalized facts used by the workflow engine.

    This object deliberately contains no HTML and no SQLAlchemy objects. Builders
    can create it from DB rows, test fixtures or future integration adapters.
    """

    opportunity_id: str = ""
    company_profile: str = "blif"
    stage: str = "NEW_LEAD"
    status: str = "open"
    is_terminal: bool = False
    is_reconstructed: bool = False

    has_fiscal_customer: bool = False
    fiscal_identity_validated: bool = False
    fiscal_data_complete: bool = False
    has_fiscal_conflict: bool = False
    has_nif_conflict: bool = False

    has_quote: bool = False
    quote_id: str | None = None
    quote_number: str | None = None
    quote_amount: float | None = None
    # A completed SEND_QUOTE task is separate evidence from a linked Jasmin document.
    # It prevents duplicate quote creation while still requiring document reconciliation.
    quote_sent: bool = False

    has_invoice: bool = False
    invoice_id: str | None = None
    invoice_number: str | None = None
    invoice_amount: float | None = None
    invoice_sent: bool | None = None

    payment_confirmed: bool = False
    payment_terms: str = PAYMENT_BEFORE_SHIPPING
    delivery_terms: str = DELIVERY_CARRIER

    has_odoo_sale: bool = False
    odoo_in_production: bool = False
    odoo_physical_ready: bool = False
    odoo_physical_validated: bool = False
    odoo_ready: bool = False
    order_shipped: bool = False
    order_delivered: bool = False

    has_pending_task: bool = False
    has_invalid_payment_task_without_document: bool = False
    invalid_payment_task_id: str | None = None
    invalid_payment_task_action_code: str | None = None
    pending_task_id: str | None = None
    pending_task_action_code: str | None = None
    pending_task_label: str | None = None
    pending_task_note: str | None = None

    has_reconciliation_candidate: bool = False
    reconciliation_label: str | None = None

    messages: list[str] = field(default_factory=list)

    @property
    def primary_document_number(self) -> str:
        return self.invoice_number or self.quote_number or ""

    @property
    def primary_document_id(self) -> str | None:
        return self.invoice_id or self.quote_id


def _find_doc(docs: list[dict[str, Any]], kinds: set[str]) -> dict[str, Any] | None:
    for doc in docs:
        kind = _s(doc.get("document_kind") or doc.get("kind") or doc.get("type")).lower()
        role = _s(doc.get("role") or "current").lower()
        active = doc.get("is_active", True)
        if kind in kinds and role in {"current", "accepted", "historical", "history"} and active is not False:
            return doc
    return None


def _amount(doc: dict[str, Any] | None) -> float | None:
    if not doc:
        return None
    value = doc.get("total_amount", doc.get("amount", doc.get("total")))
    try:
        return float(value) if value not in (None, "") else None
    except Exception:
        return None


def _document_sent_evidence(doc: dict[str, Any] | None) -> bool:
    """Return True when the document row itself carries sent evidence.

    Jasmin imports do not always expose a stable "sent" status.  From
    v4928.1.5.85 the repair/audit workflow can store local evidence on
    commercial_documents.payload.  Completed SEND_INVOICE tasks are handled by
    _completed_send_invoice_task_evidence below because older rows often have
    the evidence only in tasks, not in the document payload.
    """
    if not doc:
        return False
    payload = _payload_dict(doc.get("payload"))
    return bool(
        doc.get("sent_at")
        or doc.get("sent")
        or _s(doc.get("status")).lower() in {"sent", "issued_sent"}
        or payload.get("clientflow_invoice_sent_evidence")
        or payload.get("invoice_sent_at")
    )


def _task_is_completed(task: dict[str, Any]) -> bool:
    status = _s(task.get("status")).lower()
    return status in {
        "done",
        "completed",
        "complete",
        "closed",
        "resolved",
        "concluida",
        "concluído",
        "concluída",
    } or bool(task.get("completed_at"))


def _completed_send_quote_task_evidence(tasks: list[dict[str, Any]]) -> bool:
    """Return True when this opportunity already has a completed SEND_QUOTE task.

    Sending a quote and linking its Jasmin row are distinct facts. Older flows
    often completed the operator task before reconciliation imported the
    document. Treating the missing document as "quote not sent" caused duplicate
    quote creation and obsolete quote follow-ups.
    """
    for task in tasks or []:
        if _upper(task.get("action_code")) == "SEND_QUOTE" and _task_is_completed(task):
            return True
    return False


def _completed_send_invoice_task_evidence(tasks: list[dict[str, Any]], invoice: dict[str, Any] | None) -> bool:
    from app.invoice_evidence import completed_send_invoice_task_evidence

    if not invoice:
        return False
    return completed_send_invoice_task_evidence(tasks, [invoice])

def _operation_card(snapshot: dict[str, Any] | None, key: str) -> dict[str, Any]:
    for card in (snapshot or {}).get("cards", []) or []:
        if _s(card.get("key")) == key:
            return card if isinstance(card, dict) else {}
    return {}


def _operation_status(snapshot: dict[str, Any] | None, key: str) -> str:
    return _s(_operation_card(snapshot, key).get("status"))


def _payload_dict(value: Any) -> dict[str, Any]:
    if isinstance(value, dict):
        return value
    if isinstance(value, str) and value.strip():
        try:
            import json
            parsed = json.loads(value)
            return parsed if isinstance(parsed, dict) else {}
        except Exception:
            return {}
    return {}


def _payload_list(value: Any) -> list[Any]:
    if isinstance(value, list):
        return value
    if isinstance(value, str) and value.strip():
        try:
            import json
            parsed = json.loads(value)
            return parsed if isinstance(parsed, list) else []
        except Exception:
            return []
    return []


def _operation_link_exists(snapshot: dict[str, Any] | None, *, system: str, external_type: str, statuses: set[str] | None = None) -> bool:
    statuses = statuses or set()
    for link in (snapshot or {}).get("links", []) or []:
        if _s(link.get("system")) == system and _s(link.get("external_type")) == external_type:
            return not statuses or _s(link.get("status")) in statuses
    return False


def build_opportunity_evidence(
    opportunity: dict[str, Any] | None = None,
    *,
    linked_documents: list[dict[str, Any]] | None = None,
    tasks: list[dict[str, Any]] | None = None,
    operation_snapshot: dict[str, Any] | None = None,
    linked_customer: dict[str, Any] | None = None,
    fiscal_data_complete: bool | None = None,
    has_reconciliation_candidate: bool = False,
    reconciliation_label: str | None = None,
    company_profile: str = "blif",
) -> OpportunityEvidence:
    """Normalize current opportunity state into engine evidence.

    The builder is intentionally defensive: it accepts plain dictionaries from
    UI services and from tests. Future adapters for other ERPs/CRMs should map
    their source payloads into the same fields instead of changing the rules.
    """

    opportunity = opportunity or {}
    docs = list(linked_documents or [])
    tasks = list(tasks or [])
    metadata = opportunity.get("metadata") if isinstance(opportunity.get("metadata"), dict) else {}
    stage = _upper(opportunity.get("stage") or "NEW_LEAD")
    status = _s(opportunity.get("status") or "open").lower()
    terminal = status == "closed" or stage in TERMINAL_STAGES
    record_mode = _s(metadata.get("clientflow_record_mode"))

    quote = _find_doc(docs, QUOTE_KINDS)
    invoice = _find_doc(docs, INVOICE_KINDS)
    quote_sent = bool(quote) or _completed_send_quote_task_evidence(tasks)

    pending_task = None
    invalid_payment_task = None
    for task in tasks:
        if _s(task.get("status")) == "pending":
            action_code = _upper(task.get("action_code"))
            # A pending payment-confirmation task is not actionable without any
            # commercial document.  Old triage sometimes inferred
            # CONFIRM_PAYMENT merely from subjects like "Orçamento 2026/193".
            # Ignore it as the primary task so the workflow can require a quote
            # or document association first.
            if action_code in {"CONFIRM_PAYMENT", "FOLLOW_UP_PAYMENT"} and not (quote or invoice):
                invalid_payment_task = invalid_payment_task or task
                continue
            pending_task = task
            break

    payment_confirmed = _operation_link_exists(
        operation_snapshot,
        system="clientflow",
        external_type="payment",
        statuses={"confirmed"},
    ) or stage == "PAYMENT_CONFIRMED"

    odoo_sale_status = _operation_status(operation_snapshot, "odoo_sale_order")
    # O ClientFlow deve decidir pelo estado da venda/encomenda e sobretudo pelo
    # picking/WH-OUT.  Ordens de fabrico/MO são detalhe técnico do Odoo e não
    # devem bloquear nem conduzir a próxima ação comercial.
    production_status = _operation_status(operation_snapshot, "odoo_physical_status")
    physical_status_card = _operation_card(operation_snapshot, "odoo_physical_status")
    physical_payload = _payload_dict(physical_status_card.get("payload"))
    physical_status = _s(physical_status_card.get("status") or physical_payload.get("physical_status") or physical_payload.get("status")).lower()
    physical_text = " ".join(
        _s(physical_payload.get(key))
        for key in ("label", "reason", "next_action", "state", "status", "physical_status")
    ).casefold()
    outgoing_pickings = _payload_list(physical_payload.get("outgoing_pickings") or physical_payload.get("pickings"))
    picking_states = {
        _s(p.get("state")).lower()
        for p in outgoing_pickings
        if isinstance(p, dict) and _s(p.get("state"))
    }
    physical_done = (
        bool(physical_payload.get("delivery_done"))
        or physical_status in {"done", "shipped", "delivered", "validated"}
        or (bool(picking_states) and picking_states <= {"done", "cancel"} and "done" in picking_states)
    )
    physical_ready = (
        bool(physical_payload.get("ready_to_ship") or physical_payload.get("delivery_ready"))
        or physical_status in {"ready_to_ship", "validated", "ready"}
        or "assigned" in picking_states
    )
    physical_indicates_production = (
        bool(physical_status_card)
        and not physical_done
        and not physical_ready
        and (
            physical_status not in {"", "not_created", "not_found", "no_order", "cancelled", "shipped", "done", "delivered", "validated"}
            or any(token in physical_text for token in ("produção", "produc", "prepara", "confirmed", "aguardar", "waiting", "em curso"))
        )
    )
    physical_validation_status = _operation_status(operation_snapshot, "physical_validation")
    shipment_status = _operation_status(operation_snapshot, "packlink_shipment")

    if fiscal_data_complete is None:
        fiscal_data_complete = bool(linked_customer and linked_customer.get("tax_id"))

    has_conflict = bool(
        opportunity.get("has_nif_conflict")
        or metadata.get("has_nif_conflict")
        or metadata.get("fiscal_conflict")
    )

    invoice_sent = _document_sent_evidence(invoice) or _completed_send_invoice_task_evidence(tasks, invoice)

    return OpportunityEvidence(
        opportunity_id=_s(opportunity.get("id")),
        company_profile=company_profile,
        stage=stage,
        status=status,
        is_terminal=terminal,
        is_reconstructed=record_mode in {"reconstructed_invoice_review", "historical_reconstructed", "legacy_review"},
        has_fiscal_customer=bool(linked_customer or opportunity.get("fiscal_customer_id") or opportunity.get("customer_id")),
        fiscal_identity_validated=bool(linked_customer or opportunity.get("fiscal_customer_id")),
        fiscal_data_complete=bool(fiscal_data_complete),
        has_fiscal_conflict=has_conflict,
        has_nif_conflict=has_conflict,
        has_quote=bool(quote),
        quote_id=_s(quote.get("id")) if quote else None,
        quote_number=_doc_number(quote) or None,
        quote_amount=_amount(quote),
        quote_sent=quote_sent,
        has_invoice=bool(invoice),
        invoice_id=_s(invoice.get("id")) if invoice else None,
        invoice_number=_doc_number(invoice) or None,
        invoice_amount=_amount(invoice),
        # invoice_sent=None if not invoice else _document_sent_evidence(invoice)
        invoice_sent=None if not invoice else invoice_sent,
        payment_confirmed=payment_confirmed,
        payment_terms=_s(metadata.get("payment_terms") or PAYMENT_BEFORE_SHIPPING),
        delivery_terms=_s(metadata.get("delivery_terms") or DELIVERY_CARRIER),
        has_odoo_sale=odoo_sale_status not in {"", "not_created", "no_order", "not_found"} or _operation_link_exists(operation_snapshot, system="odoo", external_type="sale_order"),
        odoo_in_production=(not physical_done) and (physical_indicates_production or stage in {"IN_PRODUCTION"}),
        # Odoo picking state ``assigned`` only means stock is reserved/available.
        # It is eligible for a human physical validation, not yet for shipment.
        odoo_physical_ready=(not physical_done) and physical_ready,
        odoo_physical_validated=physical_validation_status in {"validated", "ready_to_ship"},
        odoo_ready=(not physical_done) and physical_ready and physical_validation_status in {"validated", "ready_to_ship"},
        # For workflow decisions, "shipped" means the Odoo delivery/picking flow
        # is closed/done. A Packlink shipment or a manufacturing order must not,
        # by itself, move the opportunity to close.
        order_shipped=physical_done or stage in {"SHIPPED"},
        order_delivered=physical_done or stage in {"DELIVERED", "WON"} or _operation_status(operation_snapshot, "delivery") == "delivered",
        has_pending_task=bool(pending_task),
        has_invalid_payment_task_without_document=bool(invalid_payment_task),
        invalid_payment_task_id=_s(invalid_payment_task.get("id")) if invalid_payment_task else None,
        invalid_payment_task_action_code=_upper(invalid_payment_task.get("action_code")) if invalid_payment_task else None,
        pending_task_id=_s(pending_task.get("id")) if pending_task else None,
        pending_task_action_code=_upper(pending_task.get("action_code")) if pending_task else None,
        pending_task_label=_s(pending_task.get("action") or pending_task.get("label")) if pending_task else None,
        pending_task_note=_s(pending_task.get("note") or pending_task.get("action")) if pending_task else None,
        has_reconciliation_candidate=bool(has_reconciliation_candidate),
        reconciliation_label=reconciliation_label,
    )
