"""crm.contact.* Core resolver and internal plans. Public catalog truth stays 6M.

Eligible providers: customer HubSpot Contact and Salesforce Contact only.
Never Cloud lead / crm_contact / contact_submission, never Salesforce Lead.
"""
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 (
    CRMCanonicalError,
    CRMContactInput,
    CRMObjectRef,
    dump_crm_input,
)
from src.domain.connectors.crm_resolve import (
    CRM_PROVIDERS,
    HUBSPOT_NULL_CLEAR,
    CRMConnectionSnapshot,
    CRMPlan,
    CRMResolution,
    DispatchAuthorization,
    decode_crm_cursor,
    dispatch_crm_plan,
    encode_crm_cursor,
    enqueue_output,
    fail_crm,
    map_enqueue_error,
    select_crm_connection,
)

CRMContactPlan = CRMPlan
CRMContactResolution = CRMResolution

CRM_CONTACT_FROZEN_VERBS = (
    "crm.contact.create",
    "crm.contact.get",
    "crm.contact.update",
    "crm.contact.upsert",
    "crm.contact.search",
)
HUBSPOT_CONTACT_FIELDS = {
    "email": "email",
    "first_name": "firstname",
    "last_name": "lastname",
    "phone": "phone",
}
SALESFORCE_CONTACT_FIELDS = {
    "email": "Email",
    "first_name": "FirstName",
    "last_name": "LastName",
    "phone": "Phone",
}
ACTION_BY_CAPABILITY: dict[str, dict[str, str]] = {
    "crm.contact.create": {
        "hubspot": "hubspot.create_contact",
        "salesforce": "salesforce.create_contact",
    },
    "crm.contact.get": {
        "hubspot": "hubspot.get_contact",
        "salesforce": "salesforce.get_contact",
    },
    "crm.contact.update": {
        "hubspot": "hubspot.update_contact",
        "salesforce": "salesforce.update_contact",
    },
    "crm.contact.upsert": {
        "hubspot": "hubspot.upsert_contact",
        "salesforce": "salesforce.upsert_contact",
    },
    "crm.contact.search": {
        "hubspot": "hubspot.search_contacts",
        "salesforce": "salesforce.search_contacts",
    },
}
@dataclass(frozen=True)
class CRMContactPublicResult:
    capability: str
    status: str = "rejected"
    contact: dict[str, Any] | None = None
    items: list[dict[str, Any]] | None = None
    next_cursor: str | 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.contact is not None:
            body["contact"] = dict(self.contact)
        if self.items is not None:
            body["items"] = list(self.items)
        if self.next_cursor is not None:
            body["next_cursor"] = self.next_cursor
        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 _contact_input(raw: Any) -> CRMContactInput | str:
    if not isinstance(raw, dict):
        return "crm_validation_failed"
    try:
        return CRMContactInput.model_validate(raw)
    except (ValidationError, CRMCanonicalError):
        return "crm_property_invalid"


def _contact_ref(raw: Any) -> CRMObjectRef | str:
    if isinstance(raw, str):
        return "crm_validation_failed"
    if not isinstance(raw, dict):
        return "crm_validation_failed"
    if raw.get("object_type") != "contact":
        return "crm_validation_failed"
    try:
        return CRMObjectRef.model_validate(raw)
    except ValidationError:
        return "crm_validation_failed"


def map_contact_properties(dumped: dict[str, Any], provider: str) -> dict[str, Any]:
    names = HUBSPOT_CONTACT_FIELDS if provider == "hubspot" else SALESFORCE_CONTACT_FIELDS
    bag: dict[str, Any] = {}
    for canonical, wire in names.items():
        if canonical == "email":
            continue
        if canonical in dumped:
            bag[wire] = dumped[canonical]
    extra = dumped.get("properties")
    if isinstance(extra, dict):
        bag.update(extra)
    return bag


def map_contact_capability_input(
    capability: str, provider: str, payload: dict[str, Any]
) -> dict[str, Any] | str:
    if provider not in CRM_PROVIDERS:
        return "crm_unbound"
    if capability in {"crm.contact.create", "crm.contact.upsert"}:
        parsed = _contact_input(payload.get("contact"))
        if isinstance(parsed, str):
            return parsed
        dumped = dump_crm_input(parsed)
        if capability == "crm.contact.upsert":
            email = dumped.get("email")
            if not isinstance(email, str) or not email.strip():
                return "crm_validation_failed"
        props = map_contact_properties(dumped, provider)
        if provider == "hubspot":
            mapped: dict[str, Any] = {"properties": props}
            if "email" in dumped:
                mapped["email"] = dumped["email"]
            return mapped
        fields = dict(props)
        mapped = {"fields": fields}
        if "email" in dumped:
            mapped["email"] = dumped["email"]
            fields["Email"] = dumped["email"]
        return mapped
    if capability == "crm.contact.get":
        parsed_ref = _contact_ref(payload.get("contact"))
        if isinstance(parsed_ref, str):
            return parsed_ref
        if provider == "hubspot":
            return {
                "contact_id": parsed_ref.external_id,
                "properties": ["email", "firstname", "lastname", "phone"],
            }
        return {
            "contact_id": parsed_ref.external_id,
            "fields": ["Id", "Email", "FirstName", "LastName", "Phone"],
        }
    if capability == "crm.contact.update":
        parsed_ref = _contact_ref(payload.get("contact"))
        if isinstance(parsed_ref, str):
            return parsed_ref
        parsed = _contact_input(payload.get("changes") or {})
        if isinstance(parsed, str):
            return parsed
        dumped = dump_crm_input(parsed)
        props = map_contact_properties(dumped, provider)
        if "email" in dumped:
            if provider == "hubspot":
                props["email"] = dumped["email"]
            else:
                props["Email"] = dumped["email"]
        if provider == "hubspot":
            return {
                "contact_id": parsed_ref.external_id,
                "properties": props,
            }
        return {"contact_id": parsed_ref.external_id, "fields": props}
    if capability == "crm.contact.search":
        email = payload.get("email")
        if not isinstance(email, str) or not email.strip():
            return "crm_validation_failed"
        if payload.keys() - {"email", "cursor", "metadata", "provider_hint"}:
            return "crm_validation_failed"
        if provider == "hubspot":
            mapped = {
                "filter_groups": [
                    {
                        "filters": [
                            {
                                "propertyName": "email",
                                "operator": "EQ",
                                "value": email.strip(),
                            }
                        ]
                    }
                ],
                "properties": ["email", "firstname", "lastname", "phone"],
                "limit": 10,
            }
            cursor = payload.get("cursor")
            if isinstance(cursor, str) and cursor.strip():
                after = decode_crm_cursor("hubspot", cursor.strip())
                if after is None:
                    return "crm_validation_failed"
                mapped["after"] = after
            return mapped
        if payload.get("cursor"):
            return "crm_validation_failed"
        return {
            "filters": {"Email": email.strip()},
            "fields": ["Id", "Email", "FirstName", "LastName", "Phone"],
            "limit": 10,
        }
    return "crm_validation_failed"


def normalize_provider_contact(provider: str, raw: dict[str, Any]) -> dict[str, Any] | None:
    if provider == "hubspot":
        external = raw.get("id")
        props = raw.get("properties") if isinstance(raw.get("properties"), dict) else {}
        if not isinstance(external, str) or not external:
            return None
        return {
            "ref": {"object_type": "contact", "external_id": str(external)},
            "email": props.get("email"),
            "first_name": props.get("firstname"),
            "last_name": props.get("lastname"),
            "phone": props.get("phone"),
        }
    external = raw.get("Id") or raw.get("id")
    if not isinstance(external, str) or not external:
        return None
    return {
        "ref": {"object_type": "contact", "external_id": str(external)},
        "email": raw.get("Email"),
        "first_name": raw.get("FirstName"),
        "last_name": raw.get("LastName"),
        "phone": raw.get("Phone"),
    }


def resolve_crm_contact(
    *,
    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_CONTACT_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 {
            "contact_id",
            "Id",
            "filter_groups",
            "query",
            "soql",
            "after",
            "nextRecordsUrl",
            "queryLocator",
            "external_id_field",
            "lead_id",
        }
    )
    if leaked:
        return _fail(
            "provider_fields_forbidden",
            "crm.contact.* does not accept provider API fields",
            {"forbidden_fields": leaked},
        )

    selected = select_crm_connection(
        capability=capability,
        connections=connections,
        action_by_capability=ACTION_BY_CAPABILITY,
        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_contact_capability_input(
        capability, selected.plan.provider, payload
    )
    if isinstance(mapped, str):
        return _fail(mapped, f"{capability} input is invalid", selected.diagnostics)
    notes = {
        **selected.diagnostics,
        "execution_domain": "C",
        "salesforce_lead_excluded": True,
        "hubspot_null_clear": (
            HUBSPOT_NULL_CLEAR if selected.plan.provider == "hubspot" else None
        ),
    }
    return CRMResolution(
        ok=True,
        plan=CRMPlan(
            capability=capability,
            connection_id=selected.plan.connection_id,
            provider=selected.plan.provider,
            action=selected.plan.action,
            resolved_input=mapped,
            notes={k: v for k, v in notes.items() if v is not None},
        ),
        diagnostics=notes,
    )


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


def to_public_crm_contact_result(
    resolution: CRMContactResolution,
    *,
    capability: str,
    enqueue: dict[str, Any] | None = None,
) -> CRMContactPublicResult:
    if not resolution.ok:
        return CRMContactPublicResult(
            capability=capability,
            status="rejected",
            reason_code=resolution.reason_code,
            message=resolution.message,
        )
    if enqueue is None:
        return CRMContactPublicResult(
            capability=capability,
            status="rejected",
            reason_code="crm_provider_unavailable",
            message="crm.contact.* has no enqueue path",
        )
    if not enqueue.get("ok"):
        code = str(enqueue.get("reason_code") or "crm_provider_unavailable")
        return CRMContactPublicResult(
            capability=capability,
            status="rejected",
            reason_code=map_enqueue_error(code, capability=capability),
            message=str(enqueue.get("message") or "integration.execute enqueue failed"),
        )
    output = enqueue_output(enqueue)
    if output is None:
        return CRMContactPublicResult(capability=capability, status="queued")
    if output.get("already_exists") is True and capability == "crm.contact.create":
        return CRMContactPublicResult(
            capability=capability,
            status="rejected",
            reason_code="crm_duplicate_match",
            message="Contact already exists; create does not upsert",
        )
    provider = resolution.plan.provider if resolution.plan else ""
    if capability == "crm.contact.search":
        rows = output.get("results") or output.get("records") or []
        items = []
        if isinstance(rows, list):
            for row in rows:
                if isinstance(row, dict):
                    item = normalize_provider_contact(provider, row)
                    if item:
                        items.append(item)
        paging = output.get("paging")
        cursor = None
        if isinstance(paging, dict):
            nxt = paging.get("next")
            if isinstance(nxt, dict):
                cursor = nxt.get("after")
        opaque = (
            encode_crm_cursor(provider, str(cursor))
            if cursor and provider == "hubspot"
            else None
        )
        return CRMContactPublicResult(
            capability=capability,
            status="succeeded",
            items=items,
            next_cursor=opaque,
        )
    contact = normalize_provider_contact(provider, output)
    if contact is None:
        return CRMContactPublicResult(
            capability=capability,
            status="rejected",
            reason_code="crm_contact_not_found",
            message="Provider result did not include a contact identity",
        )
    return CRMContactPublicResult(
        capability=capability, status="succeeded", contact=contact
    )
