"""crm.association.create. Public catalog truth stays 6M.

Canonical directions only:
  contact → company  (primary company relationship)
  deal → company     (primary company relationship)

Contact↔Deal is PROVIDER_SPECIFIC_FOR_NOW (OpportunityContactRole ≠ HubSpot link).
HubSpot association_type_id and Salesforce AccountId stay in provider payloads.
"""
from __future__ import annotations

from dataclasses import dataclass
from typing import Any
from uuid import UUID

from pydantic import ValidationError

from src.domain.connectors.crm_canonical import CRMAssociation
from src.domain.connectors.crm_resolve import (
    CRMConnectionSnapshot,
    CRMPlan,
    CRMResolution,
    DispatchAuthorization,
    dispatch_crm_plan,
    fail_crm,
    map_enqueue_error,
    select_crm_connection,
)

CRM_ASSOCIATION_FROZEN_VERBS = ("crm.association.create",)
CONTACT_DEAL_CLASSIFICATION = "PROVIDER_SPECIFIC_FOR_NOW"
# HubSpot v3/v4 HUBSPOT_DEFINED defaults for the primary company relationship.
# Never returned on the public capability contract.
HUBSPOT_DEFAULT_TYPE_ID = {
    "contact_company": 1,
    "deal_company": 5,
}
HUBSPOT_OBJECT_PLURAL = {
    "contact": "contacts",
    "company": "companies",
    "deal": "deals",
}
SUPPORTED_KINDS: dict[tuple[str, str], str] = {
    ("contact", "company"): "contact_company",
    ("deal", "company"): "deal_company",
}
ACTION_BY_KIND: dict[str, dict[str, str]] = {
    "contact_company": {
        "hubspot": "hubspot.associate_objects",
        "salesforce": "salesforce.update_contact",
    },
    "deal_company": {
        "hubspot": "hubspot.associate_objects",
        "salesforce": "salesforce.update_opportunity",
    },
}
ACTION_BY_CAPABILITY: dict[str, dict[str, str]] = {
    "crm.association.create": {
        "hubspot": "hubspot.associate_objects",
        "salesforce": "salesforce.update_contact",
    }
}
PROVIDER_FIELD_KEYS = frozenset(
    {
        "association_type_id",
        "association_category",
        "association_id",
        "AccountId",
        "lookup_field",
        "WhoId",
        "WhatId",
        "OpportunityContactRole",
        "from_object_type",
        "to_object_type",
        "contact_id",
        "company_id",
        "deal_id",
        "opportunity_id",
    }
)


@dataclass(frozen=True)
class CRMAssociationPublicResult:
    capability: str
    status: str = "rejected"
    association: dict[str, Any] | None = None
    reason_code: str | None = None
    message: str | None = None

    def as_dict(self) -> dict[str, Any]:
        body: dict[str, Any] = {
            "capability": self.capability,
            "status": self.status,
        }
        if self.association is not None:
            body["association"] = dict(self.association)
        if self.reason_code is not None:
            body["reason_code"] = self.reason_code
        if self.message is not None:
            body["message"] = self.message
        return body


def _fail(
    code: str, message: str, diagnostics: dict[str, Any] | None = None
) -> CRMResolution:
    return fail_crm(code, message, diagnostics)


def _catalog_invocable(capability: str) -> bool:
    from src.domain.connectors.invoke_capabilities import capability_surface

    return bool(capability_surface(capability, []).get("invocable"))


def parse_association(raw: Any) -> CRMAssociation | str:
    if not isinstance(raw, dict):
        return "crm_association_invalid"
    try:
        return CRMAssociation.model_validate(raw)
    except ValidationError:
        return "crm_association_invalid"


def classify_relationship(association: CRMAssociation) -> str | None:
    pair = (association.from_ref.object_type, association.to.object_type)
    return SUPPORTED_KINDS.get(pair)


def association_echo(association: CRMAssociation) -> dict[str, Any]:
    return {
        "from": {
            "object_type": association.from_ref.object_type,
            "external_id": association.from_ref.external_id,
        },
        "to": {
            "object_type": association.to.object_type,
            "external_id": association.to.external_id,
        },
    }


def map_association_input(
    kind: str, provider: str, association: CRMAssociation
) -> dict[str, Any] | str:
    if kind not in ACTION_BY_KIND:
        return "crm_association_unsupported"
    source = association.from_ref.external_id
    target = association.to.external_id
    if provider == "hubspot":
        return {
            "from_object_type": HUBSPOT_OBJECT_PLURAL[association.from_ref.object_type],
            "from_object_id": source,
            "to_object_type": HUBSPOT_OBJECT_PLURAL[association.to.object_type],
            "to_object_id": target,
            "association_type_id": HUBSPOT_DEFAULT_TYPE_ID[kind],
            "association_category": "HUBSPOT_DEFINED",
        }
    if provider == "salesforce" and kind == "contact_company":
        return {"contact_id": source, "fields": {"AccountId": target}}
    if provider == "salesforce" and kind == "deal_company":
        return {"opportunity_id": source, "fields": {"AccountId": target}}
    return "crm_association_unsupported"


def actions_for_kind(kind: str) -> dict[str, dict[str, str]]:
    return {"crm.association.create": ACTION_BY_KIND[kind]}


def resolve_crm_association(
    *,
    capability: str,
    payload: dict[str, Any],
    connections: list[CRMConnectionSnapshot],
    enabled_capabilities: set[str] | None = None,
    provider_hint: str | None = None,
    binding_connection_id: UUID | None = None,
    approval_present: bool = False,
    enforce_invocable: bool = False,
) -> CRMResolution:
    if capability not in CRM_ASSOCIATION_FROZEN_VERBS:
        return _fail("capability_not_bound", f"Unknown capability {capability}")
    if enforce_invocable and not _catalog_invocable(capability):
        from src.domain.connectors.invoke_capabilities import INVOCABLE_FALSE_MEANS

        return _fail("capability_not_invocable", INVOCABLE_FALSE_MEANS)

    leaked = sorted(key for key in payload if key in PROVIDER_FIELD_KEYS)
    if leaked:
        return _fail(
            "provider_fields_forbidden",
            "crm.association.create does not accept provider relationship fields",
            {"forbidden_fields": leaked},
        )

    parsed = parse_association(payload.get("association"))
    if isinstance(parsed, str):
        return _fail(parsed, "association.from and association.to are required")

    pair = (parsed.from_ref.object_type, parsed.to.object_type)
    kind = classify_relationship(parsed)
    if kind is None:
        extras: dict[str, Any] = {}
        if pair in {("contact", "deal"), ("deal", "contact")}:
            extras = {
                "classification": CONTACT_DEAL_CLASSIFICATION,
                "reason": "Salesforce OpportunityContactRole is not a bare association",
            }
        return _fail(
            "crm_association_unsupported",
            "Canonical association supports only contact→company and deal→company",
            {"pair": list(pair), **extras},
        )

    selected = select_crm_connection(
        capability=capability,
        connections=connections,
        action_by_capability=actions_for_kind(kind),
        enabled_capabilities=enabled_capabilities,
        provider_hint=provider_hint,
        binding_connection_id=binding_connection_id,
        approval_present=approval_present,
    )
    if not selected.ok or selected.plan is None:
        return selected
    mapped = map_association_input(kind, selected.plan.provider, parsed)
    if isinstance(mapped, str):
        return _fail(mapped, "Association mapping failed", selected.diagnostics)
    notes = {
        **selected.diagnostics,
        "execution_domain": "C",
        "relationship_kind": kind,
        "canonical_direction": f"{pair[0]}->{pair[1]}",
        "cardinality": "primary_company_relationship",
        "ensure_exists": True,
        "salesforce_account_id_replaces_primary": selected.plan.provider == "salesforce",
        "hubspot_association_is_additive": selected.plan.provider == "hubspot",
        "echo": association_echo(parsed),
    }
    return CRMResolution(
        ok=True,
        plan=CRMPlan(
            capability=capability,
            connection_id=selected.plan.connection_id,
            provider=selected.plan.provider,
            action=ACTION_BY_KIND[kind][selected.plan.provider],
            resolved_input=mapped,
            notes=notes,
        ),
        diagnostics=notes,
    )


def dispatch_crm_association_plan(
    plan: CRMPlan,
    authorization: DispatchAuthorization,
    *,
    diagnostics: dict[str, Any] | None = None,
) -> CRMResolution:
    return dispatch_crm_plan(plan, authorization, diagnostics=diagnostics)


def to_public_crm_association_result(
    resolution: CRMResolution,
    *,
    capability: str,
    enqueue: dict[str, Any] | None = None,
) -> CRMAssociationPublicResult:
    if not resolution.ok:
        return CRMAssociationPublicResult(
            capability=capability,
            status="rejected",
            reason_code=resolution.reason_code,
            message=resolution.message,
        )
    if enqueue is None:
        return CRMAssociationPublicResult(
            capability=capability,
            status="rejected",
            reason_code="crm_provider_unavailable",
            message="crm.association.create has no enqueue path",
        )
    if not enqueue.get("ok"):
        code = str(enqueue.get("reason_code") or "crm_provider_unavailable")
        return CRMAssociationPublicResult(
            capability=capability,
            status="rejected",
            reason_code=map_enqueue_error(code, capability=capability),
            message=str(enqueue.get("message") or "integration.execute enqueue failed"),
        )
    echo = None
    if resolution.plan and isinstance(resolution.plan.notes.get("echo"), dict):
        echo = dict(resolution.plan.notes["echo"])
    output = None
    for key in ("result", "output"):
        value = enqueue.get(key)
        if isinstance(value, dict) and value:
            output = value
            break
    if output is None:
        return CRMAssociationPublicResult(capability=capability, status="queued")
    return CRMAssociationPublicResult(
        capability=capability,
        status="succeeded",
        association=echo,
    )
