"""Canonical domain-event alias + producer mapping (Phase 10D2).

PackEvent.id remains identity. Producers and aliases are Core contract data.
Cloud must not infer event names from entity keys or the last segment of an id.
"""
from __future__ import annotations

from typing import Any

from src.domain.feature_packs.contract import (
    FeaturePackContract,
    PackEvent,
    PackEventAlias,
    PackEventProducer,
    PackWorkflowContract,
)

# Generic record/resource operations. Siblings of domain events, never aliases.
GENERIC_RECORD_EVENT_IDS = frozenset(
    {
        "record.created",
        "record.updated",
        "record.deleted",
        "record.restored",
        "record.cloned",
        "resource.created",
        "resource.updated",
        "resource.deleted",
    }
)

# Inbound CRM identities (connectors.crm_events). Duplicated so feature_packs
# does not depend on the connectors domain. Tests assert the sets stay aligned.
INBOUND_CRM_EVENT_IDENTITIES = frozenset(
    {
        "crm.contact.created",
        "crm.contact.updated",
        "crm.company.created",
        "crm.company.updated",
        "crm.deal.created",
        "crm.deal.updated",
        "crm.contact.deleted",
        "crm.company.deleted",
        "crm.deal.deleted",
        "crm.contact.restored",
        "crm.company.restored",
        "crm.deal.restored",
        "crm.association.changed",
        "crm.object.changed",
        "crm.lead.created",
        "crm.lead.updated",
    }
)

# Composition aliases that reuse a string also used as a rejected inbound type.
# Documented overlap, not a merge of families.
ALLOWED_INBOUND_NAME_OVERLAP = frozenset({"crm.lead.created"})

# App Block events with no pack-owned canonical. Option B: composition-only.
COMPOSITION_ONLY_EVENT_IDS = frozenset(
    {
        "forms.quote_request.submitted",
        "user.logged_in",
    }
)


def workflow_states(workflow: PackWorkflowContract) -> set[str]:
    states: set[str] = set(workflow.transitions.keys())
    for targets in workflow.transitions.values():
        states.update(targets)
    return states


def entity_fields_from_composition(composition: dict[str, Any] | None) -> dict[str, set[str]]:
    fields_by_entity: dict[str, set[str]] = {}
    for entity in (composition or {}).get("entities") or []:
        if not isinstance(entity, dict):
            continue
        key = str(entity.get("entity_key") or "")
        if not key:
            continue
        schema = entity.get("schema") if isinstance(entity.get("schema"), dict) else {}
        keys: set[str] = set()
        for field in schema.get("fields") or []:
            if isinstance(field, dict) and field.get("key"):
                keys.add(str(field["key"]))
        fields_by_entity[key] = keys
    return fields_by_entity


def validate_pack_event_mappings(
    contract: FeaturePackContract,
    *,
    entity_fields: dict[str, set[str]] | None = None,
) -> None:
    """Hard-fail malformed producer/alias data before it can reach Cloud."""
    declared_ids = [event.id for event in contract.events]
    seen_ids: set[str] = set()
    producer_owners: dict[tuple[str, str, str | None, str | None, str | None], str] = {}
    alias_targets: dict[str, str] = {}
    workflows = {item.entity_key: item for item in contract.workflows}
    entity_keys = {item.entity_key for item in contract.entities}

    for event in contract.events:
        if event.id in GENERIC_RECORD_EVENT_IDS:
            raise ValueError(
                f"pack event '{event.id}' collides with a generic record/resource sibling"
            )
        if event.id in seen_ids:
            raise ValueError(f"duplicate pack event id '{event.id}' on {contract.slug}")
        seen_ids.add(event.id)
        if event.producer is not None:
            _validate_producer(
                contract.slug,
                event,
                entity_keys=entity_keys,
                workflows=workflows,
                entity_fields=entity_fields or {},
            )
            if event.recipient is not None:
                _validate_recipient(contract.slug, event, entity_fields=entity_fields or {})
            key = event.producer.identity()
            other = producer_owners.get(key)
            if other is not None and other != event.id:
                raise ValueError(
                    f"duplicate producer mapping on {contract.slug}: "
                    f"'{other}' and '{event.id}' share {key}"
                )
            producer_owners[key] = event.id
        for alias in event.aliases:
            _validate_alias(
                contract.slug,
                event.id,
                alias,
                declared_ids=declared_ids,
                alias_targets=alias_targets,
            )


def _validate_producer(
    slug: str,
    event: PackEvent,
    *,
    entity_keys: set[str],
    workflows: dict[str, PackWorkflowContract],
    entity_fields: dict[str, set[str]],
) -> None:
    producer = event.producer
    assert producer is not None
    if producer.entity not in entity_keys:
        raise ValueError(
            f"event '{event.id}' producer entity '{producer.entity}' "
            f"is not declared on pack {slug}"
        )
    fields = entity_fields.get(producer.entity)

    if producer.kind == "workflow.transition":
        workflow = workflows.get(producer.entity)
        if workflow is None:
            raise ValueError(
                f"event '{event.id}' workflow producer on {slug} has no "
                f"workflow for entity '{producer.entity}'"
            )
        if producer.field != workflow.field:
            raise ValueError(
                f"event '{event.id}' workflow field '{producer.field}' does not "
                f"match schema.workflow.field '{workflow.field}' on {slug}"
            )
        allowed = workflow_states(workflow)
        if producer.from_state is not None and producer.from_state not in allowed:
            raise ValueError(
                f"event '{event.id}' from_state '{producer.from_state}' is not a "
                f"workflow state on {slug}"
            )
        if producer.to_state is not None and producer.to_state not in allowed:
            raise ValueError(
                f"event '{event.id}' to_state '{producer.to_state}' is not a "
                f"workflow state on {slug}"
            )
        return

    if producer.kind == "field.changed" and fields:
        if producer.field not in fields:
            raise ValueError(
                f"event '{event.id}' field.changed field '{producer.field}' is not "
                f"declared on entity '{producer.entity}'"
            )


def _validate_recipient(
    slug: str,
    event: PackEvent,
    *,
    entity_fields: dict[str, set[str]],
) -> None:
    """A recipient route reads a field off the SAME record that emitted the
    event (event.producer.entity) -- catch a typo'd field/role_field at
    pack-definition time, same discipline as _validate_producer's
    field.changed check, instead of letting Cloud discover it at runtime
    against a record it fetches by envelope.resource.id."""
    recipient = event.recipient
    producer = event.producer
    assert recipient is not None and producer is not None
    fields = entity_fields.get(producer.entity)
    if not fields:
        return  # no field inventory supplied at this call site -- skip, same as _validate_producer
    if recipient.field is not None and recipient.field not in fields:
        raise ValueError(
            f"event '{event.id}' recipient field '{recipient.field}' is not "
            f"declared on entity '{producer.entity}' on {slug}"
        )
    if recipient.role_field is not None and recipient.role_field not in fields:
        raise ValueError(
            f"event '{event.id}' recipient role_field '{recipient.role_field}' is not "
            f"declared on entity '{producer.entity}' on {slug}"
        )


def _validate_alias(
    slug: str,
    canonical_id: str,
    alias: PackEventAlias,
    *,
    declared_ids: list[str],
    alias_targets: dict[str, str],
) -> None:
    if alias.id == canonical_id:
        raise ValueError(f"canonical event '{canonical_id}' cannot alias itself")
    if alias.id in GENERIC_RECORD_EVENT_IDS:
        raise ValueError(
            f"alias '{alias.id}' cannot shadow a generic record/resource sibling"
        )
    if alias.id in declared_ids:
        raise ValueError(
            f"alias '{alias.id}' on {slug} shadows canonical pack event id '{alias.id}'"
        )
    inbound_hit = alias.id in INBOUND_CRM_EVENT_IDENTITIES
    if inbound_hit and alias.id not in ALLOWED_INBOUND_NAME_OVERLAP:
        raise ValueError(
            f"alias '{alias.id}' collides with a frozen inbound CRM event identity"
        )
    existing = alias_targets.get(alias.id)
    if existing is not None and existing != canonical_id:
        raise ValueError(
            f"alias '{alias.id}' resolves to both '{existing}' and '{canonical_id}'"
        )
    alias_targets[alias.id] = canonical_id


def validate_catalog_event_registry(contracts: list[FeaturePackContract]) -> None:
    """Cross-pack alias uniqueness. Shared canonical ids (message.sent) are allowed."""
    alias_targets: dict[str, str] = {}
    canonical_ids: set[str] = set()
    for contract in contracts:
        validate_pack_event_mappings(contract)
        for event in contract.events:
            canonical_ids.add(event.id)
    for contract in contracts:
        for event in contract.events:
            for alias in event.aliases:
                if alias.id in canonical_ids:
                    raise ValueError(
                        f"alias '{alias.id}' shadows canonical PackEvent.id '{alias.id}'"
                    )
                other = alias_targets.get(alias.id)
                if other is not None and other != event.id:
                    raise ValueError(
                        f"alias '{alias.id}' resolves to both '{other}' and '{event.id}'"
                    )
                alias_targets[alias.id] = event.id


def catalog_event_index(contracts: list[FeaturePackContract]) -> dict[str, str]:
    """Map canonical id and aliases → canonical PackEvent.id."""
    index: dict[str, str] = {}
    for contract in contracts:
        for event in contract.events:
            index[event.id] = event.id
            for alias in event.aliases:
                index[alias.id] = event.id
    return index


def runtime_domain_event_payload(event: PackEvent) -> dict[str, Any] | None:
    """Subset Cloud may consume. producer=None is omitted (non-emittable).
    recipient is omitted entirely when absent (most events have none) --
    Cloud must not infer a routing rule for an event that didn't declare one,
    same principle as never inferring an event name from an entity key."""
    if event.producer is None:
        return None
    payload: dict[str, Any] = {
        "id": event.id,
        "version": event.version,
        "aliases": [alias.model_dump() for alias in event.aliases],
        "producer": event.producer.model_dump(exclude_none=True),
    }
    if event.recipient is not None:
        payload["recipient"] = event.recipient.model_dump(exclude_none=True)
    return payload


def resolve_canonical_event_id(
    name: str, index: dict[str, str]
) -> str | None:
    """Resolve a subscriber/composition name to a pack canonical id.

    Generic siblings and composition-only App Block events return None.
    """
    if name in GENERIC_RECORD_EVENT_IDS or name in COMPOSITION_ONLY_EVENT_IDS:
        return None
    return index.get(name)
