"""7P Forms composition binding. Derives ResolvedFormRuntime from installed composition.

App Blocks own callable form identity. Feature Packs supply backend config.
No Forms table. Client code cannot register bindings.
"""
from __future__ import annotations

from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from typing import Any, Literal

from src.domain.app_blocks.canonical import FORM_RUNTIME_BY_BLOCK as BLOCK_FORM_KEYS
from src.domain.forms.actions import (
    FormPostSubmitAction,
    FormPostSubmitPlan,
    validate_post_submit_plan,
)
from src.domain.forms.anti_abuse import FormAntiAbusePolicy
from src.domain.forms.canonical import FormCanonicalError, project_form_definition
from src.domain.forms.conditions import FormConditionSet, validate_condition_rules
from src.domain.forms.contact_notifications import forms_contact_notification_actions
from src.domain.forms.crm import crm_contact_upsert_action
from src.domain.forms.public import ResolvedFormRuntime
from src.domain.forms.steps import validate_step_plan

FORM_BINDING_CONFLICT = "form_binding_conflict"
PUBLIC_SUBMISSION_PACK = "contact-capture"
CALLABLE_PACK_STATUSES = frozenset(
    {
        "installed",
        "configuration_required",
        "configured",
        "deployed",
        "verified",
        "enabled",
        "supported",
    }
)

MigrationClass = Literal["MIGRATE", "COMPOSE", "KEEP_LEGACY", "PROVIDER_SPECIFIC"]

FORMS_COMPOSITION_INVENTORY: tuple[dict[str, Any], ...] = (
    {
        "id": "forms.contact",
        "kind": "app_block",
        "intent": "contact",
        "submit": "/api/contact",
        "classification": "COMPOSE",
        "form_key": "contact",
        "legacy_route": "POST /api/contact",
    },
    {
        "id": "forms.quote_request",
        "kind": "app_block",
        "intent": "quote",
        "submit": "submitFormEntry_or_contact_capture",
        "classification": "COMPOSE",
        "form_key": "quote_request",
        "files": True,
    },
    {
        "id": "contact-capture",
        "kind": "feature_pack",
        "intent": "contact",
        "classification": "COMPOSE",
        "alias_of": None,
        "pack_is_not_form_key": True,
    },
    {
        "id": "form.public_submission",
        "kind": "pack_provide_id",
        "classification": "KEEP_LEGACY",
        "note": "alias of contact-capture; not FormSubmission",
    },
    {
        "id": "form.public_create",
        "kind": "composer_primitive",
        "classification": "KEEP_LEGACY",
        "note": "anonymous Cloud record create; not FormDefinition",
    },
    {
        "id": "submitFormEntry",
        "kind": "codegen_adapter",
        "classification": "KEEP_LEGACY",
        "note": "entity public-create; not canonical Forms",
    },
    {
        "id": "/api/contact",
        "kind": "legacy_route",
        "classification": "KEEP_LEGACY",
        "note": "existing deployments remain; new installs use canonical Forms",
    },
    {
        "id": "form.standard",
        "kind": "starter_composite",
        "classification": "KEEP_LEGACY",
        "note": "renderer, not FormDefinition",
    },
    {
        "id": "form.field",
        "kind": "starter_composite",
        "classification": "KEEP_LEGACY",
    },
    {
        "id": "form.section",
        "kind": "starter_composite",
        "classification": "KEEP_LEGACY",
    },
    {
        "id": "crm.lead",
        "kind": "app_block",
        "classification": "KEEP_LEGACY",
        "note": "local lead records, not Forms",
    },
    {
        "id": "crm.pipeline",
        "kind": "pack_alias",
        "classification": "KEEP_LEGACY",
        "note": "local lead-lifecycle, not form_key",
    },
)

FORMS_COMPOSITION_PHASE_7P: dict[str, Any] = {
    "slice_id": "7p_app_block_feature_pack_starter",
    "status": "frozen",
    "source_of_truth": "installed_composition",
    "lookup": "project_id_plus_form_key",
    "schema_migration": False,
    "form_submit_capability": False,
    "legacy_contact_route": "retained",
    "submit_form_entry": "retained_for_non_forms",
    "client_registration": False,
    "first_wins": False,
    "conflict_code": FORM_BINDING_CONFLICT,
    "docs": "docs/audits/FORMS_COMPOSITION_7P.md",
}


@dataclass(frozen=True)
class InstalledPackView:
    slug: str
    status: str
    config: Mapping[str, Any]


@dataclass(frozen=True)
class FormBindingSource:
    form_key: str
    owner_id: str
    owner_kind: Literal["app_block", "feature_pack"]
    pack_slug: str
    pack_config: Mapping[str, Any]


@dataclass(frozen=True)
class FormBindingResolution:
    sources: tuple[FormBindingSource, ...]
    conflicts: frozenset[str]
    runtimes: dict[str, ResolvedFormRuntime]


def classify_composition(artifact_id: str) -> str:
    for row in FORMS_COMPOSITION_INVENTORY:
        if row["id"] == artifact_id:
            return str(row["classification"])
    raise KeyError(artifact_id)


def _truthy(config: Mapping[str, Any], key: str) -> bool:
    value = config.get(key)
    return value in {True, "true", "1", 1}


def _contact_plan(config: Mapping[str, Any]) -> FormPostSubmitPlan:
    actions: list[FormPostSubmitAction] = []
    if config.get("crm_sync_enabled") is not False:
        actions.append(crm_contact_upsert_action(optional=True))
    actions.extend(forms_contact_notification_actions(config))
    return FormPostSubmitPlan(actions=tuple(actions))


def _quote_plan(config: Mapping[str, Any]) -> FormPostSubmitPlan:
    actions: list[FormPostSubmitAction] = []
    actions.extend(forms_contact_notification_actions(config))
    return FormPostSubmitPlan(actions=tuple(actions))


def _configured_definition_keys(config: Mapping[str, Any]) -> tuple[str, ...]:
    raw = config.get("form_definitions")
    if not isinstance(raw, dict):
        return ()
    return tuple(
        str(key)
        for key, value in raw.items()
        if isinstance(key, str) and isinstance(value, dict)
    )


def _anti_abuse(config: Mapping[str, Any]) -> FormAntiAbusePolicy:
    return FormAntiAbusePolicy(
        challenge_required=_truthy(config, "challenge_required"),
        honeypot_enabled=True,
    )


def build_resolved_runtime(source: FormBindingSource) -> ResolvedFormRuntime | None:
    configured = source.form_key in _configured_definition_keys(source.pack_config)
    if source.form_key not in {"contact", "quote_request"} and not configured:
        return None
    try:
        definition = project_form_definition(
            form_key=source.form_key, config=dict(source.pack_config)
        )
    except FormCanonicalError:
        # An explicitly configured but malformed definition is a composition
        # blocker. Never expose a different fallback schema or turn it into a
        # request-time 500.
        return None
    if source.form_key == "contact":
        plan = _contact_plan(source.pack_config)
    elif source.form_key == "quote_request":
        plan = _quote_plan(source.pack_config)
    else:
        # A Core-resolved form that is not contact or quote is persisted as
        # captured data. Do not invent CRM or email mappings for its fields.
        plan = FormPostSubmitPlan(actions=())
    runtime = ResolvedFormRuntime(
        definition=definition,
        conditions=FormConditionSet(),
        anti_abuse=_anti_abuse(source.pack_config),
        plan=plan,
        persist_first=True,
    )
    errors = validate_resolved_runtime(runtime)
    if errors:
        return None
    return runtime


def validate_resolved_runtime(runtime: ResolvedFormRuntime) -> tuple[str, ...]:
    errors: list[str] = []
    errors.extend(validate_condition_rules(runtime.conditions, runtime.definition))
    errors.extend(validate_post_submit_plan(runtime.plan, runtime.definition))
    if runtime.step_plan is not None:
        errors.extend(validate_step_plan(runtime.step_plan, runtime.definition))
    return tuple(dict.fromkeys(errors))


def collect_form_binding_sources(
    *,
    planned_block_ids: Sequence[str],
    packs: Sequence[InstalledPackView],
) -> tuple[FormBindingSource, ...]:
    installed = {
        pack.slug: pack
        for pack in packs
        if pack.status in CALLABLE_PACK_STATUSES
    }
    planned_forms = [block_id for block_id in planned_block_ids if block_id in BLOCK_FORM_KEYS]
    sources: list[FormBindingSource] = []
    if planned_forms:
        pack = installed.get(PUBLIC_SUBMISSION_PACK)
        if pack is not None:
            for block_id in planned_forms:
                sources.append(
                    FormBindingSource(
                        form_key=BLOCK_FORM_KEYS[block_id],
                        owner_id=block_id,
                        owner_kind="app_block",
                        pack_slug=pack.slug,
                        pack_config=dict(pack.config),
                    )
                )
            _append_configured_form_sources(sources, pack)
        return tuple(sources)
    pack = installed.get(PUBLIC_SUBMISSION_PACK)
    if pack is None:
        return ()
    sources.append(
        FormBindingSource(
            form_key="contact",
            owner_id="contact-capture",
            owner_kind="feature_pack",
            pack_slug=pack.slug,
            pack_config=dict(pack.config),
        )
    )
    _append_configured_form_sources(sources, pack)
    return tuple(sources)


def _append_configured_form_sources(
    sources: list[FormBindingSource], pack: InstalledPackView
) -> None:
    """Bind every Core-resolved form definition on the installed pack.

    App Block ids only cover contact and quote. A student application or any
    other form Core already stored on the pack must be callable at
    ``/api/forms/{form_key}/submissions`` or the public submit returns
    ``form_unavailable``.
    """
    present = {source.form_key for source in sources}
    for form_key in _configured_definition_keys(pack.config):
        if form_key in present:
            continue
        sources.append(
            FormBindingSource(
                form_key=form_key,
                owner_id=f"form_definition:{form_key}",
                owner_kind="feature_pack",
                pack_slug=pack.slug,
                pack_config=dict(pack.config),
            )
        )
        present.add(form_key)


def resolve_form_bindings(sources: Sequence[FormBindingSource]) -> FormBindingResolution:
    by_key: dict[str, list[FormBindingSource]] = {}
    for source in sources:
        by_key.setdefault(source.form_key, []).append(source)
    conflicts: set[str] = set()
    runtimes: dict[str, ResolvedFormRuntime] = {}
    for form_key, group in by_key.items():
        owners = {item.owner_id for item in group}
        if len(owners) > 1:
            conflicts.add(form_key)
            continue
        runtime = build_resolved_runtime(group[0])
        if runtime is None:
            continue
        runtimes[form_key] = runtime
    return FormBindingResolution(
        sources=tuple(sources),
        conflicts=frozenset(conflicts),
        runtimes=runtimes,
    )


class CompositionFormRuntimeStore:
    """Project-scoped snapshot. get() does not first-win across conflicts."""

    def __init__(
        self,
        project_id: str,
        resolution: FormBindingResolution,
    ) -> None:
        self.project_id = project_id
        self.resolution = resolution

    def get(self, project_id: str, form_key: str) -> ResolvedFormRuntime | None:
        if project_id != self.project_id:
            return None
        if form_key in self.resolution.conflicts:
            return None
        return self.resolution.runtimes.get(form_key)


class OverlayFormRuntimeStore:
    """Test overlay in front of composition. Overlay wins for explicit fixtures."""

    def __init__(self, overlay, fallback) -> None:
        self._overlay = overlay
        self._fallback = fallback

    def get(self, project_id: str, form_key: str) -> ResolvedFormRuntime | None:
        found = self._overlay.get(project_id, form_key)
        if found is not None:
            return found
        return self._fallback.get(project_id, form_key)


def planned_form_keys(planned_block_ids: Sequence[str]) -> tuple[str, ...]:
    return tuple(
        BLOCK_FORM_KEYS[block_id]
        for block_id in planned_block_ids
        if block_id in BLOCK_FORM_KEYS
    )


def publish_form_readiness(
    *,
    generated_form_keys: Sequence[str],
    resolution: FormBindingResolution,
) -> tuple[str, ...]:
    """generated keys must have exactly one valid binding. Documented publish hook."""
    errors: list[str] = []
    for key in generated_form_keys:
        if key in resolution.conflicts:
            errors.append(FORM_BINDING_CONFLICT)
            continue
        if key not in resolution.runtimes:
            errors.append("form_unavailable")
    return tuple(dict.fromkeys(errors))
