"""Document evidence inventory helpers for Deal Desk close certification."""

from __future__ import annotations

from typing import Any, Literal

from pydantic import BaseModel, ConfigDict, Field, computed_field

from g_gremlin import sfdc as sfdc_helpers

from .config import DealDeskConfig

CloseDocumentProviderHint = Literal["docusign", "pandadoc", "adobe", "unknown"]
CloseDocumentClassification = Literal[
    "signed_order_form",
    "contract",
    "quote_supporting",
    "signature_metadata",
    "unknown",
]
CloseDocumentEvidenceStatus = Literal["not_required", "missing", "ambiguous", "ready"]
CloseDocumentFormatKind = Literal["native_text", "fillable_form", "scanned", "mixed", "unknown"]
CloseDocumentRoutedExtractionPath = Literal[
    "provider_api",
    "salesforce_contentversion_metadata",
    "document_vision_fallback",
    "native_text_parse",
    "fillable_form_parse",
    "unavailable",
]

_PROVIDER_KEYWORDS: dict[CloseDocumentProviderHint, tuple[str, ...]] = {
    "docusign": ("docusign", "envelope"),
    "pandadoc": ("pandadoc",),
    "adobe": ("adobe sign", "adobesign", "adobe"),
    "unknown": (),
}
_ORDER_FORM_KEYWORDS = ("order form", "orderform", "signed order form", "booking form")
_CONTRACT_KEYWORDS = ("msa", "agreement", "contract", "subscription agreement", "sales order")
_QUOTE_KEYWORDS = ("quote", "proposal", "pricing")
_SIGNED_KEYWORDS = ("signed", "executed", "completed", "countersigned")


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


class CloseDocumentCandidateV1(_DocumentModel):
    version: int = 1
    document_id: str
    linked_entity_id: str
    source_object: Literal["ContentDocumentLink", "Attachment"]
    title: str
    file_extension: str | None = None
    content_type: str | None = None
    created_at: str | None = None
    provider_hint: CloseDocumentProviderHint = "unknown"
    classification: CloseDocumentClassification = "unknown"
    confidence: float = Field(default=0.0, ge=0.0, le=1.0)
    observed_version_id: str | None = None
    observed_content_hash: str | None = None
    document_format: CloseDocumentFormatKind = "unknown"
    format_confidence: float = Field(default=0.0, ge=0.0, le=1.0)
    format_notes: list[str] = Field(default_factory=list)
    routed_extraction_path: CloseDocumentRoutedExtractionPath | None = None
    summary: str | None = None

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


class CloseDocumentInventoryV1(_DocumentModel):
    version: int = 1
    opportunity_id: str
    status: CloseDocumentEvidenceStatus = "not_required"
    required: bool = False
    min_attachments: int = 0
    candidate_count: int = 0
    likely_contract_count: int = 0
    candidates: list[CloseDocumentCandidateV1] = Field(default_factory=list)
    notes: list[str] = Field(default_factory=list)
    summary: str = ""

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


def inventory_close_documents(
    *,
    opportunity_id: str,
    org_alias: str | None,
    config: DealDeskConfig,
) -> CloseDocumentInventoryV1:
    if not config.attachment_rules.enabled:
        return CloseDocumentInventoryV1(
            opportunity_id=opportunity_id,
            status="not_required",
            summary="Document evidence inventory is disabled in Deal Desk config.",
        )

    candidates: list[CloseDocumentCandidateV1] = []
    notes: list[str] = []
    for source in config.attachment_rules.sources:
        if source == "ContentDocumentLink":
            try:
                candidates.extend(
                    _content_document_candidates(
                        opportunity_id=opportunity_id,
                        org_alias=org_alias,
                    )
                )
            except Exception:
                notes.append("Unable to query ContentDocumentLink; evidence inventory is incomplete.")
        elif source == "Attachment":
            try:
                candidates.extend(
                    _attachment_candidates(
                        opportunity_id=opportunity_id,
                        org_alias=org_alias,
                    )
                )
            except Exception:
                notes.append("Unable to query Attachment; evidence inventory is incomplete.")

    deduped = _dedupe_candidates(candidates)
    deduped.sort(key=lambda item: (float(item.confidence), str(item.created_at or "")), reverse=True)
    required = bool(config.attachment_rules.min_attachments > 0 or config.attachment_rules.block_close_if_missing)
    likely_contracts = _likely_contract_candidates(deduped)
    status = _inventory_status(
        candidates=deduped,
        likely_contracts=likely_contracts,
        required=required,
        min_attachments=int(config.attachment_rules.min_attachments or 0),
        has_query_gaps=bool(notes),
    )
    return CloseDocumentInventoryV1(
        opportunity_id=opportunity_id,
        status=status,
        required=required,
        min_attachments=int(config.attachment_rules.min_attachments or 0),
        candidate_count=len(deduped),
        likely_contract_count=len(likely_contracts),
        candidates=deduped,
        notes=notes,
        summary=_inventory_summary(
            status=status,
            required=required,
            candidate_count=len(deduped),
            likely_contract_count=len(likely_contracts),
            min_attachments=int(config.attachment_rules.min_attachments or 0),
            has_query_gaps=bool(notes),
        ),
    )


def _content_document_candidates(
    *,
    opportunity_id: str,
    org_alias: str | None,
) -> list[CloseDocumentCandidateV1]:
    escaped_id = sfdc_helpers.escape_soql_value(opportunity_id)
    rows = sfdc_helpers.run_soql(
        (
            "SELECT LinkedEntityId, ContentDocumentId, ContentDocument.Title, "
            "ContentDocument.FileExtension, ContentDocument.FileType, ContentDocument.CreatedDate "
            "FROM ContentDocumentLink "
            f"WHERE LinkedEntityId = '{escaped_id}' "
            "ORDER BY ContentDocument.CreatedDate DESC LIMIT 200"
        ),
        org_alias=org_alias,
    )
    return [
        _content_document_candidate(row)
        for row in rows
        if str(row.get("ContentDocumentId") or "").strip()
    ]


def _attachment_candidates(
    *,
    opportunity_id: str,
    org_alias: str | None,
) -> list[CloseDocumentCandidateV1]:
    escaped_id = sfdc_helpers.escape_soql_value(opportunity_id)
    rows = sfdc_helpers.run_soql(
        (
            "SELECT ParentId, Id, Name, ContentType, CreatedDate "
            "FROM Attachment "
            f"WHERE ParentId = '{escaped_id}' "
            "ORDER BY CreatedDate DESC LIMIT 200"
        ),
        org_alias=org_alias,
    )
    return [
        _attachment_candidate(row)
        for row in rows
        if str(row.get("Id") or "").strip()
    ]


def _content_document_candidate(row: dict[str, Any]) -> CloseDocumentCandidateV1:
    title = str(row.get("ContentDocument.Title") or row.get("Title") or "").strip() or "Untitled document"
    file_extension = _normalized_extension(row.get("ContentDocument.FileExtension"))
    content_type = str(row.get("ContentDocument.FileType") or "").strip() or None
    provider_hint = _provider_hint(title=title, content_type=content_type)
    classification, confidence = _classify_candidate(
        title=title,
        content_type=content_type,
        provider_hint=provider_hint,
    )
    return CloseDocumentCandidateV1(
        document_id=str(row.get("ContentDocumentId") or "").strip(),
        linked_entity_id=str(row.get("LinkedEntityId") or "").strip(),
        source_object="ContentDocumentLink",
        title=title,
        file_extension=file_extension,
        content_type=content_type,
        created_at=str(row.get("ContentDocument.CreatedDate") or "").strip() or None,
        provider_hint=provider_hint,
        classification=classification,
        confidence=confidence,
        summary=_candidate_summary(
            title=title,
            classification=classification,
            provider_hint=provider_hint,
        ),
    )


def _attachment_candidate(row: dict[str, Any]) -> CloseDocumentCandidateV1:
    title = str(row.get("Name") or "").strip() or "Untitled attachment"
    content_type = str(row.get("ContentType") or "").strip() or None
    provider_hint = _provider_hint(title=title, content_type=content_type)
    classification, confidence = _classify_candidate(
        title=title,
        content_type=content_type,
        provider_hint=provider_hint,
    )
    return CloseDocumentCandidateV1(
        document_id=str(row.get("Id") or "").strip(),
        linked_entity_id=str(row.get("ParentId") or "").strip(),
        source_object="Attachment",
        title=title,
        file_extension=_title_extension(title),
        content_type=content_type,
        created_at=str(row.get("CreatedDate") or "").strip() or None,
        provider_hint=provider_hint,
        classification=classification,
        confidence=confidence,
        summary=_candidate_summary(
            title=title,
            classification=classification,
            provider_hint=provider_hint,
        ),
    )


def _provider_hint(*, title: str, content_type: str | None) -> CloseDocumentProviderHint:
    haystack = " ".join([title, str(content_type or "")]).lower()
    for provider, keywords in _PROVIDER_KEYWORDS.items():
        if provider == "unknown":
            continue
        if any(keyword in haystack for keyword in keywords):
            return provider
    return "unknown"


def _classify_candidate(
    *,
    title: str,
    content_type: str | None,
    provider_hint: CloseDocumentProviderHint,
) -> tuple[CloseDocumentClassification, float]:
    haystack = " ".join([title, str(content_type or ""), provider_hint]).lower()
    if any(keyword in haystack for keyword in _ORDER_FORM_KEYWORDS):
        if any(keyword in haystack for keyword in _SIGNED_KEYWORDS):
            return "signed_order_form", 0.96
        return "signed_order_form", 0.86
    if provider_hint != "unknown":
        if any(keyword in haystack for keyword in _SIGNED_KEYWORDS):
            return "signature_metadata", 0.88
        return "signature_metadata", 0.78
    if any(keyword in haystack for keyword in _CONTRACT_KEYWORDS):
        if any(keyword in haystack for keyword in _SIGNED_KEYWORDS):
            return "contract", 0.84
        return "contract", 0.72
    if any(keyword in haystack for keyword in _QUOTE_KEYWORDS):
        return "quote_supporting", 0.56
    return "unknown", 0.22


def _likely_contract_candidates(
    candidates: list[CloseDocumentCandidateV1],
) -> list[CloseDocumentCandidateV1]:
    return [
        candidate
        for candidate in candidates
        if candidate.classification in {"signed_order_form", "contract", "signature_metadata"}
        and float(candidate.confidence) >= 0.7
    ]


def _inventory_status(
    *,
    candidates: list[CloseDocumentCandidateV1],
    likely_contracts: list[CloseDocumentCandidateV1],
    required: bool,
    min_attachments: int,
    has_query_gaps: bool,
) -> CloseDocumentEvidenceStatus:
    if not required:
        return "not_required"
    if len(candidates) < max(1, min_attachments):
        return "missing"
    if has_query_gaps and not likely_contracts:
        return "ambiguous"
    if not likely_contracts:
        return "ambiguous"
    if len(likely_contracts) > 1:
        strongest = likely_contracts[0]
        second = likely_contracts[1]
        if abs(float(strongest.confidence) - float(second.confidence)) < 0.12:
            return "ambiguous"
    return "ready"


def _inventory_summary(
    *,
    status: CloseDocumentEvidenceStatus,
    required: bool,
    candidate_count: int,
    likely_contract_count: int,
    min_attachments: int,
    has_query_gaps: bool,
) -> str:
    if status == "not_required":
        if required:
            return "Document evidence is required, but inventory status could not be determined."
        return (
            f"Document inventory found {candidate_count} attachment"
            f"{'' if candidate_count == 1 else 's'}; evidence is not required by current config."
        )
    if status == "missing":
        return (
            f"Document evidence is required, but only {candidate_count} attachment"
            f"{'' if candidate_count == 1 else 's'} were found"
            f" against a minimum of {max(1, min_attachments)}."
        )
    if status == "ambiguous":
        base = (
            f"Document evidence inventory found {candidate_count} attachment"
            f"{'' if candidate_count == 1 else 's'} but only {likely_contract_count} likely close-evidence candidate"
            f"{'' if likely_contract_count == 1 else 's'}."
        )
        if has_query_gaps:
            return f"{base} One or more document sources could not be queried."
        return base
    return (
        f"Document evidence inventory found {candidate_count} attachment"
        f"{'' if candidate_count == 1 else 's'} with {likely_contract_count} likely close-evidence candidate"
        f"{'' if likely_contract_count == 1 else 's'}."
    )


def _candidate_summary(
    *,
    title: str,
    classification: CloseDocumentClassification,
    provider_hint: CloseDocumentProviderHint,
) -> str:
    if provider_hint != "unknown":
        return f"{title} ({provider_hint}, {classification.replace('_', ' ')})"
    return f"{title} ({classification.replace('_', ' ')})"


def _dedupe_candidates(candidates: list[CloseDocumentCandidateV1]) -> list[CloseDocumentCandidateV1]:
    deduped: list[CloseDocumentCandidateV1] = []
    seen: set[tuple[str, str]] = set()
    for candidate in candidates:
        key = (candidate.source_object, candidate.document_id)
        if key in seen:
            continue
        seen.add(key)
        deduped.append(candidate)
    return deduped


def _normalized_extension(value: Any) -> str | None:
    text = str(value or "").strip().lower()
    if not text:
        return None
    return text.lstrip(".") or None


def _title_extension(title: str) -> str | None:
    normalized = str(title or "").strip()
    if "." not in normalized:
        return None
    return _normalized_extension(normalized.rsplit(".", 1)[-1])


__all__ = [
    "CloseDocumentProviderHint",
    "CloseDocumentClassification",
    "CloseDocumentEvidenceStatus",
    "CloseDocumentCandidateV1",
    "CloseDocumentInventoryV1",
    "inventory_close_documents",
]
