from __future__ import annotations

import hashlib
from datetime import datetime
from pathlib import Path
from typing import Any, Literal

from pydantic import BaseModel, ConfigDict, Field, computed_field

from .config import DealDeskConfig
from .context import ArtifactRef, DealContextV1, QuoteRef
from .documents import (
    CloseDocumentCandidateV1,
    CloseDocumentFormatKind,
    CloseDocumentRoutedExtractionPath,
)
from .extraction import (
    CloseDocumentExtractionMethod,
    CloseDocumentExtractionResultV1,
)

QuoteDocumentFingerprintSource = Literal["cpq_generated", "attachment"]
QuoteFingerprintComparisonOutcome = Literal[
    "matched",
    "mismatch",
    "missing_baseline",
    "missing_observation",
]
QuoteIntegrityStatus = Literal["not_available", "pending_baseline", "clear", "review_required", "blocked"]
QuoteIntegrityFieldKey = Literal["amount", "term_months", "service_start_date", "service_end_date", "discount_pct"]
QuoteIntegrityFieldOutcome = Literal[
    "matched",
    "mismatch",
    "missing_quote_value",
    "missing_document_value",
    "not_applicable",
]
AutonomyTier = Literal["green", "yellow", "red"]

_SHARED_COMMERCIAL_FIELD_KEYS: tuple[QuoteIntegrityFieldKey, ...] = (
    "amount",
    "term_months",
    "service_start_date",
    "service_end_date",
    "discount_pct",
)
_DATE_FORMATS = (
    "%Y-%m-%d",
    "%Y/%m/%d",
    "%m/%d/%Y",
    "%m-%d-%Y",
    "%b %d, %Y",
    "%B %d, %Y",
    "%Y-%m-%dT%H:%M:%S%z",
    "%Y-%m-%dT%H:%M:%S.%f%z",
)
_METHOD_PRIORITY: dict[str, int] = {
    "provider_api": 3,
    "document_vision": 2,
    "salesforce_contentversion_metadata": 1,
}


class _IntegrityModel(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]) -> "_IntegrityModel":
        return cls.model_validate(data)


class BlessedQuoteTermsV1(_IntegrityModel):
    version: int = 1
    quote_id: str
    anchor_source: str = "deal_context"
    amount: float | None = None
    term_months: int | None = None
    service_start_date: str | None = None
    service_end_date: str | None = None
    discount_pct: float | None = None

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


class QuoteDocumentFingerprintV1(_IntegrityModel):
    version: int = 1
    fingerprint_id: str
    opportunity_id: str
    crm_org_id: str
    org_alias: str | None = None
    quote_id: str
    document_id: str
    version_id: str | None = None
    title: str | None = None
    provider_hint: str | None = None
    content_hash: str
    captured_at: str
    source: QuoteDocumentFingerprintSource
    source_path: str | None = None
    document_format: CloseDocumentFormatKind = "unknown"
    routed_extraction_path: CloseDocumentRoutedExtractionPath | None = None

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


class QuoteFingerprintComparisonV1(_IntegrityModel):
    version: int = 1
    quote_id: str
    outcome: QuoteFingerprintComparisonOutcome
    severity: Literal["blocking", "warning", "info"] = "info"
    autonomy_tier: AutonomyTier = "yellow"
    summary: str = ""
    baseline_fingerprint_id: str | None = None
    baseline_document_id: str | None = None
    baseline_content_hash: str | None = None
    observed_fingerprint_id: str | None = None
    observed_document_id: str | None = None
    observed_content_hash: str | None = None
    observed_title: str | None = None

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


class QuoteIntegrityFieldComparisonV1(_IntegrityModel):
    version: int = 1
    canonical_key: QuoteIntegrityFieldKey
    quote_value: Any = None
    document_value: Any = None
    outcome: QuoteIntegrityFieldOutcome = "not_applicable"
    severity: Literal["blocking", "warning", "info"] = "info"
    autonomy_tier: AutonomyTier = "yellow"
    document_id: str | None = None
    extraction_method: CloseDocumentExtractionMethod | None = None
    routed_extraction_path: CloseDocumentRoutedExtractionPath | None = None
    field_confidence: float | None = Field(default=None, ge=0.0, le=1.0)
    delta_summary: str | None = None

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


class QuoteIntegrityComparisonV1(_IntegrityModel):
    version: int = 1
    quote_id: str
    status: Literal["not_available", "partial", "clear", "review_required"] = "not_available"
    summary: str = ""
    blessed_terms: BlessedQuoteTermsV1 | None = None
    field_comparisons: list[QuoteIntegrityFieldComparisonV1] = Field(default_factory=list)
    match_count: int = 0
    mismatch_count: int = 0
    missing_quote_value_count: int = 0
    missing_document_value_count: int = 0

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


class QuoteIntegrityAssessmentV1(_IntegrityModel):
    version: int = 1
    quote_id: str | None = None
    status: QuoteIntegrityStatus = "not_available"
    summary: str = ""
    blessed_terms: BlessedQuoteTermsV1 | None = None
    fingerprints: list[QuoteDocumentFingerprintV1] = Field(default_factory=list)
    binary_comparisons: list[QuoteFingerprintComparisonV1] = Field(default_factory=list)
    semantic_comparison: QuoteIntegrityComparisonV1 | None = None
    fingerprint_count: int = 0
    binary_mismatch_count: int = 0
    semantic_mismatch_count: int = 0
    discrepancy_count: int = 0

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


def capture_quote_document_fingerprints(
    *,
    opportunity_id: str,
    crm_org_id: str,
    org_alias: str | None,
    context: DealContextV1 | None,
    inventory_candidates: list[CloseDocumentCandidateV1] | None,
    captured_at: str,
) -> list[QuoteDocumentFingerprintV1]:
    quote_ref = _latest_quote_ref(context)
    quote_id = _quote_id(context)
    fingerprints: list[QuoteDocumentFingerprintV1] = []

    baseline = _cpq_generated_fingerprint(
        opportunity_id=opportunity_id,
        crm_org_id=crm_org_id,
        org_alias=org_alias,
        quote_id=quote_id,
        quote_ref=quote_ref,
        artifact_refs=(context.artifact_refs if context is not None else []),
        captured_at=captured_at,
    )
    if baseline is not None:
        fingerprints.append(baseline)

    for candidate in list(inventory_candidates or []):
        content_hash = str(candidate.observed_content_hash or "").strip()
        if not content_hash:
            continue
        fingerprints.append(
            QuoteDocumentFingerprintV1(
                fingerprint_id=_fingerprint_id(
                    quote_id=quote_id,
                    document_id=candidate.document_id,
                    source="attachment",
                    captured_at=captured_at,
                    content_hash=content_hash,
                ),
                opportunity_id=opportunity_id,
                crm_org_id=crm_org_id,
                org_alias=org_alias,
                quote_id=quote_id,
                document_id=candidate.document_id,
                version_id=candidate.observed_version_id,
                title=candidate.title,
                provider_hint=candidate.provider_hint,
                content_hash=content_hash,
                captured_at=captured_at,
                source="attachment",
                document_format=candidate.document_format,
                routed_extraction_path=candidate.routed_extraction_path,
            )
        )

    deduped: list[QuoteDocumentFingerprintV1] = []
    seen: set[tuple[str, str, str, str]] = set()
    for fingerprint in fingerprints:
        key = (
            str(fingerprint.quote_id or ""),
            str(fingerprint.document_id or ""),
            str(fingerprint.source or ""),
            str(fingerprint.content_hash or ""),
        )
        if key in seen:
            continue
        seen.add(key)
        deduped.append(fingerprint)
    return deduped


def build_quote_integrity_assessment(
    *,
    context: DealContextV1 | None,
    config: DealDeskConfig,
    document_extraction: CloseDocumentExtractionResultV1 | None,
    current_fingerprints: list[QuoteDocumentFingerprintV1] | None,
    persisted_fingerprints: list[QuoteDocumentFingerprintV1] | None = None,
) -> QuoteIntegrityAssessmentV1:
    quote_id = _quote_id(context)
    blessed_terms = _build_blessed_quote_terms(context=context, config=config, quote_id=quote_id)
    current_fingerprint_list = list(current_fingerprints or [])
    persisted_fingerprint_list = list(persisted_fingerprints or current_fingerprint_list)
    semantic = _build_semantic_comparison(
        quote_id=quote_id,
        blessed_terms=blessed_terms,
        document_extraction=document_extraction,
    )
    binary = _build_binary_comparisons(
        quote_id=quote_id,
        document_extraction=document_extraction,
        current_fingerprints=current_fingerprint_list,
        persisted_fingerprints=persisted_fingerprint_list,
    )
    baseline = _latest_fingerprint(persisted_fingerprint_list, source="cpq_generated")
    assessment_fingerprints = _assessment_fingerprints(
        current_fingerprints=current_fingerprint_list,
        baseline=baseline,
    )
    binary_mismatch_count = sum(1 for item in binary if item.outcome == "mismatch")
    semantic_mismatch_count = (
        int(semantic.mismatch_count or 0)
        if semantic is not None and semantic.status in {"partial", "clear", "review_required"}
        else 0
    )
    status = _assessment_status(
        binary=binary,
        semantic=semantic,
        fingerprints=assessment_fingerprints,
        blessed_terms=blessed_terms,
    )
    summary = _assessment_summary(
        status=status,
        binary=binary,
        semantic=semantic,
        fingerprint_count=len(assessment_fingerprints),
        has_quote_anchor=blessed_terms is not None,
    )
    return QuoteIntegrityAssessmentV1(
        quote_id=None if quote_id == "unknown" else quote_id,
        status=status,
        summary=summary,
        blessed_terms=blessed_terms,
        fingerprints=assessment_fingerprints,
        binary_comparisons=binary,
        semantic_comparison=semantic,
        fingerprint_count=len(assessment_fingerprints),
        binary_mismatch_count=binary_mismatch_count,
        semantic_mismatch_count=semantic_mismatch_count,
        discrepancy_count=binary_mismatch_count + semantic_mismatch_count,
    )


def _build_semantic_comparison(
    *,
    quote_id: str,
    blessed_terms: BlessedQuoteTermsV1 | None,
    document_extraction: CloseDocumentExtractionResultV1 | None,
) -> QuoteIntegrityComparisonV1 | None:
    comparisons_by_key = {
        comparison.canonical_key: comparison
        for comparison in list(document_extraction.comparisons if document_extraction is not None else [])
    }
    if blessed_terms is None and not comparisons_by_key:
        return None

    field_comparisons: list[QuoteIntegrityFieldComparisonV1] = []
    for canonical_key in _SHARED_COMMERCIAL_FIELD_KEYS:
        if canonical_key == "discount_pct":
            continue
        document_comparison = comparisons_by_key.get(canonical_key)
        quote_value = getattr(blessed_terms, canonical_key, None) if blessed_terms is not None else None
        document_value = document_comparison.extracted_value if document_comparison is not None else None
        outcome = _field_outcome(quote_value=quote_value, document_value=document_value)
        if outcome == "not_applicable":
            continue
        severity, autonomy_tier, delta_summary = _field_signal(
            canonical_key=canonical_key,
            quote_value=quote_value,
            document_value=document_value,
            outcome=outcome,
        )
        field_comparisons.append(
            QuoteIntegrityFieldComparisonV1(
                canonical_key=canonical_key,
                quote_value=quote_value,
                document_value=document_value,
                outcome=outcome,
                severity=severity,
                autonomy_tier=autonomy_tier,
                document_id=document_comparison.document_id if document_comparison is not None else None,
                extraction_method=document_comparison.extraction_method if document_comparison is not None else None,
                routed_extraction_path=document_comparison.routed_extraction_path if document_comparison is not None else None,
                field_confidence=document_comparison.field_confidence if document_comparison is not None else None,
                delta_summary=delta_summary,
            )
        )

    if not field_comparisons:
        return QuoteIntegrityComparisonV1(
            quote_id=quote_id,
            status="not_available",
            summary="No shared commercial quote terms were available for semantic integrity comparison.",
            blessed_terms=blessed_terms,
        )

    match_count = sum(1 for item in field_comparisons if item.outcome == "matched")
    mismatch_count = sum(1 for item in field_comparisons if item.outcome == "mismatch")
    missing_quote_value_count = sum(1 for item in field_comparisons if item.outcome == "missing_quote_value")
    missing_document_value_count = sum(1 for item in field_comparisons if item.outcome == "missing_document_value")
    if mismatch_count:
        status = "review_required"
    elif missing_quote_value_count or missing_document_value_count:
        status = "partial"
    else:
        status = "clear"
    return QuoteIntegrityComparisonV1(
        quote_id=quote_id,
        status=status,
        summary=_semantic_summary(
            match_count=match_count,
            mismatch_count=mismatch_count,
            missing_quote_value_count=missing_quote_value_count,
            missing_document_value_count=missing_document_value_count,
        ),
        blessed_terms=blessed_terms,
        field_comparisons=field_comparisons,
        match_count=match_count,
        mismatch_count=mismatch_count,
        missing_quote_value_count=missing_quote_value_count,
        missing_document_value_count=missing_document_value_count,
    )


def _build_binary_comparisons(
    *,
    quote_id: str,
    document_extraction: CloseDocumentExtractionResultV1 | None,
    current_fingerprints: list[QuoteDocumentFingerprintV1],
    persisted_fingerprints: list[QuoteDocumentFingerprintV1],
) -> list[QuoteFingerprintComparisonV1]:
    baseline = _latest_fingerprint(persisted_fingerprints, source="cpq_generated")
    observed = _select_primary_observed_fingerprint(
        document_extraction=document_extraction,
        fingerprints=current_fingerprints,
    )
    if baseline is None and observed is None:
        return []
    if baseline is None:
        return [
            QuoteFingerprintComparisonV1(
                quote_id=quote_id,
                outcome="missing_baseline",
                severity="info",
                autonomy_tier="yellow",
                summary="Observed close-evidence content hash was captured, but no CPQ-generated quote fingerprint is available yet.",
                observed_fingerprint_id=observed.fingerprint_id if observed is not None else None,
                observed_document_id=observed.document_id if observed is not None else None,
                observed_content_hash=observed.content_hash if observed is not None else None,
                observed_title=observed.title if observed is not None else None,
            )
        ]
    if observed is None:
        return [
            QuoteFingerprintComparisonV1(
                quote_id=quote_id,
                outcome="missing_observation",
                severity="info",
                autonomy_tier="yellow",
                summary="A CPQ-generated quote fingerprint exists, but no observed signed close-evidence hash is available yet.",
                baseline_fingerprint_id=baseline.fingerprint_id,
                baseline_document_id=baseline.document_id,
                baseline_content_hash=baseline.content_hash,
            )
        ]
    if baseline.content_hash == observed.content_hash:
        return [
            QuoteFingerprintComparisonV1(
                quote_id=quote_id,
                outcome="matched",
                severity="info",
                autonomy_tier="green",
                summary="Observed close-evidence hash matches the stored CPQ-generated quote fingerprint.",
                baseline_fingerprint_id=baseline.fingerprint_id,
                baseline_document_id=baseline.document_id,
                baseline_content_hash=baseline.content_hash,
                observed_fingerprint_id=observed.fingerprint_id,
                observed_document_id=observed.document_id,
                observed_content_hash=observed.content_hash,
                observed_title=observed.title,
            )
        ]
    return [
        QuoteFingerprintComparisonV1(
            quote_id=quote_id,
            outcome="mismatch",
            severity="blocking",
            autonomy_tier="red",
            summary="Observed close-evidence hash does not match the stored CPQ-generated quote fingerprint.",
            baseline_fingerprint_id=baseline.fingerprint_id,
            baseline_document_id=baseline.document_id,
            baseline_content_hash=baseline.content_hash,
            observed_fingerprint_id=observed.fingerprint_id,
            observed_document_id=observed.document_id,
            observed_content_hash=observed.content_hash,
            observed_title=observed.title,
        )
    ]


def _build_blessed_quote_terms(
    *,
    context: DealContextV1 | None,
    config: DealDeskConfig,
    quote_id: str,
) -> BlessedQuoteTermsV1 | None:
    if context is None:
        return None
    normalized = dict(context.normalized_quote_request or {})
    original = dict(context.original_request or {})
    amount = _coerce_float(
        normalized.get("amount"),
        original.get(str(config.amount_rules.amount_field or "").strip()),
        original.get("Amount"),
        original.get("GrandTotal"),
    )
    term_months = _coerce_int(
        normalized.get("term_months"),
        original.get(str(config.amount_rules.term_months_field or "").strip()),
        original.get("ContractTerm"),
        original.get("SBQQ__SubscriptionTerm__c"),
    )
    service_start_date = _normalize_date(
        original.get(str(config.date_rules.contract_start_field or "").strip()),
        original.get("ServiceDate"),
        original.get("StartDate"),
        original.get("SBQQ__StartDate__c"),
    )
    service_end_date = _normalize_date(
        original.get(str(config.date_rules.contract_end_field or "").strip()),
        original.get("EndDate"),
        original.get("SBQQ__EndDate__c"),
    )
    discount_pct = _coerce_float(
        normalized.get("discount_pct"),
        original.get("Discount__c"),
        original.get("DiscountPercent"),
        original.get("SBQQ__CustomerDiscount__c"),
    )
    if all(value is None for value in (amount, term_months, service_start_date, service_end_date, discount_pct)):
        return None
    return BlessedQuoteTermsV1(
        quote_id=quote_id,
        amount=amount,
        term_months=term_months,
        service_start_date=service_start_date,
        service_end_date=service_end_date,
        discount_pct=discount_pct,
    )


def _select_primary_observed_fingerprint(
    *,
    document_extraction: CloseDocumentExtractionResultV1 | None,
    fingerprints: list[QuoteDocumentFingerprintV1],
) -> QuoteDocumentFingerprintV1 | None:
    attachment_fingerprints = [item for item in fingerprints if item.source == "attachment"]
    if not attachment_fingerprints:
        return None
    evidence_by_document_id = {
        item.document_id: item
        for item in list(document_extraction.evidence if document_extraction is not None else [])
    }
    ranked = sorted(
        attachment_fingerprints,
        key=lambda item: _observed_fingerprint_rank(item=item, evidence_by_document_id=evidence_by_document_id),
        reverse=True,
    )
    return ranked[0] if ranked else None


def _observed_fingerprint_rank(
    *,
    item: QuoteDocumentFingerprintV1,
    evidence_by_document_id: dict[str, Any],
) -> tuple[int, float, int, str]:
    evidence = evidence_by_document_id.get(item.document_id)
    method_rank = _METHOD_PRIORITY.get(str(getattr(evidence, "extraction_method", "") or ""), 0)
    confidence = float(getattr(evidence, "confidence", 0.0) or 0.0)
    likely_quote = 1 if str(item.routed_extraction_path or "") in {"provider_api", "salesforce_contentversion_metadata"} else 0
    return (method_rank, confidence, likely_quote, str(item.captured_at or ""))


def _assessment_status(
    *,
    binary: list[QuoteFingerprintComparisonV1],
    semantic: QuoteIntegrityComparisonV1 | None,
    fingerprints: list[QuoteDocumentFingerprintV1],
    blessed_terms: BlessedQuoteTermsV1 | None,
) -> QuoteIntegrityStatus:
    if any(item.outcome == "mismatch" for item in binary):
        return "blocked"
    if semantic is not None and semantic.status == "review_required":
        return "review_required"
    if not fingerprints and blessed_terms is None:
        return "not_available"
    if any(item.outcome in {"missing_baseline", "missing_observation"} for item in binary):
        return "pending_baseline"
    if semantic is not None and semantic.status == "partial":
        return "pending_baseline"
    if binary or (semantic is not None and semantic.status == "clear"):
        return "clear"
    return "pending_baseline"


def _assessment_fingerprints(
    *,
    current_fingerprints: list[QuoteDocumentFingerprintV1],
    baseline: QuoteDocumentFingerprintV1 | None,
) -> list[QuoteDocumentFingerprintV1]:
    selected: list[QuoteDocumentFingerprintV1] = list(current_fingerprints)
    if baseline is not None and not any(item.fingerprint_id == baseline.fingerprint_id for item in selected):
        selected.append(baseline)
    return selected


def _assessment_summary(
    *,
    status: QuoteIntegrityStatus,
    binary: list[QuoteFingerprintComparisonV1],
    semantic: QuoteIntegrityComparisonV1 | None,
    fingerprint_count: int,
    has_quote_anchor: bool,
) -> str:
    if status == "not_available":
        return "Quote integrity verification was not available because neither a quote anchor nor observed fingerprints were captured."
    if status == "blocked":
        return "Binary quote integrity mismatch detected; block close and route Finance plus RevOps review."
    if status == "review_required":
        mismatch_count = int(semantic.mismatch_count or 0) if semantic is not None else 0
        return f"Semantic quote integrity review found {mismatch_count} commercial field mismatch(es) against the quote anchor."
    if status == "clear":
        return (
            "Quote integrity checks passed against the currently captured baseline."
            if binary
            else "Semantic quote integrity checks passed against the quote anchor."
        )
    if fingerprint_count and not any(item.outcome == "matched" for item in binary):
        if has_quote_anchor:
            return "Observed document fingerprints were captured and semantic quote integrity ran, but no blessed binary baseline was available yet."
        return "Observed document fingerprints were captured, but no blessed binary baseline was available yet."
    return "Quote integrity verification is partially available and awaiting stronger baseline coverage."


def _semantic_summary(
    *,
    match_count: int,
    mismatch_count: int,
    missing_quote_value_count: int,
    missing_document_value_count: int,
) -> str:
    parts = [f"{match_count} matched field(s)"]
    if mismatch_count:
        parts.append(f"{mismatch_count} mismatch(es)")
    if missing_quote_value_count:
        parts.append(f"{missing_quote_value_count} missing quote anchor value(s)")
    if missing_document_value_count:
        parts.append(f"{missing_document_value_count} missing document value(s)")
    return "Semantic quote integrity compared " + ", ".join(parts) + "."


def _field_outcome(*, quote_value: Any, document_value: Any) -> QuoteIntegrityFieldOutcome:
    if quote_value in (None, "") and document_value in (None, ""):
        return "not_applicable"
    if quote_value in (None, ""):
        return "missing_quote_value"
    if document_value in (None, ""):
        return "missing_document_value"
    normalized_quote = _normalize_value(quote_value)
    normalized_document = _normalize_value(document_value)
    if normalized_quote == normalized_document:
        return "matched"
    return "mismatch"


def _field_signal(
    *,
    canonical_key: QuoteIntegrityFieldKey,
    quote_value: Any,
    document_value: Any,
    outcome: QuoteIntegrityFieldOutcome,
) -> tuple[Literal["blocking", "warning", "info"], AutonomyTier, str]:
    label = canonical_key.replace("_", " ")
    if outcome == "matched":
        return "info", "green", f"Quote and signed document agree on {label}."
    if outcome == "missing_quote_value":
        return "info", "yellow", f"Quote anchor is missing {label}; semantic integrity could not verify this field."
    if outcome == "missing_document_value":
        return "info", "yellow", f"Signed document extraction did not produce {label}; semantic integrity remains partial."
    return (
        "warning",
        "red",
        f"Quote {label} {quote_value!r} differs from the signed document value {document_value!r}.",
    )


def _cpq_generated_fingerprint(
    *,
    opportunity_id: str,
    crm_org_id: str,
    org_alias: str | None,
    quote_id: str,
    quote_ref: QuoteRef | None,
    artifact_refs: list[ArtifactRef],
    captured_at: str,
) -> QuoteDocumentFingerprintV1 | None:
    source_path = _baseline_quote_path(quote_ref=quote_ref, artifact_refs=artifact_refs)
    if not source_path:
        return None
    path = Path(source_path).expanduser()
    if not path.exists() or not path.is_file():
        return None
    try:
        payload = path.read_bytes()
    except Exception:
        return None
    if not payload:
        return None
    content_hash = f"sha256:{hashlib.sha256(payload).hexdigest()}"
    return QuoteDocumentFingerprintV1(
        fingerprint_id=_fingerprint_id(
            quote_id=quote_id,
            document_id=f"quote:{quote_id}",
            source="cpq_generated",
            captured_at=captured_at,
            content_hash=content_hash,
        ),
        opportunity_id=opportunity_id,
        crm_org_id=crm_org_id,
        org_alias=org_alias,
        quote_id=quote_id,
        document_id=f"quote:{quote_id}",
        title=path.name,
        content_hash=content_hash,
        captured_at=captured_at,
        source="cpq_generated",
        source_path=str(path),
        document_format="unknown",
    )


def _baseline_quote_path(*, quote_ref: QuoteRef | None, artifact_refs: list[ArtifactRef]) -> str | None:
    if quote_ref is not None and str(quote_ref.pdf_artifact_path or "").strip():
        return str(quote_ref.pdf_artifact_path).strip()
    quote_pdf_artifacts = [item for item in artifact_refs if item.type == "quote_pdf" and str(item.path or "").strip()]
    if not quote_pdf_artifacts:
        return None
    quote_pdf_artifacts.sort(key=lambda item: str(item.created_at or ""))
    return quote_pdf_artifacts[-1].path


def _latest_quote_ref(context: DealContextV1 | None) -> QuoteRef | None:
    refs = list(context.quote_refs) if context is not None else []
    if not refs:
        return None
    refs.sort(key=lambda item: str(item.created_at or ""))
    return refs[-1]


def _quote_id(context: DealContextV1 | None) -> str:
    quote_ref = _latest_quote_ref(context)
    if quote_ref is None or not str(quote_ref.quote_id or "").strip():
        return "unknown"
    return str(quote_ref.quote_id).strip()


def _latest_fingerprint(
    fingerprints: list[QuoteDocumentFingerprintV1],
    *,
    source: QuoteDocumentFingerprintSource,
) -> QuoteDocumentFingerprintV1 | None:
    matches = [item for item in fingerprints if item.source == source]
    if not matches:
        return None
    matches.sort(key=lambda item: str(item.captured_at or ""))
    return matches[-1]


def _fingerprint_id(
    *,
    quote_id: str,
    document_id: str,
    source: QuoteDocumentFingerprintSource,
    captured_at: str,
    content_hash: str,
) -> str:
    timestamp = "".join(ch for ch in str(captured_at or "") if ch.isalnum())[-14:] or "fingerprint"
    suffix = str(content_hash or "").split(":")[-1][:10] or "hash"
    doc_key = "".join(ch for ch in str(document_id or "") if ch.isalnum())[-12:] or "document"
    quote_key = "".join(ch for ch in str(quote_id or "") if ch.isalnum())[-12:] or "quote"
    return f"qfp-{quote_key}-{doc_key}-{source}-{timestamp}-{suffix}"


def _coerce_float(*values: Any) -> float | None:
    for value in values:
        if value in (None, ""):
            continue
        try:
            return float(str(value).replace(",", "").replace("%", "").strip())
        except Exception:
            continue
    return None


def _coerce_int(*values: Any) -> int | None:
    for value in values:
        if value in (None, ""):
            continue
        try:
            return int(float(str(value).replace(",", "").strip()))
        except Exception:
            continue
    return None


def _normalize_date(*values: Any) -> str | None:
    for value in values:
        if value in (None, ""):
            continue
        text = str(value).strip()
        if not text:
            continue
        try:
            parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
            return parsed.date().isoformat()
        except Exception:
            pass
        for fmt in _DATE_FORMATS:
            try:
                return datetime.strptime(text, fmt).date().isoformat()
            except Exception:
                continue
    return None


def _normalize_value(value: Any) -> Any:
    if value in (None, ""):
        return None
    normalized_date = _normalize_date(value)
    if normalized_date is not None:
        return normalized_date
    try:
        numeric = float(str(value).replace(",", "").replace("%", "").strip())
    except Exception:
        return str(value).strip()
    rounded = round(numeric, 4)
    if rounded.is_integer():
        return int(rounded)
    return rounded


__all__ = [
    "AutonomyTier",
    "BlessedQuoteTermsV1",
    "QuoteDocumentFingerprintSource",
    "QuoteDocumentFingerprintV1",
    "QuoteFingerprintComparisonOutcome",
    "QuoteFingerprintComparisonV1",
    "QuoteIntegrityAssessmentV1",
    "QuoteIntegrityComparisonV1",
    "QuoteIntegrityFieldComparisonV1",
    "QuoteIntegrityFieldKey",
    "QuoteIntegrityFieldOutcome",
    "QuoteIntegrityStatus",
    "build_quote_integrity_assessment",
    "capture_quote_document_fingerprints",
]
