"""Quote readiness evaluation for Deal Desk."""

from __future__ import annotations

from dataclasses import dataclass
from statistics import mean
from typing import Any, Iterable, Literal

from pydantic import BaseModel, ConfigDict, Field

from .context import DealAssumption, DealContextV1
from .forensics import (
    extract_amount,
    extract_discount_pct,
    extract_payment_terms,
    extract_product_family,
    extract_segment,
    extract_term_months,
    infer_deal_motion,
    normalize_deal_record,
    resolve_size_band,
)
from .profiles import (
    ApprovalPattern,
    DealArchetype,
    DealPatternProfileV1,
    PatternUpdateEventV1,
    QuoteEnvironmentProfileV1,
)
from .similarity import SimilarityInput, rank_similar_deals


@dataclass(frozen=True)
class _LegalEntityFallback:
    field_name: str
    value: str
    confidence: float
    severity: Literal["warning", "info"]
    message: str
    rationale: str


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

    code: str
    severity: Literal["blocking", "warning", "info"]
    message: str
    field_name: str | None = None


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

    record_id: str
    similarity_score: float = Field(ge=0.0, le=1.0)
    matched_dimensions: list[str] = Field(default_factory=list)
    mismatched_dimensions: list[str] = Field(default_factory=list)
    reasoning: str = ""


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

    environment_confidence: float = Field(ge=0.0, le=1.0)
    pattern_confidence: float = Field(ge=0.0, le=1.0)
    motion_confidence: float = Field(ge=0.0, le=1.0)
    archetype_confidence: float = Field(ge=0.0, le=1.0)
    similarity_evidence_confidence: float = Field(ge=0.0, le=1.0)
    field_mapping_confidence: float = Field(ge=0.0, le=1.0)
    receipt_success_confidence: float = Field(ge=0.0, le=1.0)
    evidence_count: int = 0
    learning_state_adjustment: float = 0.0
    issue_penalty: float = 0.0
    final_confidence: float = Field(ge=0.0, le=1.0)


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

    ready: bool
    readiness_mode: Literal["ready", "review_required", "unsupported", "insufficient_evidence"] = "review_required"
    overall_confidence: float = Field(ge=0.0, le=1.0)
    inferred_motion_id: str | None = None
    matched_archetype_id: str | None = None
    learning_state: str | None = None
    recommended_approval_roles: list[str] = Field(default_factory=list)
    issues: list[ReadinessIssue] = Field(default_factory=list)
    similar_deals: list[SimilarDealEvidence] = Field(default_factory=list)
    similarity_reasoning: list[str] = Field(default_factory=list)
    confidence_breakdown: ConfidenceBreakdown | None = None
    context_id: str | None = None

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


def evaluate_quote_readiness(
    *,
    opportunity_id: str,
    crm_org_id: str,
    generated_at: str,
    environment_profile: QuoteEnvironmentProfileV1,
    pattern_profile: DealPatternProfileV1,
    quote_request: dict[str, Any] | None = None,
    opportunity_snapshot: dict[str, Any] | None = None,
    existing_context: DealContextV1 | None = None,
    historical_records: list[dict[str, Any]] | None = None,
    update_events: list[PatternUpdateEventV1] | None = None,
) -> tuple[QuoteReadinessResult, DealContextV1]:
    merged = dict(opportunity_snapshot or {})
    merged.update(dict(quote_request or {}))
    best_mappings = _best_mapping_by_semantic(environment_profile.field_mappings)

    motion_id, motion_confidence, _motion_reason = infer_deal_motion(merged)
    amount = _value_for_semantic("amount", merged, best_mappings)
    if amount in (None, ""):
        amount = extract_amount(merged)
    if amount not in (None, ""):
        try:
            amount = float(amount)
        except Exception:
            amount = extract_amount(merged)

    segment = _string_or_none(_value_for_semantic("segment", merged, best_mappings)) or extract_segment(merged)
    product_family = _string_or_none(_value_for_semantic("product_family", merged, best_mappings)) or extract_product_family(merged)
    discount_pct = _coerce_float(_value_for_semantic("discount_pct", merged, best_mappings))
    if discount_pct is None:
        discount_pct = extract_discount_pct(merged)
    term_months = _coerce_int(_value_for_semantic("term_months", merged, best_mappings))
    if term_months is None:
        term_months = extract_term_months(merged)
    payment_terms = _string_or_none(_value_for_semantic("payment_terms", merged, best_mappings)) or extract_payment_terms(merged)
    template_key = _string_or_none(_value_for_semantic("template_key", merged, best_mappings))
    size_band = resolve_size_band(_coerce_float(amount), config=None)

    archetype = _select_archetype(
        pattern_profile,
        motion_id=motion_id,
        segment=segment,
        product_family=product_family,
        size_band=size_band,
    )
    approval_pattern = _select_approval_pattern(pattern_profile, motion_id=motion_id, segment=segment)

    issues: list[ReadinessIssue] = []
    unsupported_reasons = _unsupported_reasons(environment_profile)
    if environment_profile.detected_engine == "unknown":
        issues.append(
            ReadinessIssue(
                code="DD_NO_QUOTE_ENGINE",
                severity="blocking",
                message="No supported quote object was detected in the target org.",
            )
        )
    elif environment_profile.detected_engine == "mixed":
        issues.append(
            ReadinessIssue(
                code="DD_AMBIGUOUS_QUOTE_ENGINE",
                severity="blocking",
                message="Multiple quote engines were detected; the execution path is ambiguous.",
            )
        )
    elif environment_profile.detected_engine == "third_party":
        issues.append(
            ReadinessIssue(
                code="DD_THIRD_PARTY_ENGINE",
                severity="blocking",
                message="Only third-party quote tooling was detected in the target org.",
            )
        )
    if archetype is None:
        issues.append(
            ReadinessIssue(
                code="DD_NO_ARCHETYPE_MATCH",
                severity="warning",
                message="No close historical archetype matched this quote request.",
            )
        )
    if pattern_profile.source_record_count < 5:
        issues.append(
            ReadinessIssue(
                code="DD_THIN_HISTORY",
                severity="warning",
                message="Historical deal profile is still thin; recommendations are directional.",
            )
        )
    if pattern_profile.learning_state == "history_only":
        issues.append(
            ReadinessIssue(
                code="DD_HISTORY_ONLY",
                severity="info",
                message="Pattern profile is based on historical CRM data only and has not learned from receipts yet.",
            )
        )
    if "No quote document/template object detected." in environment_profile.unsupported_reasons:
        issues.append(
            ReadinessIssue(
                code="DD_TEMPLATE_SIGNAL_MISSING",
                severity="warning",
                message="No quote template/document object was detected; artifact delivery may be limited.",
            )
        )

    required_semantics = {
        "primary_contact": "blocking",
        "legal_entity": "blocking",
        "payment_terms": "blocking",
        "contract_start": "warning",
        "contract_end": "warning",
    }
    assumptions: list[DealAssumption] = []
    for semantic_key, severity in required_semantics.items():
        mapping = best_mappings.get(semantic_key)
        legal_entity_fallback = _account_level_legal_entity_fallback(merged) if semantic_key == "legal_entity" else None
        if mapping is None:
            if legal_entity_fallback is not None:
                issues.append(
                    ReadinessIssue(
                        code="DD_LEGAL_ENTITY_ACCOUNT_FALLBACK",
                        severity=legal_entity_fallback.severity,
                        message=legal_entity_fallback.message,
                        field_name=legal_entity_fallback.field_name,
                    )
                )
                assumptions.append(
                    DealAssumption(
                        key=semantic_key,
                        value=legal_entity_fallback.value,
                        confidence=legal_entity_fallback.confidence,
                        rationale=legal_entity_fallback.rationale,
                        status="open",
                    )
                )
                continue
            issues.append(
                ReadinessIssue(
                    code=f"DD_NO_{semantic_key.upper()}_MAPPING",
                    severity=severity,
                    message=f"Could not resolve a field mapping for {semantic_key.replace('_', ' ')}.",
                )
            )
            assumptions.append(
                DealAssumption(
                    key=semantic_key,
                    value=None,
                    confidence=0.0,
                    rationale="No field mapping candidate was confirmed for this semantic key.",
                    status="open",
                )
            )
            continue
        if (not mapping.confirmed) or float(mapping.confidence) < 0.8:
            issues.append(
                ReadinessIssue(
                    code=f"DD_AMBIGUOUS_{semantic_key.upper()}_MAPPING",
                    severity="warning",
                    message=(
                        f"{semantic_key.replace('_', ' ').title()} mapping is still low confidence "
                        f"({float(mapping.confidence):.2f})."
                    ),
                    field_name=mapping.field_name,
                )
            )
        value = _value_for_semantic(semantic_key, merged, best_mappings)
        if value not in (None, ""):
            assumptions.append(
                DealAssumption(
                    key=semantic_key,
                    value=value,
                    confidence=0.95,
                    rationale=f"Observed on {mapping.object_name}.{mapping.field_name}",
                    status="confirmed",
                )
            )
            continue
        if semantic_key == "legal_entity" and legal_entity_fallback is not None:
            issues.append(
                ReadinessIssue(
                    code="DD_MISSING_LEGAL_ENTITY_USING_ACCOUNT_FALLBACK",
                    severity="warning",
                    message=(
                        f"Legal entity is not populated on {mapping.object_name}.{mapping.field_name}; "
                        f"using {legal_entity_fallback.field_name} as a lower-confidence fallback."
                    ),
                    field_name=legal_entity_fallback.field_name,
                )
            )
            assumptions.append(
                DealAssumption(
                    key=semantic_key,
                    value=legal_entity_fallback.value,
                    confidence=min(
                        float(mapping.confidence),
                        legal_entity_fallback.confidence,
                    ),
                    rationale=(
                        f"Expected on {mapping.object_name}.{mapping.field_name}; "
                        f"fallback observed on {legal_entity_fallback.field_name}"
                    ),
                    status="open",
                )
            )
            continue
        issues.append(
            ReadinessIssue(
                code=f"DD_MISSING_{semantic_key.upper()}",
                severity=severity,
                message=f"{semantic_key.replace('_', ' ').title()} is missing.",
                field_name=mapping.field_name,
            )
        )
        assumptions.append(
            DealAssumption(
                key=semantic_key,
                value=None,
                confidence=float(mapping.confidence),
                rationale=f"Expected on {mapping.object_name}.{mapping.field_name}",
                status="open",
            )
        )

    if archetype is not None and template_key in (None, "") and archetype.common_templates:
        assumptions.append(
            DealAssumption(
                key="template_key",
                value=archetype.common_templates[0],
                confidence=float(archetype.confidence),
                rationale="Common template for matched historical archetype.",
                status="open",
            )
        )
    if archetype is not None and payment_terms in (None, "") and archetype.typical_payment_terms:
        assumptions.append(
            DealAssumption(
                key="payment_terms",
                value=archetype.typical_payment_terms,
                confidence=float(archetype.confidence),
                rationale="Typical payment terms for matched historical archetype.",
                status="open",
            )
        )

    recommended_roles = []
    if approval_pattern is not None:
        recommended_roles.extend(approval_pattern.approver_roles)
    if archetype is not None:
        recommended_roles.extend(archetype.common_approver_roles)
    recommended_roles = _dedupe(recommended_roles)

    normalized_history = [
        normalize_deal_record(record)
        for record in (historical_records or [])
        if str(record.get("Id") or "").strip() and str(record.get("Id") or "").strip() != opportunity_id
    ]
    ranked_deals = rank_similar_deals(
        SimilarityInput(
            motion_id=motion_id,
            segment=segment,
            product_family=product_family,
            size_band=size_band,
            discount_pct=discount_pct,
            term_months=term_months,
            payment_terms=payment_terms,
            template_key=template_key,
            recommended_approval_roles=tuple(recommended_roles),
        ),
        normalized_history,
        limit=5,
    )
    if not ranked_deals:
        issues.append(
            ReadinessIssue(
                code="DD_NO_SIMILAR_DEALS",
                severity="warning",
                message="No historical deals produced usable similarity evidence.",
            )
        )
    elif ranked_deals[0].similarity_score < 0.45:
        issues.append(
            ReadinessIssue(
                code="DD_WEAK_SIMILARITY_EVIDENCE",
                severity="warning",
                message=(
                    f"Top historical evidence scored only {ranked_deals[0].similarity_score:.2f}; "
                    "the current deal shape is weakly represented in history."
                ),
            )
        )

    if archetype is not None and discount_pct is not None and archetype.p90_discount_pct is not None:
        if discount_pct > float(archetype.p90_discount_pct):
            issues.append(
                ReadinessIssue(
                    code="DD_DISCOUNT_ABOVE_P90",
                    severity="warning",
                    message=(
                        f"Requested discount {discount_pct:.2f}% is above the historical p90 "
                        f"for this archetype ({archetype.p90_discount_pct:.2f}%)."
                    ),
                )
            )

    confidence_breakdown = _compute_confidence(
        environment_confidence=environment_profile.engine_confidence,
        pattern_confidence=pattern_profile.profile_confidence,
        motion_confidence=motion_confidence,
        archetype=archetype,
        issues=issues,
        learning_state=pattern_profile.learning_state,
        field_mapping_confidence=_field_mapping_confidence(required_semantics.keys(), best_mappings),
        ranked_deals=ranked_deals,
        update_events=update_events or [],
    )
    overall_confidence = float(confidence_breakdown.final_confidence)
    readiness_mode = _readiness_mode(
        unsupported_reasons=unsupported_reasons,
        pattern_profile=pattern_profile,
        issues=issues,
        ranked_deals=ranked_deals,
        overall_confidence=overall_confidence,
    )
    ready = readiness_mode == "ready"

    context = _build_context(
        opportunity_id=opportunity_id,
        crm_org_id=crm_org_id,
        org_alias=environment_profile.org_alias,
        generated_at=generated_at,
        readiness_mode=readiness_mode,
        existing_context=existing_context,
        pattern_profile=pattern_profile,
        environment_profile=environment_profile,
        motion_id=motion_id,
        motion_confidence=motion_confidence,
        archetype=archetype,
        merged_request=merged,
        assumptions=assumptions,
        evidence_ids=[
            *[deal.record_id for deal in ranked_deals],
            *(archetype.evidence_ids if archetype is not None else []),
        ],
        overall_confidence=overall_confidence,
        renewal_opportunity_id=None,
    )
    result = QuoteReadinessResult(
        ready=ready,
        readiness_mode=readiness_mode,
        overall_confidence=overall_confidence,
        inferred_motion_id=motion_id,
        matched_archetype_id=archetype.archetype_id if archetype is not None else None,
        learning_state=pattern_profile.learning_state,
        recommended_approval_roles=recommended_roles,
        issues=issues,
        similar_deals=[
            SimilarDealEvidence(
                record_id=deal.record_id,
                similarity_score=deal.similarity_score,
                matched_dimensions=list(deal.matched_dimensions),
                mismatched_dimensions=list(deal.mismatched_dimensions),
                reasoning=deal.reasoning,
            )
            for deal in ranked_deals
        ],
        similarity_reasoning=_build_similarity_reasoning(
            ranked_deals=ranked_deals,
            archetype=archetype,
            pattern_profile=pattern_profile,
        ),
        confidence_breakdown=confidence_breakdown,
        context_id=context.context_id,
    )
    return result, context


def _build_context(
    *,
    opportunity_id: str,
    crm_org_id: str,
    org_alias: str | None,
    generated_at: str,
    readiness_mode: str,
    existing_context: DealContextV1 | None,
    pattern_profile: DealPatternProfileV1,
    environment_profile: QuoteEnvironmentProfileV1,
    motion_id: str,
    motion_confidence: float,
    archetype: DealArchetype | None,
    merged_request: dict[str, Any],
    assumptions: list[DealAssumption],
    evidence_ids: list[str],
    overall_confidence: float,
    renewal_opportunity_id: str | None,
) -> DealContextV1:
    created_at = existing_context.created_at if existing_context is not None else generated_at
    context_id = existing_context.context_id if existing_context is not None else f"ctx-{opportunity_id}"
    assumptions_at_creation = (
        existing_context.assumptions_at_creation if existing_context is not None and existing_context.assumptions_at_creation
        else [assumption.model_copy(deep=True) for assumption in assumptions]
    )
    return DealContextV1(
        context_id=context_id,
        crm_org_id=crm_org_id,
        org_alias=org_alias,
        opportunity_id=opportunity_id,
        account_id=_string_or_none(merged_request.get("AccountId")),
        created_at=created_at,
        updated_at=generated_at,
        lifecycle_state=_context_state_for_mode(readiness_mode),
        close_workflow_state=(
            existing_context.close_workflow_state if existing_context is not None else "deal_active"
        ),
        close_submitted_at=existing_context.close_submitted_at if existing_context is not None else None,
        archived=False,
        inferred_motion_id=motion_id,
        inferred_motion_confidence=motion_confidence,
        archetype_id=archetype.archetype_id if archetype is not None else None,
        pattern_profile_id=pattern_profile.profile_id,
        environment_profile_id=environment_profile.profile_id,
        confidence_at_creation=existing_context.confidence_at_creation if existing_context is not None else overall_confidence,
        original_request=dict(merged_request),
        normalized_quote_request=_normalized_quote_request(merged_request, motion_id, motion_confidence),
        assumptions=assumptions,
        assumptions_at_creation=assumptions_at_creation,
        evidence_ids=_dedupe(evidence_ids),
        approval_chain=list(existing_context.approval_chain) if existing_context is not None else [],
        quote_refs=list(existing_context.quote_refs) if existing_context is not None else [],
        receipt_ids=list(existing_context.receipt_ids) if existing_context is not None else [],
        artifact_refs=list(existing_context.artifact_refs) if existing_context is not None else [],
        close_findings=list(existing_context.close_findings) if existing_context is not None else [],
        close_packet_paths=list(existing_context.close_packet_paths) if existing_context is not None else [],
        reconciliation_packet_paths=(
            list(existing_context.reconciliation_packet_paths) if existing_context is not None else []
        ),
        latest_reconciliation_packet_id=(
            existing_context.latest_reconciliation_packet_id if existing_context is not None else None
        ),
        renewal_opportunity_id=renewal_opportunity_id or (existing_context.renewal_opportunity_id if existing_context is not None else None),
    )


def _normalized_quote_request(
    merged_request: dict[str, Any],
    motion_id: str,
    motion_confidence: float,
) -> dict[str, Any]:
    payload = {
        "motion_id": motion_id,
        "motion_confidence": motion_confidence,
        "amount": extract_amount(merged_request),
        "discount_pct": extract_discount_pct(merged_request),
        "term_months": extract_term_months(merged_request),
        "segment": extract_segment(merged_request),
        "product_family": extract_product_family(merged_request),
        "payment_terms": extract_payment_terms(merged_request),
    }
    return {key: value for key, value in payload.items() if value not in (None, "")}


def _select_archetype(
    profile: DealPatternProfileV1,
    *,
    motion_id: str,
    segment: str | None,
    product_family: str | None,
    size_band: str,
) -> DealArchetype | None:
    scored: list[tuple[float, DealArchetype]] = []
    for archetype in profile.archetypes:
        if archetype.motion_id != motion_id:
            continue
        score = float(archetype.confidence)
        if segment and archetype.segment and archetype.segment.lower() == segment.lower():
            score += 0.15
        if product_family and archetype.product_family and archetype.product_family.lower() == product_family.lower():
            score += 0.15
        if size_band and archetype.size_band and archetype.size_band.lower() == size_band.lower():
            score += 0.1
        scored.append((score, archetype))
    if not scored:
        return None
    scored.sort(key=lambda item: (-item[0], -item[1].observed_deal_count, item[1].archetype_id))
    return scored[0][1]


def _select_approval_pattern(
    profile: DealPatternProfileV1,
    *,
    motion_id: str,
    segment: str | None,
) -> ApprovalPattern | None:
    scored: list[tuple[float, ApprovalPattern]] = []
    for pattern in profile.approval_patterns:
        if pattern.motion_id not in (None, motion_id):
            continue
        score = float(pattern.confidence)
        if segment and pattern.segment and pattern.segment.lower() == segment.lower():
            score += 0.15
        scored.append((score, pattern))
    if not scored:
        return None
    scored.sort(key=lambda item: (-item[0], -item[1].observed_count, item[1].pattern_id))
    return scored[0][1]


def _compute_confidence(
    *,
    environment_confidence: float,
    pattern_confidence: float,
    motion_confidence: float,
    archetype: DealArchetype | None,
    issues: list[ReadinessIssue],
    learning_state: str,
    field_mapping_confidence: float,
    ranked_deals: list[Any],
    update_events: list[PatternUpdateEventV1],
) -> ConfidenceBreakdown:
    similarity_evidence_confidence = _similarity_evidence_confidence(ranked_deals)
    receipt_success_confidence = _receipt_success_confidence(update_events)
    evidence_count = len([deal for deal in ranked_deals if float(deal.similarity_score) >= 0.55])
    learning_state_adjustment = 0.0
    if learning_state == "history_plus_receipts":
        learning_state_adjustment = 0.04
    elif learning_state == "mature":
        learning_state_adjustment = 0.08

    issue_penalty = 0.0
    for issue in issues:
        if issue.severity == "blocking":
            issue_penalty += 0.14
        elif issue.severity == "warning":
            issue_penalty += 0.05
        else:
            issue_penalty += 0.01

    archetype_confidence = float(archetype.confidence) if archetype is not None else 0.25
    base_confidence = (
        (float(environment_confidence) * 0.18)
        + (float(pattern_confidence) * 0.16)
        + (float(motion_confidence) * 0.14)
        + (archetype_confidence * 0.12)
        + (similarity_evidence_confidence * 0.22)
        + (field_mapping_confidence * 0.08)
        + (receipt_success_confidence * 0.10)
    )
    evidence_bonus = min(evidence_count, 5) * 0.02
    final_confidence = max(
        0.0,
        min(round(base_confidence + learning_state_adjustment + evidence_bonus - issue_penalty, 4), 1.0),
    )
    return ConfidenceBreakdown(
        environment_confidence=max(0.0, min(round(float(environment_confidence), 4), 1.0)),
        pattern_confidence=max(0.0, min(round(float(pattern_confidence), 4), 1.0)),
        motion_confidence=max(0.0, min(round(float(motion_confidence), 4), 1.0)),
        archetype_confidence=max(0.0, min(round(archetype_confidence, 4), 1.0)),
        similarity_evidence_confidence=max(0.0, min(round(similarity_evidence_confidence, 4), 1.0)),
        field_mapping_confidence=max(0.0, min(round(field_mapping_confidence, 4), 1.0)),
        receipt_success_confidence=max(0.0, min(round(receipt_success_confidence, 4), 1.0)),
        evidence_count=evidence_count,
        learning_state_adjustment=round(learning_state_adjustment, 4),
        issue_penalty=round(issue_penalty, 4),
        final_confidence=final_confidence,
    )


def _unsupported_reasons(environment_profile: QuoteEnvironmentProfileV1) -> list[str]:
    reasons: list[str] = []
    if environment_profile.detected_engine == "unknown":
        reasons.append("No quote engine was detected in the target org.")
    elif environment_profile.detected_engine == "mixed":
        reasons.append("Multiple quote engines were detected and the active execution path is ambiguous.")
    elif environment_profile.detected_engine == "third_party":
        reasons.append("Only third-party quote tooling was detected in the target org.")
    return reasons


def _readiness_mode(
    *,
    unsupported_reasons: list[str],
    pattern_profile: DealPatternProfileV1,
    issues: list[ReadinessIssue],
    ranked_deals: list[Any],
    overall_confidence: float,
) -> Literal["ready", "review_required", "unsupported", "insufficient_evidence"]:
    has_blocking_issue = any(issue.severity == "blocking" for issue in issues)
    best_similarity = float(ranked_deals[0].similarity_score) if ranked_deals else 0.0
    evidence_count = len([deal for deal in ranked_deals if float(deal.similarity_score) >= 0.55])
    if unsupported_reasons:
        return "unsupported"
    if pattern_profile.source_record_count < 3:
        return "insufficient_evidence"
    if evidence_count == 0 and (pattern_profile.source_record_count < 10 or best_similarity < 0.45):
        return "insufficient_evidence"
    if overall_confidence >= 0.64 and not has_blocking_issue and best_similarity >= 0.6:
        return "ready"
    return "review_required"


def _context_state_for_mode(readiness_mode: str) -> str:
    mapping = {
        "ready": "quote_ready",
        "review_required": "quote_review_required",
        "unsupported": "quote_unsupported",
        "insufficient_evidence": "quote_insufficient_evidence",
    }
    return mapping.get(readiness_mode, "quote_review_required")


def _field_mapping_confidence(
    required_semantics: Iterable[str],
    best_mappings: dict[str, Any],
) -> float:
    scores: list[float] = []
    for semantic_key in required_semantics:
        mapping = best_mappings.get(str(semantic_key))
        if mapping is None:
            scores.append(0.0)
            continue
        confidence = float(mapping.confidence)
        if mapping.confirmed:
            confidence = min(1.0, confidence + 0.05)
        scores.append(confidence)
    if not scores:
        return 0.0
    return round(sum(scores) / len(scores), 4)


def _similarity_evidence_confidence(ranked_deals: list[Any]) -> float:
    if not ranked_deals:
        return 0.18
    top_scores = [float(deal.similarity_score) for deal in ranked_deals[:3]]
    coverage = mean(float(getattr(deal, "coverage_ratio", 0.0)) for deal in ranked_deals[:3])
    return round((mean(top_scores) * 0.8) + (coverage * 0.2), 4)


def _receipt_success_confidence(update_events: list[PatternUpdateEventV1]) -> float:
    successes = 0
    failures = 0
    for event in update_events:
        source_type = str(event.source_type or "").strip().lower()
        outcome = str(event.extracted_signals.outcome or "").strip().lower()
        if source_type not in {
            "quote_receipt",
            "approval_receipt",
            "approval_stamp",
            "close_packet",
            "renewal_shell",
        }:
            continue
        if outcome in {"failed", "error", "rejected", "rolled_back"}:
            failures += 1
        else:
            successes += 1
    if successes == 0 and failures == 0:
        return 0.35
    confidence = 0.35 + (min(successes, 5) * 0.09) - (min(failures, 3) * 0.08)
    return max(0.0, min(round(confidence, 4), 1.0))


def _build_similarity_reasoning(
    *,
    ranked_deals: list[Any],
    archetype: DealArchetype | None,
    pattern_profile: DealPatternProfileV1,
) -> list[str]:
    reasoning: list[str] = []
    if ranked_deals:
        top_deal = ranked_deals[0]
        if top_deal.matched_dimensions:
            matched = ", ".join(top_deal.matched_dimensions[:3])
            reasoning.append(
                f"Top evidence {top_deal.record_id} scored {top_deal.similarity_score:.2f} with matches on {matched}."
            )
        else:
            reasoning.append(
                f"Top evidence {top_deal.record_id} scored {top_deal.similarity_score:.2f} with limited direct matches."
            )
        if top_deal.mismatched_dimensions:
            reasoning.append(f"Primary divergence: {top_deal.mismatched_dimensions[0]}.")
    else:
        reasoning.append("No historical deals were comparable enough to rank as evidence.")

    if archetype is not None:
        reasoning.append(
            f"Matched archetype {archetype.archetype_id} from {archetype.observed_deal_count} observed deals."
        )
    else:
        reasoning.append("No archetype produced a confident match for this deal shape.")

    evidence_count = len([deal for deal in ranked_deals if float(deal.similarity_score) >= 0.55])
    if evidence_count < 3:
        reasoning.append(
            f"Only {evidence_count} similar deals cleared the evidence threshold across {pattern_profile.source_record_count} historical records."
        )
    return reasoning


def _best_mapping_by_semantic(candidates: list[Any]) -> dict[str, Any]:
    best: dict[str, Any] = {}
    for candidate in candidates:
        current = best.get(candidate.semantic_key)
        if current is None or float(candidate.confidence) > float(current.confidence):
            best[candidate.semantic_key] = candidate
    return best


def _value_for_semantic(
    semantic_key: str,
    payload: dict[str, Any],
    best_mappings: dict[str, Any],
) -> Any:
    if semantic_key in payload and payload.get(semantic_key) not in (None, ""):
        return payload.get(semantic_key)
    mapping = best_mappings.get(semantic_key)
    if mapping is None:
        return None
    if getattr(mapping, "object_name", "").lower() == "account":
        account_key = f"Account.{mapping.field_name}"
        if payload.get(account_key) not in (None, ""):
            return payload.get(account_key)
    return payload.get(mapping.field_name)


def _account_level_legal_entity_fallback(payload: dict[str, Any]) -> _LegalEntityFallback | None:
    strong_candidates: tuple[tuple[str, float], ...] = (
        ("Account.Legal_Name__c", 0.82),
        ("Account.Legal_Entity__c", 0.8),
        ("Account.Legal_Entity_Name__c", 0.8),
        ("Account.Entity__c", 0.76),
        ("Account.Contracting_Entity__c", 0.74),
        ("Account.Billing_Entity__c", 0.72),
        ("Account.Billing_Entity_Name__c", 0.72),
        ("Account.Sold_To_Entity__c", 0.72),
        ("Account.Seller_Entity__c", 0.72),
    )
    weak_candidates: tuple[tuple[str, float], ...] = (("Account.Name", 0.48),)

    for field_name, confidence in strong_candidates:
        value = _string_or_none(payload.get(field_name))
        if value is None:
            continue
        return _LegalEntityFallback(
            field_name=field_name,
            value=value,
            confidence=confidence,
            severity="warning",
            message=(
                f"No discrete opportunity/quote legal entity field was resolved; "
                f"using {field_name} as account-level evidence."
            ),
            rationale=f"Observed on {field_name}",
        )
    for field_name, confidence in weak_candidates:
        value = _string_or_none(payload.get(field_name))
        if value is None:
            continue
        return _LegalEntityFallback(
            field_name=field_name,
            value=value,
            confidence=confidence,
            severity="info",
            message=(
                f"No legal entity field was detected; "
                f"using {field_name} as a weak account-level proxy."
            ),
            rationale=f"Observed on {field_name}",
        )
    return None


def _dedupe(values: list[str]) -> list[str]:
    seen: set[str] = set()
    ordered: list[str] = []
    for value in values:
        text = str(value or "").strip()
        if not text or text in seen:
            continue
        ordered.append(text)
        seen.add(text)
    return ordered


def _string_or_none(value: Any) -> str | None:
    if value in (None, ""):
        return None
    text = str(value).strip()
    return text or None


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


def _coerce_int(value: Any) -> int | None:
    parsed = _coerce_float(value)
    if parsed is None:
        return None
    return int(round(parsed))


__all__ = [
    "ConfidenceBreakdown",
    "QuoteReadinessResult",
    "ReadinessIssue",
    "SimilarDealEvidence",
    "evaluate_quote_readiness",
]
