"""Forms → canonical CRM contact upsert. Not a CRM runtime.

Maps accepted form values onto `crm.contact.upsert`. Identity, resolver,
duplicates, and provider adapters stay in the CRM domain.
"""
from __future__ import annotations

from collections.abc import Mapping
from typing import Any

from pydantic import BaseModel, ConfigDict, StrictBool, StrictStr

from src.domain.connectors.crm_canonical import (
    CANONICAL_FIELDS,
    CRMCanonicalError,
    CRMContactInput,
    assert_property_map,
    dump_crm_input,
)
from src.domain.forms.actions import FormPostSubmitAction
from src.domain.forms.canonical import (
    STRING_FIELD_TYPES,
    FormDefinition,
    FormFileRef,
    parse_form_file_ref,
)

GENERIC_CRM_ACTION = "crm.contact.upsert"
UPSERT_MATCH_BY = "email"
NO_EMAIL_STATUS = "skipped"
NO_EMAIL_CODE = "crm_identity_missing"
NAME_SPLITTING = False
CONTACT_NAME_COMPAT = "omitted_until_explicit_mapping"

CANONICAL_CONTACT_TARGETS: frozenset[str] = frozenset(CANONICAL_FIELDS["contact"])
PROPERTY_PREFIX = "properties."

CONTACT_COMPATIBLE_CRM_MAPPING: dict[str, str] = {
    "email": "email",
    "phone": "phone",
}

FORMS_CRM_COMPOSITION: dict[str, Any] = {
    "slice_id": "7i_crm_composition",
    "status": "frozen",
    "generic_action": GENERIC_CRM_ACTION,
    "upsert_match_by": UPSERT_MATCH_BY,
    "no_email": NO_EMAIL_CODE,
    "no_email_status": NO_EMAIL_STATUS,
    "no_create_fallback": True,
    "name_splitting": NAME_SPLITTING,
    "contact_name_compat": CONTACT_NAME_COMPAT,
    "resolver_in_forms": False,
    "provider_lead_object": False,
    "company_auto_create": False,
    "deal_auto_create": False,
    "association_auto_create": False,
    "durable_crm_ref": False,
    "crm_state_on_forms_store": False,
    "inbound_events": False,
    "public_bff": False,
    "form_submit_capability": False,
    "codegen": False,
    "app_block_migrated": False,
    "docs": "docs/audits/FORMS_CRM_COMPOSITION_7I.md",
}


class CrmContactUpsertBuild(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)
    skipped: StrictBool
    payload: dict[str, Any] | None = None
    error_code: StrictStr | None = None


def crm_contact_upsert_action(
    *,
    key: str = "sync_crm",
    mapping: Mapping[str, str] | None = None,
    optional: bool = True,
) -> FormPostSubmitAction:
    return FormPostSubmitAction(
        key=key,
        action_type=GENERIC_CRM_ACTION,
        mapping=dict(mapping or CONTACT_COMPATIBLE_CRM_MAPPING),
        optional=optional,
    )


def _parse_target(target: str) -> tuple[str, str] | str:
    if target in CANONICAL_CONTACT_TARGETS:
        return ("canonical", target)
    if target.startswith(PROPERTY_PREFIX):
        name = target[len(PROPERTY_PREFIX) :]
        if not name or "." in name:
            return "unknown_crm_target"
        if name in CANONICAL_CONTACT_TARGETS:
            return "canonical_property_collision"
        return ("property", name)
    return "unknown_crm_target"


def _source_field(definition: FormDefinition, key: str):
    for field in definition.fields:
        if field.key == key:
            return field
    return None


def validate_crm_contact_mapping(
    action: FormPostSubmitAction,
    definition: FormDefinition,
) -> tuple[str, ...]:
    if action.action_type != GENERIC_CRM_ACTION or action.provider_specific:
        return ()
    errors: list[str] = []
    if action.constants:
        errors.append("unexpected_crm_constant")
    seen_targets: set[str] = set()
    properties: dict[str, Any] = {}
    for target, source in action.mapping.items():
        parsed = _parse_target(target)
        if isinstance(parsed, str):
            errors.append(parsed)
            continue
        kind, name = parsed
        if name in seen_targets:
            errors.append("duplicate_crm_target")
            continue
        seen_targets.add(name)
        field = _source_field(definition, source)
        if field is None:
            errors.append("unknown_source_field")
            continue
        if field.type == "file":
            errors.append("unsupported_crm_file_mapping")
            continue
        if kind == "canonical" and field.type not in STRING_FIELD_TYPES:
            errors.append("unsupported_crm_source_type")
            continue
        if kind == "property":
            if field.type == "multi_select":
                errors.append("unsupported_crm_source_type")
                continue
            properties[name] = None
    try:
        assert_property_map(properties or None, object_type="contact")
    except CRMCanonicalError as exc:
        errors.append(exc.code)
    return tuple(dict.fromkeys(errors))


def _is_file_value(value: Any) -> bool:
    if isinstance(value, FormFileRef):
        return True
    return parse_form_file_ref(value) is not None


def build_crm_contact_upsert_payload(
    action: FormPostSubmitAction,
    values: Mapping[str, Any],
) -> CrmContactUpsertBuild:
    """Map accepted values to `{contact: CRMContactInput dump}`. No resolver."""
    canonical: dict[str, Any] = {}
    properties: dict[str, Any] = {}
    for target, source in action.mapping.items():
        parsed = _parse_target(target)
        if isinstance(parsed, str) or source not in values:
            continue
        value = values[source]
        if _is_file_value(value):
            return CrmContactUpsertBuild(
                skipped=False,
                error_code="unsupported_crm_file_mapping",
            )
        kind, name = parsed
        if kind == "canonical":
            canonical[name] = value
        else:
            properties[name] = value
    if properties:
        canonical["properties"] = properties
    email = canonical.get("email")
    if not isinstance(email, str) or not email.strip():
        return CrmContactUpsertBuild(
            skipped=True,
            error_code=NO_EMAIL_CODE,
        )
    try:
        contact = CRMContactInput(**canonical)
    except (CRMCanonicalError, ValueError):
        return CrmContactUpsertBuild(
            skipped=False,
            error_code="crm_validation_failed",
        )
    return CrmContactUpsertBuild(
        skipped=False,
        payload={"contact": dump_crm_input(contact)},
    )
