"""Reconciliation packet contracts for close review and certification."""

from __future__ import annotations

import hashlib
from datetime import datetime, timezone
from typing import Any, Literal

from pydantic import BaseModel, ConfigDict, Field, computed_field

from .context import DealContextV1
from .documents import CloseDocumentCandidateV1, CloseDocumentInventoryV1
from .extraction import (
    CloseDocumentEvidenceV1,
    CloseDocumentExtractionResultV1,
    CloseDocumentFieldComparisonV1,
)
from .integrity import (
    QuoteDocumentFingerprintV1,
    QuoteFingerprintComparisonV1,
    QuoteIntegrityAssessmentV1,
    QuoteIntegrityComparisonV1,
    QuoteIntegrityFieldComparisonV1,
)
from .readiness import QuoteReadinessResult, ReadinessIssue


def _utc_now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


class _ReconciliationModel(BaseModel):
    model_config = ConfigDict(extra="ignore")

    def to_dict(self) -> dict[str, Any]:
        return self.model_dump(exclude_none=False, by_alias=True)

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "_ReconciliationModel":
        return cls.model_validate(data)


class ReconciliationEvidenceRef(_ReconciliationModel):
    source_type: Literal[
        "readiness_issue",
        "deal_context",
        "close_packet",
        "receipt",
        "document_candidate",
        "document_inventory",
        "document_evidence",
        "quote_fingerprint",
        "quote_integrity",
    ]
    reference: str
    summary: str | None = None


class ReconciliationDiscrepancyV1(_ReconciliationModel):
    discrepancy_id: str
    code: str
    severity: Literal["blocking", "warning", "info"]
    classification: Literal["auto_fixable", "approval_required", "block_close", "informational"]
    message: str
    field_name: str | None = None
    suggested_action: str | None = None
    evidence: list[ReconciliationEvidenceRef] = Field(default_factory=list)


class ReconciliationPacketV1(_ReconciliationModel):
    version: int = 1
    packet_id: str
    crm_org_id: str
    org_alias: str | None = None
    opportunity_id: str
    context_id: str | None = None
    generated_at: str
    close_workflow_state: str
    overall_disposition: Literal["queued_for_certification", "approval_required", "block_close"]
    summary: str
    discrepancies: list[ReconciliationDiscrepancyV1] = Field(default_factory=list)
    blocking_discrepancy_count: int = 0
    approval_required_count: int = 0
    auto_fixable_count: int = 0
    informational_count: int = 0
    document_evidence_status: str = "not_required"
    document_candidate_count: int = 0
    document_contract_candidate_count: int = 0
    document_summary: str = ""
    document_candidates: list[CloseDocumentCandidateV1] = Field(default_factory=list)
    document_extraction_status: str = "not_attempted"
    document_evidence_count: int = 0
    document_comparison_count: int = 0
    document_discrepancy_count: int = 0
    document_evidence_summary: str = ""
    document_evidence: list[CloseDocumentEvidenceV1] = Field(default_factory=list)
    document_field_comparisons: list[CloseDocumentFieldComparisonV1] = Field(default_factory=list)
    quote_integrity_status: str = "not_available"
    quote_integrity_summary: str = ""
    quote_integrity_discrepancy_count: int = 0
    quote_fingerprint_count: int = 0
    quote_binary_mismatch_count: int = 0
    quote_semantic_mismatch_count: int = 0
    quote_integrity: QuoteIntegrityAssessmentV1 | None = None
    quote_document_fingerprints: list[QuoteDocumentFingerprintV1] = Field(default_factory=list)
    quote_binary_comparisons: list[QuoteFingerprintComparisonV1] = Field(default_factory=list)
    quote_semantic_comparison: QuoteIntegrityComparisonV1 | None = None

    @computed_field(alias="$schema")
    @property
    def schema(self) -> str:
        return "ReconciliationPacket/v1"


def build_reconciliation_packet_from_readiness(
    *,
    crm_org_id: str,
    org_alias: str | None,
    opportunity_id: str,
    readiness: QuoteReadinessResult,
    context: DealContextV1 | None,
    document_inventory: CloseDocumentInventoryV1 | None = None,
    document_extraction: CloseDocumentExtractionResultV1 | None = None,
    quote_integrity: QuoteIntegrityAssessmentV1 | None = None,
    block_close_if_missing_documents: bool = False,
    generated_at: str | None = None,
) -> ReconciliationPacketV1:
    packet_generated_at = str(generated_at or _utc_now_iso())
    discrepancies = [
        _discrepancy_from_issue(
            issue=issue,
            opportunity_id=opportunity_id,
            context_id=context.context_id if context is not None else None,
        )
        for issue in readiness.issues
    ]
    if document_inventory is not None:
        discrepancies.extend(
            _document_discrepancies(
                inventory=document_inventory,
                opportunity_id=opportunity_id,
                context_id=context.context_id if context is not None else None,
                block_close_if_missing_documents=block_close_if_missing_documents,
            )
        )
    if document_extraction is not None:
        discrepancies.extend(
            _document_extraction_discrepancies(
                extraction=document_extraction,
                opportunity_id=opportunity_id,
                context_id=context.context_id if context is not None else None,
                inventory=document_inventory,
            )
        )
    if quote_integrity is not None:
        discrepancies.extend(
            _quote_integrity_discrepancies(
                assessment=quote_integrity,
                opportunity_id=opportunity_id,
                context_id=context.context_id if context is not None else None,
            )
        )
    blocking_count = sum(1 for item in discrepancies if item.classification == "block_close")
    approval_count = sum(1 for item in discrepancies if item.classification == "approval_required")
    auto_fixable_count = sum(1 for item in discrepancies if item.classification == "auto_fixable")
    informational_count = sum(1 for item in discrepancies if item.classification == "informational")
    if blocking_count:
        overall_disposition = "block_close"
    elif approval_count or auto_fixable_count:
        overall_disposition = "approval_required"
    else:
        overall_disposition = "queued_for_certification"

    return ReconciliationPacketV1(
        packet_id=_packet_id(
            opportunity_id=opportunity_id,
            generated_at=packet_generated_at,
            readiness_mode=readiness.readiness_mode,
            issue_codes=[issue.code for issue in readiness.issues],
        ),
        crm_org_id=crm_org_id,
        org_alias=org_alias,
        opportunity_id=opportunity_id,
        context_id=context.context_id if context is not None else readiness.context_id,
        generated_at=packet_generated_at,
        close_workflow_state=(context.close_workflow_state if context is not None else "deal_active"),
        overall_disposition=overall_disposition,
        summary=_packet_summary(
            overall_disposition=overall_disposition,
            blocking_count=blocking_count,
            approval_count=approval_count,
            auto_fixable_count=auto_fixable_count,
            informational_count=informational_count,
        ),
        discrepancies=discrepancies,
        blocking_discrepancy_count=blocking_count,
        approval_required_count=approval_count,
        auto_fixable_count=auto_fixable_count,
        informational_count=informational_count,
        document_evidence_status=(
            document_inventory.status if document_inventory is not None else "not_required"
        ),
        document_candidate_count=(
            document_inventory.candidate_count if document_inventory is not None else 0
        ),
        document_contract_candidate_count=(
            document_inventory.likely_contract_count if document_inventory is not None else 0
        ),
        document_summary=(
            document_inventory.summary if document_inventory is not None else ""
        ),
        document_candidates=(
            list(document_inventory.candidates) if document_inventory is not None else []
        ),
        document_extraction_status=(
            document_extraction.status if document_extraction is not None else "not_attempted"
        ),
        document_evidence_count=(
            document_extraction.evidence_count if document_extraction is not None else 0
        ),
        document_comparison_count=(
            document_extraction.comparison_count if document_extraction is not None else 0
        ),
        document_discrepancy_count=(
            document_extraction.discrepancy_count if document_extraction is not None else 0
        ),
        document_evidence_summary=(
            document_extraction.summary if document_extraction is not None else ""
        ),
        document_evidence=(
            list(document_extraction.evidence) if document_extraction is not None else []
        ),
        document_field_comparisons=(
            list(document_extraction.comparisons) if document_extraction is not None else []
        ),
        quote_integrity_status=(quote_integrity.status if quote_integrity is not None else "not_available"),
        quote_integrity_summary=(quote_integrity.summary if quote_integrity is not None else ""),
        quote_integrity_discrepancy_count=(
            quote_integrity.discrepancy_count if quote_integrity is not None else 0
        ),
        quote_fingerprint_count=(quote_integrity.fingerprint_count if quote_integrity is not None else 0),
        quote_binary_mismatch_count=(
            quote_integrity.binary_mismatch_count if quote_integrity is not None else 0
        ),
        quote_semantic_mismatch_count=(
            quote_integrity.semantic_mismatch_count if quote_integrity is not None else 0
        ),
        quote_integrity=quote_integrity,
        quote_document_fingerprints=(
            list(quote_integrity.fingerprints) if quote_integrity is not None else []
        ),
        quote_binary_comparisons=(
            list(quote_integrity.binary_comparisons) if quote_integrity is not None else []
        ),
        quote_semantic_comparison=(
            quote_integrity.semantic_comparison if quote_integrity is not None else None
        ),
    )


def _discrepancy_from_issue(
    *,
    issue: ReadinessIssue,
    opportunity_id: str,
    context_id: str | None,
) -> ReconciliationDiscrepancyV1:
    classification, suggested_action = _classify_issue(issue)
    evidence = [
        ReconciliationEvidenceRef(
            source_type="readiness_issue",
            reference=f"{opportunity_id}:{issue.code}",
            summary=issue.message,
        )
    ]
    if context_id:
        evidence.append(
            ReconciliationEvidenceRef(
                source_type="deal_context",
                reference=context_id,
                summary="Deal Desk context used to frame the close review.",
            )
        )
    return ReconciliationDiscrepancyV1(
        discrepancy_id=_discrepancy_id(opportunity_id=opportunity_id, code=issue.code, field_name=issue.field_name),
        code=issue.code,
        severity=issue.severity,
        classification=classification,
        message=issue.message,
        field_name=issue.field_name,
        suggested_action=suggested_action,
        evidence=evidence,
    )


def _classify_issue(
    issue: ReadinessIssue,
) -> tuple[Literal["auto_fixable", "approval_required", "block_close", "informational"], str | None]:
    code = str(issue.code or "").strip().upper()
    field_name = str(issue.field_name or "").strip() or None
    if issue.severity == "blocking":
        if field_name and code.startswith("DD_MISSING_") and not code.startswith("DD_NO_"):
            return "auto_fixable", f"Populate {field_name} before certifying close."
        return "block_close", "Resolve this blocker before final close progression."
    if issue.severity == "warning":
        if field_name and "AMBIGUOUS" in code:
            return "approval_required", f"Confirm mapping for {field_name} with RevOps."
        return "approval_required", "Review this discrepancy before certifying close."
    return "informational", None


def _document_discrepancies(
    *,
    inventory: CloseDocumentInventoryV1,
    opportunity_id: str,
    context_id: str | None,
    block_close_if_missing_documents: bool,
) -> list[ReconciliationDiscrepancyV1]:
    if not inventory.required:
        return []
    if inventory.status not in {"missing", "ambiguous"}:
        return []
    classification: Literal["approval_required", "block_close"]
    severity: Literal["blocking", "warning"]
    if block_close_if_missing_documents:
        classification = "block_close"
        severity = "blocking"
    else:
        classification = "approval_required"
        severity = "warning"
    if inventory.status == "missing":
        code = "DD_MISSING_CLOSE_EVIDENCE"
        message = inventory.summary or "Required close evidence is missing."
        suggested_action = "Attach a signed order form or contract artifact before certifying close."
    else:
        code = "DD_AMBIGUOUS_CLOSE_EVIDENCE"
        message = inventory.summary or "Close evidence is present but could not be classified confidently."
        suggested_action = "Review the attached contract/order-form artifacts with RevOps before certifying close."

    evidence = [
        ReconciliationEvidenceRef(
            source_type="document_inventory",
            reference=f"inventory:{opportunity_id}",
            summary=inventory.summary,
        )
    ]
    if context_id:
        evidence.append(
            ReconciliationEvidenceRef(
                source_type="deal_context",
                reference=context_id,
                summary="Deal Desk context used to frame the close review.",
            )
        )
    for candidate in inventory.candidates[:3]:
        evidence.append(
            ReconciliationEvidenceRef(
                source_type="document_candidate",
                reference=candidate.document_id,
                summary=candidate.summary,
            )
        )
    return [
        ReconciliationDiscrepancyV1(
            discrepancy_id=_discrepancy_id(opportunity_id=opportunity_id, code=code, field_name=None),
            code=code,
            severity=severity,
            classification=classification,
            message=message,
            suggested_action=suggested_action,
            evidence=evidence,
        )
    ]


def _document_extraction_discrepancies(
    *,
    extraction: CloseDocumentExtractionResultV1,
    opportunity_id: str,
    context_id: str | None,
    inventory: CloseDocumentInventoryV1 | None,
) -> list[ReconciliationDiscrepancyV1]:
    if not extraction.issues:
        return []
    evidence_by_document_id = {
        item.document_id: item for item in extraction.evidence if str(item.document_id or "").strip()
    }
    discrepancies: list[ReconciliationDiscrepancyV1] = []
    for issue in extraction.issues:
        evidence: list[ReconciliationEvidenceRef] = []
        if inventory is not None:
            evidence.append(
                ReconciliationEvidenceRef(
                    source_type="document_inventory",
                    reference=f"inventory:{opportunity_id}",
                    summary=inventory.summary,
                )
            )
        if issue.document_id:
            doc_evidence = evidence_by_document_id.get(issue.document_id)
            if doc_evidence is not None:
                evidence.append(
                    ReconciliationEvidenceRef(
                        source_type="document_evidence",
                        reference=doc_evidence.evidence_id,
                        summary=doc_evidence.summary,
                    )
                )
        elif extraction.evidence:
            evidence.append(
                ReconciliationEvidenceRef(
                    source_type="document_evidence",
                    reference=extraction.evidence[0].evidence_id,
                    summary=extraction.evidence[0].summary,
                )
            )
        if context_id:
            evidence.append(
                ReconciliationEvidenceRef(
                    source_type="deal_context",
                    reference=context_id,
                    summary="Deal Desk context used to frame the close review.",
                )
            )
        discrepancies.append(
            ReconciliationDiscrepancyV1(
                discrepancy_id=_discrepancy_id(
                    opportunity_id=opportunity_id,
                    code=issue.code,
                    field_name=issue.field_name,
                ),
                code=issue.code,
                severity=issue.severity,
                classification=issue.classification,
                message=issue.message,
                field_name=issue.field_name,
                suggested_action=issue.suggested_action,
                evidence=evidence,
            )
        )
    return discrepancies


def _quote_integrity_discrepancies(
    *,
    assessment: QuoteIntegrityAssessmentV1,
    opportunity_id: str,
    context_id: str | None,
) -> list[ReconciliationDiscrepancyV1]:
    discrepancies: list[ReconciliationDiscrepancyV1] = []
    for comparison in assessment.binary_comparisons:
        if comparison.outcome != "mismatch":
            continue
        evidence = [
            ReconciliationEvidenceRef(
                source_type="quote_fingerprint",
                reference=str(comparison.baseline_fingerprint_id or "baseline"),
                summary=f"Baseline hash {comparison.baseline_content_hash or 'n/a'}",
            ),
            ReconciliationEvidenceRef(
                source_type="quote_fingerprint",
                reference=str(comparison.observed_fingerprint_id or "observed"),
                summary=f"Observed hash {comparison.observed_content_hash or 'n/a'}",
            ),
        ]
        if context_id:
            evidence.append(
                ReconciliationEvidenceRef(
                    source_type="deal_context",
                    reference=context_id,
                    summary="Deal Desk context used to frame the close review.",
                )
            )
        discrepancies.append(
            ReconciliationDiscrepancyV1(
                discrepancy_id=_discrepancy_id(
                    opportunity_id=opportunity_id,
                    code="DD_QUOTE_BINARY_INTEGRITY_MISMATCH",
                    field_name=None,
                ),
                code="DD_QUOTE_BINARY_INTEGRITY_MISMATCH",
                severity="blocking",
                classification="block_close",
                message=comparison.summary,
                suggested_action="Block close and route Finance plus RevOps review before progressing the opportunity.",
                evidence=evidence,
            )
        )
    semantic = assessment.semantic_comparison
    if semantic is None:
        return discrepancies
    for field_comparison in semantic.field_comparisons:
        if field_comparison.outcome != "mismatch":
            continue
        code = {
            "amount": "DD_QUOTE_AMOUNT_INTEGRITY_MISMATCH",
            "term_months": "DD_QUOTE_TERM_INTEGRITY_MISMATCH",
            "service_start_date": "DD_QUOTE_SERVICE_START_INTEGRITY_MISMATCH",
            "service_end_date": "DD_QUOTE_SERVICE_END_INTEGRITY_MISMATCH",
            "discount_pct": "DD_QUOTE_DISCOUNT_INTEGRITY_MISMATCH",
        }.get(field_comparison.canonical_key, "DD_QUOTE_INTEGRITY_MISMATCH")
        evidence = [
            ReconciliationEvidenceRef(
                source_type="quote_integrity",
                reference=f"{assessment.quote_id or 'quote'}:{field_comparison.canonical_key}",
                summary=field_comparison.delta_summary,
            )
        ]
        if context_id:
            evidence.append(
                ReconciliationEvidenceRef(
                    source_type="deal_context",
                    reference=context_id,
                    summary="Deal Desk context used to frame the close review.",
                )
            )
        discrepancies.append(
            ReconciliationDiscrepancyV1(
                discrepancy_id=_discrepancy_id(
                    opportunity_id=opportunity_id,
                    code=code,
                    field_name=field_comparison.canonical_key,
                ),
                code=code,
                severity="warning",
                classification="approval_required",
                message=field_comparison.delta_summary
                or f"Quote integrity mismatch detected for {field_comparison.canonical_key}.",
                field_name=field_comparison.canonical_key,
                suggested_action="Review the CPQ quote anchor against the signed document before final close progression.",
                evidence=evidence,
            )
        )
    return discrepancies


def _packet_id(
    *,
    opportunity_id: str,
    generated_at: str,
    readiness_mode: str,
    issue_codes: list[str],
) -> str:
    payload = "|".join([opportunity_id, generated_at, readiness_mode, ",".join(sorted(issue_codes))])
    digest = hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16]
    return f"rcn-{digest}"


def _discrepancy_id(*, opportunity_id: str, code: str, field_name: str | None) -> str:
    payload = "|".join([opportunity_id, code, str(field_name or "")])
    digest = hashlib.sha256(payload.encode("utf-8")).hexdigest()[:12]
    return f"disc-{digest}"


def _packet_summary(
    *,
    overall_disposition: str,
    blocking_count: int,
    approval_count: int,
    auto_fixable_count: int,
    informational_count: int,
) -> str:
    if overall_disposition == "block_close":
        return (
            f"Close review surfaced {blocking_count} block-close discrepancy"
            f"{'' if blocking_count == 1 else 'ies'}."
        )
    if overall_disposition == "approval_required":
        actionable = approval_count + auto_fixable_count
        return (
            f"Close review surfaced {actionable} discrepancy"
            f"{'' if actionable == 1 else 'ies'} that need approval or guided fixes."
        )
    return (
        f"Close review queued with {informational_count} informational note"
        f"{'' if informational_count == 1 else 's'} and no blocking discrepancies."
    )


__all__ = [
    "ReconciliationEvidenceRef",
    "ReconciliationDiscrepancyV1",
    "ReconciliationPacketV1",
    "build_reconciliation_packet_from_readiness",
]
