"""Quote environment discovery and field resolution for Deal Desk."""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Iterable, get_args

from g_gremlin import sfdc as sfdc_helpers
from g_gremlin.autopilot.ai_provider import AIProviderConfig, ai_field_mapping_pass
from g_gremlin.autopilot.discovery import DiscoveryRunner, FieldMappingSuggestion, ObjectProfile

from .config import DealDeskConfig
from .profiles import (
    FieldMappingCandidate,
    QuoteAdapterCapability,
    QuoteAdapterCapabilityProfileV1,
    QuoteEnvironmentProfileV1,
)


_QUOTE_OBJECT_CANDIDATES: tuple[str, ...] = (
    "Opportunity",
    "Quote",
    "QuoteLineItem",
    "QuoteDocument",
    "Pricebook2",
    "ProcessInstance",
    "SBQQ__Quote__c",
    "SBQQ__QuoteLine__c",
    "SBQQ__QuoteDocument__c",
    "SBQQ__QuoteTemplate__c",
)

_STANDARD_QUOTE_OBJECTS = {"Quote", "QuoteLineItem"}
_CPQ_QUOTE_OBJECTS = {"SBQQ__Quote__c", "SBQQ__QuoteLine__c"}
_QUOTE_HEADER_OBJECTS = {"Quote", "SBQQ__Quote__c"}
_QUOTE_LINE_ITEM_OBJECTS = {"QuoteLineItem", "SBQQ__QuoteLine__c"}
_ALL_ADAPTER_CAPABILITIES: tuple[QuoteAdapterCapability, ...] = get_args(QuoteAdapterCapability)


@dataclass(frozen=True)
class _SemanticRule:
    semantic_key: str
    object_names: tuple[str, ...]
    exact_names: tuple[str, ...]
    tokens: tuple[str, ...]


_SEMANTIC_RULES: tuple[_SemanticRule, ...] = (
    _SemanticRule(
        semantic_key="amount",
        object_names=("Opportunity", "Quote", "SBQQ__Quote__c"),
        exact_names=("Amount", "GrandTotal", "SBQQ__NetAmount__c", "SBQQ__CustomerAmount__c"),
        tokens=("amount", "net amount", "grand total", "customer amount", "tcv"),
    ),
    _SemanticRule(
        semantic_key="arr",
        object_names=("Opportunity",),
        exact_names=("ARR__c", "Annual_Recurring_Revenue__c"),
        tokens=("arr", "annual recurring"),
    ),
    _SemanticRule(
        semantic_key="discount_pct",
        object_names=("Opportunity", "Quote", "SBQQ__Quote__c"),
        exact_names=("Discount__c", "DiscountPercent", "SBQQ__CustomerDiscount__c"),
        tokens=("discount", "discount percent", "discount pct"),
    ),
    _SemanticRule(
        semantic_key="term_months",
        object_names=("Opportunity", "Quote", "SBQQ__Quote__c"),
        exact_names=("ContractTerm", "Term_Months__c", "Duration_Months__c", "Term__c", "SBQQ__SubscriptionTerm__c"),
        tokens=("term", "subscription term", "months"),
    ),
    _SemanticRule(
        semantic_key="payment_terms",
        object_names=("Opportunity", "Quote", "SBQQ__Quote__c"),
        exact_names=(
            "Payment_Terms__c",
            "PaymentTerms__c",
            "Payment_Term__c",
            "Billing_Terms__c",
            "Billing_Term__c",
            "Terms_For_Payment__c",
            "Terms_Of_Payment__c",
        ),
        tokens=("payment terms", "payment term", "billing terms", "billing term", "terms for payment", "terms of payment", "net terms"),
    ),
    _SemanticRule(
        semantic_key="contract_start",
        object_names=("Opportunity", "Quote", "SBQQ__Quote__c"),
        exact_names=("ServiceDate", "StartDate", "Start_Date__c", "SBQQ__StartDate__c"),
        tokens=("start date", "contract start", "service start"),
    ),
    _SemanticRule(
        semantic_key="contract_end",
        object_names=("Opportunity", "Quote", "SBQQ__Quote__c"),
        exact_names=("EndDate", "End_Date__c", "SBQQ__EndDate__c"),
        tokens=("end date", "contract end", "service end"),
    ),
    _SemanticRule(
        semantic_key="legal_entity",
        object_names=("Opportunity", "Quote", "SBQQ__Quote__c"),
        exact_names=(
            "Entity__c",
            "Legal_Entity__c",
            "Legal_Entity_Name__c",
            "Legal_Name__c",
            "Billing_Entity__c",
            "Billing_Entity_Name__c",
            "Contracting_Entity__c",
            "Sold_To_Entity__c",
            "Seller_Entity__c",
        ),
        tokens=("legal entity", "contracting entity", "contract entity", "billing entity", "sold to entity", "seller entity", "operating entity"),
    ),
    _SemanticRule(
        semantic_key="primary_contact",
        object_names=("Opportunity", "Quote", "SBQQ__Quote__c"),
        exact_names=("Primary_Contact__c", "ContactId", "SBQQ__PrimaryContact__c"),
        tokens=("primary contact", "signatory contact", "billing contact"),
    ),
    _SemanticRule(
        semantic_key="pricebook",
        object_names=("Opportunity", "Quote", "SBQQ__Quote__c"),
        exact_names=("Pricebook2Id", "SBQQ__PriceBook__c"),
        tokens=("pricebook", "price book"),
    ),
    _SemanticRule(
        semantic_key="template_key",
        object_names=("Quote", "SBQQ__Quote__c", "SBQQ__QuoteTemplate__c"),
        exact_names=("Template__c", "QuoteTemplateId", "SBQQ__Template__c"),
        tokens=("template", "document template", "quote template"),
    ),
    _SemanticRule(
        semantic_key="quote_status",
        object_names=("Quote", "SBQQ__Quote__c"),
        exact_names=("Status", "SBQQ__Status__c"),
        tokens=("status", "quote status"),
    ),
    _SemanticRule(
        semantic_key="quote_type",
        object_names=("Quote", "SBQQ__Quote__c"),
        exact_names=("Type", "SBQQ__Type__c"),
        tokens=("type", "quote type"),
    ),
)


def discover_quote_object_profiles(
    org_alias: str | None,
    *,
    object_names: Iterable[str] | None = None,
) -> tuple[list[ObjectProfile], dict[str, list[dict[str, Any]]]]:
    profiles: list[ObjectProfile] = []
    fields_by_object: dict[str, list[dict[str, Any]]] = {}
    for object_name in object_names or _QUOTE_OBJECT_CANDIDATES:
        try:
            describe_payload = sfdc_helpers.describe_sobject(object_name, org_alias=org_alias)
        except Exception:
            continue
        raw_fields = _extract_fields(describe_payload)
        if not raw_fields:
            continue
        field_names = [str(field.get("name")) for field in raw_fields if field.get("name")]
        custom_field_count = sum(1 for name in field_names if name.lower().endswith("__c"))
        profiles.append(
            ObjectProfile(
                sobject=object_name,
                record_count=0,
                key_fields=field_names,
                custom_field_count=custom_field_count,
                last_modified=None,
            )
        )
        fields_by_object[object_name] = raw_fields
    return profiles, fields_by_object


def build_quote_environment_profile(
    *,
    crm_org_id: str,
    generated_at: str,
    org_alias: str | None = None,
    ai_config: AIProviderConfig | None = None,
    object_profiles: list[ObjectProfile] | None = None,
    fields_by_object: dict[str, list[dict[str, Any]]] | None = None,
) -> QuoteEnvironmentProfileV1:
    profile_list = list(object_profiles or [])
    described_fields = dict(fields_by_object or {})
    if not profile_list or not described_fields:
        discovered_profiles, discovered_fields = discover_quote_object_profiles(org_alias)
        existing_names = {profile.sobject for profile in profile_list}
        for profile in discovered_profiles:
            if profile.sobject not in existing_names:
                profile_list.append(profile)
        described_fields.update(discovered_fields)

    suggestions = _build_mapping_suggestions(profile_list, described_fields)
    suggestions = _apply_ai_adjustments(
        suggestions=suggestions,
        object_profiles=profile_list,
        ai_config=ai_config,
    )
    field_mappings = _to_field_mapping_candidates(suggestions)
    engine_name, engine_confidence = _detect_engine(profile_list)
    quote_objects = sorted(
        profile.sobject for profile in profile_list if profile.sobject in _STANDARD_QUOTE_OBJECTS | _CPQ_QUOTE_OBJECTS
    )
    price_objects = sorted(
        profile.sobject
        for profile in profile_list
        if profile.sobject in {"Pricebook2", "PricebookEntry"}
        or "pricebook" in profile.sobject.lower()
    )
    approval_objects = sorted(
        profile.sobject
        for profile in profile_list
        if profile.sobject == "ProcessInstance" or "approval" in profile.sobject.lower()
    )
    template_objects = sorted(
        profile.sobject
        for profile in profile_list
        if "document" in profile.sobject.lower() or "template" in profile.sobject.lower()
    )

    supported_capabilities = ["quote_readiness"]
    if quote_objects:
        supported_capabilities.append("quote_execution")
    if price_objects:
        supported_capabilities.append("pricebook_resolution")
    if approval_objects:
        supported_capabilities.append("approval_intelligence")
    if template_objects:
        supported_capabilities.append("pdf_generation")

    unsupported_reasons: list[str] = []
    if not quote_objects:
        unsupported_reasons.append("No native quote object detected in target org.")
    if not template_objects:
        unsupported_reasons.append("No quote document/template object detected.")

    return QuoteEnvironmentProfileV1(
        profile_id=f"qep-{_safe_profile_key(crm_org_id, org_alias)}",
        crm_org_id=crm_org_id,
        org_alias=org_alias,
        generated_at=generated_at,
        detected_engine=engine_name,
        engine_confidence=engine_confidence,
        engine_evidence_ids=[f"describe:{name}" for name in quote_objects[:4]],
        quote_objects=quote_objects,
        price_objects=price_objects,
        approval_objects=approval_objects,
        template_objects=template_objects,
        pdf_generation_mode=_pdf_generation_mode(template_objects),
        required_fields=_required_fields_by_object(field_mappings),
        field_mappings=field_mappings,
        adapter_capability_profile=build_quote_adapter_capability_profile(
            detected_engine=engine_name,
            quote_objects=quote_objects,
            price_objects=price_objects,
            approval_objects=approval_objects,
            template_objects=template_objects,
        ),
        supported_capabilities=sorted(dict.fromkeys(supported_capabilities)),
        unsupported_reasons=unsupported_reasons,
        evidence_ids=sorted({f"describe:{name}" for name in described_fields.keys()}),
        source_receipt_ids=[],
    )


def build_dealdesk_config_draft(
    environment_profile: QuoteEnvironmentProfileV1,
    *,
    base_config: DealDeskConfig | None = None,
) -> DealDeskConfig:
    config = base_config.model_copy(deep=True) if base_config is not None else DealDeskConfig()
    best_by_semantic = _best_mapping_by_semantic(environment_profile.field_mappings)

    amount_mapping = best_by_semantic.get("amount")
    if amount_mapping is not None:
        config.amount_rules.amount_field = amount_mapping.field_name

    arr_mapping = best_by_semantic.get("arr")
    if arr_mapping is not None:
        config.amount_rules.arr_field = arr_mapping.field_name

    term_mapping = best_by_semantic.get("term_months")
    if term_mapping is not None:
        config.amount_rules.term_months_field = term_mapping.field_name

    start_mapping = best_by_semantic.get("contract_start")
    if start_mapping is not None:
        config.date_rules.contract_start_field = start_mapping.field_name

    end_mapping = best_by_semantic.get("contract_end")
    if end_mapping is not None:
        config.date_rules.contract_end_field = end_mapping.field_name

    required_defaults: list[str] = []
    for semantic_key in ("primary_contact", "legal_entity", "payment_terms"):
        mapping = best_by_semantic.get(semantic_key)
        if mapping is not None:
            required_defaults.append(mapping.field_name)

    if required_defaults:
        close_won = config.stages.close_won_stage
        config.required_fields.setdefault(close_won, {})
        config.required_fields[close_won]["default"] = sorted(dict.fromkeys(required_defaults))
    return config


def build_quote_adapter_capability_profile(
    *,
    detected_engine: str,
    quote_objects: list[str],
    price_objects: list[str],
    approval_objects: list[str],
    template_objects: list[str],
) -> QuoteAdapterCapabilityProfileV1:
    normalized_engine = str(detected_engine or "unknown").strip().lower() or "unknown"
    adapter_key, adapter_family = _adapter_identity(normalized_engine)
    supported: set[QuoteAdapterCapability] = set()
    notes = [
        "supported_capabilities are inferred from discovered CRM-visible quote objects.",
        "execution_verified_capabilities identify the subset with deterministic Deal Desk support today.",
    ]

    has_quote_header = any(item in _QUOTE_HEADER_OBJECTS for item in quote_objects)
    has_quote_line_items = any(item in _QUOTE_LINE_ITEM_OBJECTS for item in quote_objects)

    if has_quote_header:
        supported.update(("read_status", "create_or_open_quote", "update_quote"))
    if has_quote_line_items:
        supported.add("read_line_items")
    if price_objects:
        supported.add("simulate_or_price")
    if approval_objects and has_quote_header:
        supported.add("submit_for_approval")
    if template_objects and has_quote_header:
        supported.add("generate_document")

    execution_verified = _execution_verified_capabilities(
        detected_engine=normalized_engine,
        has_quote_header=has_quote_header,
        has_quote_line_items=has_quote_line_items,
    )
    execution_verified_supported = sorted(capability for capability in supported if capability in execution_verified)

    if normalized_engine == "mixed":
        notes.append(
            "Multiple quote stacks were detected; capabilities reflect only the shared CRM-visible surface."
        )
    elif normalized_engine in {"third_party", "unknown"}:
        notes.append(
            "Detected quote stack is unknown or third-party, so capability support remains conservative until a pilot requires deeper adapter work."
        )

    if not has_quote_header:
        notes.append(
            "No quote header object was discovered, so lifecycle mutation capabilities remain unsupported."
        )
    if not has_quote_line_items:
        notes.append(
            "No quote line item object was discovered, so line-item read capability remains unsupported."
        )
    if not approval_objects:
        notes.append(
            "No approval object was discovered, so approval submission support remains unsupported."
        )
    if not template_objects:
        notes.append(
            "No quote document or template object was discovered, so document generation remains unsupported."
        )
    if not execution_verified_supported:
        notes.append(
            "No advertised capabilities are execution-verified yet; support remains discovery-inferred."
        )
    elif len(execution_verified_supported) < len(supported):
        notes.append(
            "Some advertised capabilities remain inference-only; use execution_verified_capabilities before promising deep adapter coverage in pilots."
        )
    else:
        notes.append("All advertised capabilities are currently execution-verified.")
    if normalized_engine == "salesforce_cpq" and "update_quote" in supported and "update_quote" not in execution_verified:
        notes.append(
            "Salesforce CPQ guided preview can be surfaced, but quote mutation execution remains unverified until the deep write adapter ships."
        )

    unsupported = [capability for capability in _ALL_ADAPTER_CAPABILITIES if capability not in supported]
    return QuoteAdapterCapabilityProfileV1(
        adapter_key=adapter_key,
        adapter_family=adapter_family,
        detected_engine=normalized_engine,
        supported_capabilities=sorted(supported),
        execution_verified_capabilities=execution_verified_supported,
        unsupported_capabilities=unsupported,
        notes=notes,
    )


def _execution_verified_capabilities(
    *,
    detected_engine: str,
    has_quote_header: bool,
    has_quote_line_items: bool,
) -> set[QuoteAdapterCapability]:
    verified: set[QuoteAdapterCapability] = set()
    if detected_engine == "standard_quotes":
        if has_quote_header:
            verified.update(("read_status", "create_or_open_quote", "update_quote"))
        if has_quote_line_items:
            verified.add("read_line_items")
        return verified
    if detected_engine == "salesforce_cpq":
        if has_quote_header:
            verified.add("read_status")
        if has_quote_line_items:
            verified.add("read_line_items")
    return verified


def _build_mapping_suggestions(
    object_profiles: list[ObjectProfile],
    fields_by_object: dict[str, list[dict[str, Any]]],
) -> list[FieldMappingSuggestion]:
    suggestions: list[FieldMappingSuggestion] = []
    available_objects = {profile.sobject for profile in object_profiles}
    for rule in _SEMANTIC_RULES:
        for object_name in rule.object_names:
            if object_name not in available_objects:
                continue
            for field_info in fields_by_object.get(object_name, []):
                name = str(field_info.get("name") or "").strip()
                if not name:
                    continue
                label = str(field_info.get("label") or name).strip()
                confidence = _mapping_confidence(
                    field_name=name,
                    field_label=label,
                    exact_names=rule.exact_names,
                    tokens=rule.tokens,
                )
                if confidence <= 0.0:
                    continue
                suggestions.append(
                    FieldMappingSuggestion(
                        target=rule.semantic_key,
                        source_field=f"{object_name}.{name}",
                        confidence=confidence,
                        reason=_mapping_reason(
                            semantic_key=rule.semantic_key,
                            object_name=object_name,
                            field_name=name,
                            field_label=label,
                            exact_names=rule.exact_names,
                        ),
                        needs_confirmation=confidence < 0.85,
                    )
                )
    suggestions.sort(key=lambda item: (item.target, -item.confidence, item.source_field))
    deduped: list[FieldMappingSuggestion] = []
    seen: set[tuple[str, str]] = set()
    for suggestion in suggestions:
        key = (suggestion.target.lower(), suggestion.source_field.lower())
        if key in seen:
            continue
        deduped.append(suggestion)
        seen.add(key)
    return deduped


def _apply_ai_adjustments(
    *,
    suggestions: list[FieldMappingSuggestion],
    object_profiles: list[ObjectProfile],
    ai_config: AIProviderConfig | None,
) -> list[FieldMappingSuggestion]:
    if ai_config is None or not ai_config.enabled:
        return suggestions

    ambiguous = [
        suggestion.model_dump(exclude_none=False)
        for suggestion in suggestions
        if suggestion.needs_confirmation
    ]
    if not ambiguous:
        return suggestions

    adjustments = ai_field_mapping_pass(
        ai_config,
        ambiguous,
        [profile.model_dump(exclude_none=False) for profile in object_profiles],
    )
    return DiscoveryRunner._apply_ai_adjustments(suggestions, adjustments)


def _to_field_mapping_candidates(
    suggestions: list[FieldMappingSuggestion],
) -> list[FieldMappingCandidate]:
    candidates: list[FieldMappingCandidate] = []
    for suggestion in suggestions:
        object_name, field_name = _split_object_field(suggestion.source_field)
        if not object_name or not field_name:
            continue
        candidates.append(
            FieldMappingCandidate(
                semantic_key=suggestion.target,
                object_name=object_name,
                field_name=field_name,
                confidence=suggestion.confidence,
                rationale=suggestion.reason,
                evidence_ids=[f"describe:{object_name}"],
                confirmed=(not suggestion.needs_confirmation) or suggestion.confidence >= 0.8,
            )
        )
    candidates.sort(
        key=lambda item: (
            item.semantic_key,
            -float(item.confidence),
            item.object_name.lower(),
            item.field_name.lower(),
        )
    )
    return candidates


def _best_mapping_by_semantic(
    candidates: list[FieldMappingCandidate],
) -> dict[str, FieldMappingCandidate]:
    best: dict[str, FieldMappingCandidate] = {}
    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 _required_fields_by_object(
    candidates: list[FieldMappingCandidate],
) -> dict[str, list[str]]:
    grouped: dict[str, list[str]] = {}
    for candidate in candidates:
        if not candidate.confirmed:
            continue
        grouped.setdefault(candidate.object_name, [])
        if candidate.field_name not in grouped[candidate.object_name]:
            grouped[candidate.object_name].append(candidate.field_name)
    return grouped


def _detect_engine(object_profiles: list[ObjectProfile]) -> tuple[str, float]:
    present = {profile.sobject for profile in object_profiles}
    has_standard = bool(_STANDARD_QUOTE_OBJECTS & present)
    has_cpq = bool(_CPQ_QUOTE_OBJECTS & present)
    if has_standard and has_cpq:
        return "mixed", 0.95
    if has_cpq:
        return "salesforce_cpq", 0.92
    if has_standard:
        return "standard_quotes", 0.84
    return "unknown", 0.2


def _pdf_generation_mode(template_objects: list[str]) -> str | None:
    if not template_objects:
        return "unknown"
    if any(object_name.startswith("SBQQ__") for object_name in template_objects):
        return "templated"
    return "native"


def _mapping_confidence(
    *,
    field_name: str,
    field_label: str,
    exact_names: tuple[str, ...],
    tokens: tuple[str, ...],
) -> float:
    lowered_name = field_name.lower()
    lowered_label = field_label.lower()
    normalized_name = lowered_name.replace("__c", "").replace("_", " ")
    for exact_name in exact_names:
        if lowered_name == exact_name.lower():
            return 0.96
    for exact_name in exact_names:
        if lowered_name.endswith(exact_name.lower()):
            return 0.88
    for token in tokens:
        lowered_token = token.lower()
        if lowered_label == lowered_token or normalized_name == lowered_token:
            return 0.82
    token_hits = 0
    for token in tokens:
        lowered_token = token.lower()
        if lowered_token in lowered_name or lowered_token in lowered_label:
            token_hits += 1
    if token_hits == 0:
        return 0.0
    if token_hits >= 2:
        return 0.82
    return 0.66


def _mapping_reason(
    *,
    semantic_key: str,
    object_name: str,
    field_name: str,
    field_label: str,
    exact_names: tuple[str, ...],
) -> str:
    for exact_name in exact_names:
        if field_name.lower() == exact_name.lower():
            return (
                f"Exact field match for {semantic_key} on {object_name} "
                f"({field_name}, label={field_label})"
            )
    return (
        f"Field name or label suggests {semantic_key} on {object_name} "
        f"({field_name}, label={field_label})"
    )


def _extract_fields(describe_payload: dict[str, Any]) -> list[dict[str, Any]]:
    if isinstance(describe_payload.get("result"), dict):
        raw_fields = describe_payload.get("result", {}).get("fields") or []
    else:
        raw_fields = describe_payload.get("fields") or []
    return [field for field in raw_fields if isinstance(field, dict)]


def _split_object_field(value: str) -> tuple[str, str]:
    object_name, _, field_name = str(value or "").partition(".")
    return object_name.strip(), field_name.strip()


def _safe_profile_key(crm_org_id: str, org_alias: str | None) -> str:
    alias = str(org_alias or "").strip()
    if alias:
        return f"{crm_org_id}-{alias}".replace(" ", "-")
    return crm_org_id.replace(" ", "-")


def _adapter_identity(detected_engine: str) -> tuple[str, str]:
    if detected_engine == "salesforce_cpq":
        return "salesforce_cpq", "salesforce"
    if detected_engine == "standard_quotes":
        return "salesforce_standard_quotes", "salesforce"
    if detected_engine == "mixed":
        return "salesforce_mixed_quote_stack", "salesforce"
    if detected_engine == "third_party":
        return "third_party_quote_stack", "third_party"
    return "unknown_quote_stack", "unknown"


__all__ = [
    "build_quote_adapter_capability_profile",
    "build_dealdesk_config_draft",
    "build_quote_environment_profile",
    "discover_quote_object_profiles",
]
