"""6O CRM App Block / Feature Pack composition.

Blocks and packs declare product behavior and depend on catalog `crm.*`
capabilities. They never select HubSpot or Salesforce. Runtime resolution
stays in the CRM resolver; this module does not duplicate binding logic.
"""
from __future__ import annotations

from typing import Any, Literal

from src.domain.connectors.invoke_capabilities import INVOKE_CAPABILITIES, is_implemented
from src.domain.feature_packs.catalog_v2 import CATALOG_OVERLAYS

Classification = Literal["P", "C", "B", "LEGACY", "DUPLICATE", "MISNAMED"]
CouplingClass = Literal["VALID_PROVIDER_SPECIFIC", "MUST_NORMALIZE", "LEGACY", "DEAD"]
Readiness = Literal[
    "ready",
    "available_unconfigured",
    "binding_ambiguous",
    "unavailable",
    "reauthorization_required",
]

CONTACT_FORM_UPSERT_CAPABILITY = "crm.contact.upsert"
LOCAL_LEAD_ENTITY = "lead"
LOCAL_LEAD_STAGES = ("new", "contacted", "qualified", "won", "lost")

# Canonical form → CRMContact. Never firstname / FirstName / hs_object_id / AccountId.
FORM_TO_CRM_CONTACT: dict[str, str] = {
    "email": "email",
    "first_name": "first_name",
    "last_name": "last_name",
    "phone": "phone",
}
FORBIDDEN_PROVIDER_FORM_FIELDS = frozenset(
    {
        "firstname",
        "lastname",
        "FirstName",
        "LastName",
        "hs_object_id",
        "AccountId",
        "dealstage",
        "StageName",
        "portal_id",
        "org_id",
        "instance_url",
        "association_type_id",
    }
)

# Reuse pack missing_configuration_behavior. Do not invent CRM-only modes.
SYNC_POLICY_FROM_MISSING_CONFIG = {
    "disabled": "best_effort",
    "degraded": "best_effort",
    "block_install": "required",
}

CRM_RUNTIME_REASON_UX = {
    "crm_unbound": "Connect/configure a CRM",
    "binding_ambiguous": "Select/configure the CRM binding",
    "reauthorization_required": "Reconnect CRM",
    "crm_duplicate_match": "Multiple matching CRM contacts found",
    "crm_rate_limited": "Try again later / queued retry",
    "crm_provider_unavailable": "CRM temporarily unavailable",
}

SYNC_STATUS_UX = ("Synced", "Pending", "Needs CRM configuration", "Sync failed")

# Historical alias retired by 11E2. The canonical block reuses the same local
# pack; these exported metadata names remain for existing audit consumers.
CRM_PIPELINE_ALIAS_DECISION = "RETIRED_FOR_APP_BLOCK"
CRM_PIPELINE_ALIAS_TARGET = "lead-lifecycle"

CRM_EXTERNAL_SYNC_KIND = "composition_marker"
CRM_EXTERNAL_SYNC_RESOLVES_TO: dict[str, tuple[str, ...]] = {
    "contact-capture": (CONTACT_FORM_UPSERT_CAPABILITY,),
    "lead-lifecycle": (),
    "crm-pipeline": (),
    "salesforce-order-sync": (),
}

INBOUND_CRM_EVENT_COMPOSITION = "DEFER"
INBOUND_DEFER_REASON = (
    "PackWebhookBinding is provider-keyed; Feature Packs have no generic "
    "event-subscription abstraction for crm.contact.created and siblings. "
    "Outbound composition is first-class in 6O; inbound workflow wiring is 6P+."
)

OBJECT_REF_PERSISTENCE = {
    "decision": "GAP_DOCUMENTED",
    "local_id_is_not_external_ref": True,
    "do_not_use": ("job_id", "event_id", "connection_id"),
    "existing_store": (
        "contact_submission.crm_status / crm_sync_error are sync outcome flags, "
        "not CRMObjectRef. document_map aliases Google Docs connections, not "
        "local-record ↔ CRM object identity."
    ),
    "required_shape_if_added_later": (
        "local_entity_id → CRMObjectRef {object_type, external_id} scoped to connection"
    ),
}

ARTIFACTS: dict[str, dict[str, Any]] = {
    "crm.lead": {
        "kind": "app_block",
        "classification": "P",
        "local_product_role": "local lead entity/UI on Cloud `lead` records",
        "external_crm_role": "none by default",
        "decision": "KEEP",
    },
    "lead-lifecycle": {
        "kind": "feature_pack",
        "classification": "P",
        "local_product_role": "local lead lifecycle graph (new/contacted/qualified/won/lost)",
        "external_crm_role": "optional explicit logical stage mapping only; never automatic",
        "decision": "KEEP",
    },
    "forms.contact": {
        "kind": "app_block",
        "classification": "B",
        "local_product_role": "validate + persist contact_submission",
        "external_crm_role": "optional crm.contact.upsert",
        "decision": "COMPOSE",
    },
    "contact-capture": {
        "kind": "feature_pack",
        "classification": "B",
        "local_product_role": "public form persistence and notify",
        "external_crm_role": "optional crm.contact.upsert (marker crm.external_sync expands here)",
        "decision": "COMPOSE",
    },
    "crm.pipeline": {
        "kind": "app_block",
        "classification": "P",
        "local_product_role": "lead-lifecycle App Block; subject is lead",
        "external_crm_role": "none; must not imply HubSpot/Salesforce pipelines",
        "decision": CRM_PIPELINE_ALIAS_DECISION,
        "target": CRM_PIPELINE_ALIAS_TARGET,
    },
    "crm-pipeline": {
        "kind": "feature_pack",
        "classification": "LEGACY",
        "local_product_role": "legacy local crm_contact / crm_deal entities",
        "external_crm_role": "optional crm.external_sync marker; not a new-product pack",
        "decision": "KEEP_LEGACY_NON_SELECTABLE",
    },
    "crm.external_sync": {
        "kind": "composition_marker",
        "classification": "C",
        "local_product_role": "none (not an invoke verb)",
        "external_crm_role": "pack requires/supports external CRM sync",
        "decision": "FORMALIZE",
    },
    "salesforce-order-sync": {
        "kind": "feature_pack",
        "classification": "C",
        "local_product_role": "none",
        "external_crm_role": "explicit Salesforce order sync",
        "decision": "VALID_PROVIDER_SPECIFIC",
    },
}

PROVIDER_COUPLING: tuple[dict[str, str], ...] = (
    {
        "artifact": "seeds/function_library/contact-capture-crm-sync.yaml",
        "class": "MUST_NORMALIZE",
        "note": (
            "Legacy Cloud function branches on hubspot.* vs salesforce.* and "
            "integration.execute with connection_id. New composition invokes "
            "crm.contact.upsert via the public capability BFF."
        ),
    },
    {
        "artifact": "seeds/function_library/hubspot-upsert-contact.yaml",
        "class": "LEGACY",
        "note": "Provider-specific library function; not a generic Block path.",
    },
    {
        "artifact": "alembic crm-pipeline connector_actions",
        "class": "LEGACY",
        "note": (
            "Historical pack seed hubspot.upsert_contact / salesforce.upsert_lead. "
            "connector_actions remain resolved_implementation, not Block config."
        ),
    },
    {
        "artifact": "feature_packs.KNOWN_PROVIDERS hubspot/salesforce → crm.external_sync",
        "class": "VALID_PROVIDER_SPECIFIC",
        "note": (
            "Legacy token classification for requires: [hubspot]. "
            "Generic packs bind crm.contact.upsert."
        ),
    },
    {
        "artifact": "salesforce-order-sync provider_bindings",
        "class": "VALID_PROVIDER_SPECIFIC",
        "note": "Explicit Salesforce product pack.",
    },
    {
        "artifact": "contact-capture / forms.contact provider_bindings.providers",
        "class": "VALID_PROVIDER_SPECIFIC",
        "note": "Eligible connector families for configuration UI, not runtime selection.",
    },
)


def classify_artifact(artifact_id: str) -> dict[str, Any]:
    row = ARTIFACTS.get(artifact_id)
    if row is None:
        raise KeyError(artifact_id)
    return dict(row)


def expand_external_sync(pack_slug: str) -> tuple[str, ...]:
    return CRM_EXTERNAL_SYNC_RESOLVES_TO.get(pack_slug, ())


def contact_form_crm_capability(*, enabled: bool, has_email: bool) -> str | None:
    """Form capture defaults to upsert-by-email. No invented identity strategy."""
    if not enabled:
        return None
    if not has_email:
        return None
    return CONTACT_FORM_UPSERT_CAPABILITY


def map_form_fields_to_crm_contact(fields: dict[str, Any]) -> dict[str, Any]:
    leak = FORBIDDEN_PROVIDER_FORM_FIELDS.intersection(fields)
    if leak:
        raise ValueError(f"provider form fields are not allowed: {sorted(leak)}")
    contact: dict[str, Any] = {}
    for source, dest in FORM_TO_CRM_CONTACT.items():
        value = fields.get(source)
        if value is not None and value != "":
            contact[dest] = value
    extra = {
        key: value
        for key, value in fields.items()
        if key not in FORM_TO_CRM_CONTACT
        and key not in {"name", "message", "status"}
        and value not in (None, "")
    }
    if extra:
        contact["properties"] = extra
    if "name" in fields and "first_name" not in contact:
        name = str(fields.get("name") or "").strip()
        if name:
            contact["first_name"] = name
    return contact


def capability_readiness(
    capability_id: str,
    *,
    bound: bool,
    ambiguous: bool = False,
    reauthorization_required: bool = False,
) -> Readiness:
    """Install/runtime readiness from catalog truth + project bindings. No extra flags."""
    spec = INVOKE_CAPABILITIES.get(capability_id) or {}
    if not spec.get("invocable") or not is_implemented(capability_id):
        return "unavailable"
    if reauthorization_required:
        return "reauthorization_required"
    if ambiguous:
        return "binding_ambiguous"
    if not bound:
        return "available_unconfigured"
    return "ready"


def select_contact_form_composition(
    *,
    sync_to_crm: bool = False,
    hubspot_connected: bool = False,
    salesforce_connected: bool = False,
) -> dict[str, Any]:
    """Same Block/Pack/capability whether HubSpot or Salesforce is connected."""
    del hubspot_connected, salesforce_connected
    return {
        "block": "forms.contact",
        "pack": "contact-capture",
        "local_entity": "contact_submission",
        "optional_capability": CONTACT_FORM_UPSERT_CAPABILITY if sync_to_crm else None,
        "failure_isolation": "best_effort",
        "local_success_independent_of_crm": True,
    }


def select_local_lead_composition(*, sync_to_crm: bool = False) -> dict[str, Any]:
    return {
        "block": "crm.lead",
        "pack": "lead-lifecycle",
        "local_entity": LOCAL_LEAD_ENTITY,
        "local_stages": LOCAL_LEAD_STAGES,
        "optional_capability": CONTACT_FORM_UPSERT_CAPABILITY if sync_to_crm else None,
        "rewritten_to_crm_contact": False,
        "auto_external_stage_sync": False,
    }


def local_stage_to_crm_deal_update(
    *,
    configured: bool,
    logical_alias: str | None,
) -> dict[str, Any] | None:
    if not configured:
        return None
    if not logical_alias:
        raise ValueError("external stage sync requires an explicit logical CRM stage alias")
    forbidden = {"dealstage", "StageName", "appointmentscheduled", "closedwon", "Closed Won"}
    if logical_alias in forbidden:
        raise ValueError("provider stage wire values are not allowed")
    return {"capability": "crm.deal.update", "stage": logical_alias}


def generic_block_must_not_branch_on_provider(source: str) -> bool:
    needles = (
        "if HubSpot",
        "if Salesforce",
        "hubspot connected",
        "salesforce connected",
        "action.startsWith('hubspot",
        "action.startsWith('salesforce",
    )
    lowered = source.lower()
    return not any(token.lower() in lowered for token in needles)


def catalog_binding_capability(pack_slug: str) -> str | None:
    overlay = CATALOG_OVERLAYS.get(pack_slug) or {}
    bindings = overlay.get("provider_bindings") or []
    if not bindings:
        return None
    return bindings[0].capability
