"""crm.deal.create / get / update / search. Public catalog truth stays 6M.

CRMDeal → HubSpot Deal / Salesforce Opportunity.
Salesforce pipeline is not RecordType/sales process; it is rejected.
Search is exact Name only. Upsert stays provider-specific.
"""
from __future__ import annotations

from dataclasses import dataclass
from datetime import UTC, datetime
from typing import Any
from uuid import UUID

from pydantic import ValidationError

from src.domain.connectors.crm_canonical import (
    CRMCanonicalError,
    CRMDealInput,
    CRMObjectRef,
    dump_crm_input,
)
from src.domain.connectors.crm_resolve import (
    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,
)
from src.domain.connectors.crm_resources import (
    CRMResourceError,
    pipeline_map_from_config,
    resolve_alias,
    reverse_alias,
    stage_map_from_config,
)

CRM_DEAL_FROZEN_VERBS = (
    "crm.deal.create",
    "crm.deal.get",
    "crm.deal.update",
    "crm.deal.search",
)
CRM_DEAL_UPSERT_CLASSIFICATION = "PROVIDER_SPECIFIC_FOR_NOW"
HUBSPOT_DEAL_FIELDS = {
    "name": "dealname",
    "amount": "amount",
    "close_date": "closedate",
    "pipeline": "pipeline",
    "stage": "dealstage",
}
SALESFORCE_DEAL_FIELDS = {
    "name": "Name",
    "amount": "Amount",
    "close_date": "CloseDate",
    "stage": "StageName",
}
ACTION_BY_CAPABILITY: dict[str, dict[str, str]] = {
    "crm.deal.create": {
        "hubspot": "hubspot.create_deal",
        "salesforce": "salesforce.create_opportunity",
    },
    "crm.deal.get": {
        "hubspot": "hubspot.get_deal",
        "salesforce": "salesforce.get_opportunity",
    },
    "crm.deal.update": {
        "hubspot": "hubspot.update_deal",
        "salesforce": "salesforce.update_opportunity",
    },
    "crm.deal.search": {
        "hubspot": "hubspot.search_deals",
        "salesforce": "salesforce.search_opportunities",
    },
}


@dataclass(frozen=True)
class CRMDealPublicResult:
    capability: str
    status: str = "rejected"
    deal: 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.deal is not None:
            body["deal"] = dict(self.deal)
        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 _deal_input(raw: Any) -> CRMDealInput | str:
    if not isinstance(raw, dict):
        return "crm_validation_failed"
    try:
        return CRMDealInput.model_validate(raw)
    except (ValidationError, CRMCanonicalError):
        return "crm_property_invalid"


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


def map_deal_properties(
    dumped: dict[str, Any],
    provider: str,
    config: dict[str, Any] | None = None,
) -> dict[str, Any] | str:
    if "pipeline" in dumped:
        if provider == "salesforce":
            return "crm_pipeline_unsupported"
    names = HUBSPOT_DEAL_FIELDS if provider == "hubspot" else SALESFORCE_DEAL_FIELDS
    bag: dict[str, Any] = {}
    stages = stage_map_from_config(config)
    pipelines = pipeline_map_from_config(config)
    for canonical, wire in names.items():
        if canonical not in dumped:
            continue
        value = dumped[canonical]
        if value is None:
            bag[wire] = None
            continue
        if canonical == "stage":
            try:
                bag[wire] = resolve_alias(
                    str(value), stages, unbound_code="crm_stage_unbound"
                )
            except CRMResourceError as exc:
                return exc.code
            continue
        if canonical == "pipeline":
            try:
                bag[wire] = resolve_alias(
                    str(value), pipelines, unbound_code="crm_pipeline_unbound"
                )
            except CRMResourceError as exc:
                return exc.code
            continue
        bag[wire] = value
    extra = dumped.get("properties")
    if isinstance(extra, dict):
        bag.update(extra)
    return bag


def map_deal_capability_input(
    capability: str,
    provider: str,
    payload: dict[str, Any],
    config: dict[str, Any] | None = None,
) -> dict[str, Any] | str:
    if capability == "crm.deal.create":
        parsed = _deal_input(payload.get("deal"))
        if isinstance(parsed, str):
            return parsed
        dumped = dump_crm_input(parsed)
        props = map_deal_properties(dumped, provider, config)
        if isinstance(props, str):
            return props
        if provider == "hubspot":
            return {"properties": props}
        return {"fields": props}
    if capability == "crm.deal.update":
        parsed_ref = _deal_ref(payload.get("deal"))
        if isinstance(parsed_ref, str):
            return parsed_ref
        parsed = _deal_input(payload.get("changes") or {})
        if isinstance(parsed, str):
            return parsed
        dumped = dump_crm_input(parsed)
        props = map_deal_properties(dumped, provider, config)
        if isinstance(props, str):
            return props
        if provider == "hubspot":
            return {"deal_id": parsed_ref.external_id, "properties": props}
        return {"opportunity_id": parsed_ref.external_id, "fields": props}
    if capability == "crm.deal.get":
        parsed_ref = _deal_ref(payload.get("deal"))
        if isinstance(parsed_ref, str):
            return parsed_ref
        if provider == "hubspot":
            return {
                "deal_id": parsed_ref.external_id,
                "properties": [
                    "dealname",
                    "amount",
                    "closedate",
                    "pipeline",
                    "dealstage",
                ],
            }
        return {
            "opportunity_id": parsed_ref.external_id,
            "fields": ["Id", "Name", "Amount", "CloseDate", "StageName"],
        }
    if capability == "crm.deal.search":
        if any(
            key in payload
            for key in (
                "pipeline",
                "dealstage",
                "StageName",
                "RecordTypeId",
            )
        ):
            if "pipeline" in payload:
                return "crm_pipeline_unsupported"
            return "crm_field_unsupported"
        name = payload.get("name")
        stage = payload.get("stage")
        if name is not None and (not isinstance(name, str) or not name.strip()):
            return "crm_validation_failed"
        if stage is not None and (not isinstance(stage, str) or not stage.strip()):
            return "crm_validation_failed"
        if not (isinstance(name, str) and name.strip()) and not (
            isinstance(stage, str) and stage.strip()
        ):
            return "crm_validation_failed"
        extra = payload.keys() - {
            "name",
            "stage",
            "cursor",
            "metadata",
            "provider_hint",
        }
        if extra:
            return "crm_validation_failed"
        stage_value = None
        if isinstance(stage, str) and stage.strip():
            try:
                stage_value = resolve_alias(
                    stage.strip(),
                    stage_map_from_config(config),
                    unbound_code="crm_stage_unbound",
                )
            except CRMResourceError as exc:
                return exc.code
        if provider == "hubspot":
            filters = []
            if isinstance(name, str) and name.strip():
                filters.append(
                    {
                        "propertyName": "dealname",
                        "operator": "EQ",
                        "value": name.strip(),
                    }
                )
            if stage_value is not None:
                filters.append(
                    {
                        "propertyName": "dealstage",
                        "operator": "EQ",
                        "value": stage_value,
                    }
                )
            mapped: dict[str, Any] = {
                "filter_groups": [{"filters": filters}],
                "properties": [
                    "dealname",
                    "amount",
                    "closedate",
                    "pipeline",
                    "dealstage",
                ],
                "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"
        filters_sf: dict[str, str] = {}
        if isinstance(name, str) and name.strip():
            filters_sf["Name"] = name.strip()
        if stage_value is not None:
            filters_sf["StageName"] = stage_value
        return {
            "filters": filters_sf,
            "fields": ["Id", "Name", "Amount", "CloseDate", "StageName"],
            "limit": 10,
        }
    return "crm_validation_failed"


def _canonical_amount(value: Any) -> int | float | None:
    if value is None or value == "":
        return None
    if isinstance(value, bool):
        return None
    if isinstance(value, int):
        return value
    if isinstance(value, float):
        return value
    if isinstance(value, str):
        try:
            return int(value) if value.isdigit() else float(value)
        except ValueError:
            return None
    return None


def _canonical_close_date(value: Any) -> str | None:
    if value is None or value == "":
        return None
    if isinstance(value, (int, float)) and not isinstance(value, bool):
        ms = int(value)
        if ms > 10_000_000_000:
            ms = ms // 1000
        return datetime.fromtimestamp(ms, tz=UTC).date().isoformat()
    text = str(value)
    if len(text) >= 10 and text[4] == "-" and text[7] == "-":
        return text[:10]
    return text


def normalize_provider_deal(
    provider: str,
    raw: dict[str, Any],
    *,
    stage_map: dict[str, str] | None = None,
    pipeline_map: dict[str, str] | None = None,
) -> dict[str, Any] | None:
    stages = stage_map or {}
    pipelines = pipeline_map or {}
    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
        body: dict[str, Any] = {
            "ref": {"object_type": "deal", "external_id": str(external)},
            "name": props.get("dealname"),
            "amount": _canonical_amount(props.get("amount")),
            "close_date": _canonical_close_date(props.get("closedate")),
        }
        pipeline = reverse_alias(props.get("pipeline"), pipelines)
        if pipeline is not None:
            body["pipeline"] = pipeline
        stage = reverse_alias(props.get("dealstage"), stages)
        if stage is not None:
            body["stage"] = stage
        return body
    external = raw.get("Id") or raw.get("id")
    if not isinstance(external, str) or not external:
        return None
    body = {
        "ref": {"object_type": "deal", "external_id": str(external)},
        "name": raw.get("Name") or raw.get("name"),
        "amount": _canonical_amount(raw.get("Amount") if "Amount" in raw else raw.get("amount")),
        "close_date": _canonical_close_date(
            raw.get("CloseDate") if "CloseDate" in raw else raw.get("close_date")
        ),
    }
    stage = reverse_alias(raw.get("StageName") or raw.get("stage"), stages)
    if stage is not None:
        body["stage"] = stage
    return body


def resolve_crm_deal(
    *,
    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_DEAL_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 {
            "deal_id",
            "opportunity_id",
            "Id",
            "RecordTypeId",
            "filter_groups",
            "query",
            "soql",
            "after",
            "nextRecordsUrl",
            "queryLocator",
            "external_id_field",
            "lead_id",
        }
    )
    if leaked:
        return _fail(
            "provider_fields_forbidden",
            "crm.deal.* 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
    selected_row = next(
        (
            row
            for row in connections
            if row.connection_id == selected.plan.connection_id
        ),
        None,
    )
    config = selected_row.config if selected_row is not None else {}
    mapped = map_deal_capability_input(
        capability, selected.plan.provider, payload, config
    )
    if isinstance(mapped, str):
        return _fail(mapped, f"{capability} input is invalid", selected.diagnostics)
    notes = {
        **selected.diagnostics,
        "execution_domain": "C",
        "salesforce_lead_excluded": True,
        "salesforce_pipeline_unsupported": True,
        "stage_alias_mode": "logical_connection_scoped",
        "pipeline_alias_mode": "hubspot_only",
        "no_canonical_currency": True,
        "upsert_classification": CRM_DEAL_UPSERT_CLASSIFICATION,
        "stage_map": stage_map_from_config(config),
        "pipeline_map": pipeline_map_from_config(config),
        "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_deal_plan(
    plan: CRMPlan,
    authorization: DispatchAuthorization,
    *,
    diagnostics: dict[str, Any] | None = None,
) -> CRMResolution:
    return dispatch_crm_plan(plan, authorization, diagnostics=diagnostics)


def to_public_crm_deal_result(
    resolution: CRMResolution,
    *,
    capability: str,
    enqueue: dict[str, Any] | None = None,
) -> CRMDealPublicResult:
    if not resolution.ok:
        return CRMDealPublicResult(
            capability=capability,
            status="rejected",
            reason_code=resolution.reason_code,
            message=resolution.message,
        )
    if enqueue is None:
        return CRMDealPublicResult(
            capability=capability,
            status="rejected",
            reason_code="crm_provider_unavailable",
            message="crm.deal.* has no enqueue path",
        )
    if not enqueue.get("ok"):
        code = str(enqueue.get("reason_code") or "crm_provider_unavailable")
        return CRMDealPublicResult(
            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 CRMDealPublicResult(capability=capability, status="queued")
    if output.get("already_exists") is True and capability == "crm.deal.create":
        return CRMDealPublicResult(
            capability=capability,
            status="rejected",
            reason_code="crm_duplicate_match",
            message="Deal already exists; create does not upsert",
        )
    provider = resolution.plan.provider if resolution.plan else ""
    notes = resolution.plan.notes if resolution.plan else {}
    stage_map = notes.get("stage_map") if isinstance(notes.get("stage_map"), dict) else {}
    pipeline_map = (
        notes.get("pipeline_map") if isinstance(notes.get("pipeline_map"), dict) else {}
    )
    if capability == "crm.deal.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_deal(
                        provider,
                        row,
                        stage_map=stage_map,
                        pipeline_map=pipeline_map,
                    )
                    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 CRMDealPublicResult(
            capability=capability,
            status="succeeded",
            items=items,
            next_cursor=opaque,
        )
    deal = normalize_provider_deal(
        provider,
        output,
        stage_map=stage_map,
        pipeline_map=pipeline_map,
    )
    if deal is None:
        return CRMDealPublicResult(
            capability=capability,
            status="rejected",
            reason_code="crm_deal_not_found",
            message="Provider result did not include a deal identity",
        )
    return CRMDealPublicResult(capability=capability, status="succeeded", deal=deal)
