"""Post-submit action plan. Runs after acceptance; never part of FormDefinition.

Independent BEST_EFFORT actions map accepted values onto canonical capabilities
or explicit internal/provider actions. Forms does not resolve providers.
"""
from __future__ import annotations

from collections.abc import Mapping
from typing import Any, Literal, Protocol

from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr, model_validator

from src.domain.codegen.email_template_consumption import EmailTemplateInvocation
from src.domain.forms.canonical import (
    FORBIDDEN_PROVIDER_TOKENS,
    LOGICAL_KEY_PATTERN,
    RESERVED_CONTEXT_KEYS,
    FormDefinition,
    FormFileRef,
    parse_form_file_ref,
)
from src.domain.forms.conditions import (
    CONDITION_NOT_MET,
    FormConditionSet,
    evaluate_action_conditions,
    validate_condition_rules,
)
from src.domain.forms.persistence import (
    FormPersistenceContext,
    FormPersistenceError,
    FormSubmissionStore,
    persist_form_submission,
)
from src.domain.forms.runtime import AcceptedFormSubmission

CANONICAL_ACTION_TYPES: frozenset[str] = frozenset(
    {
        "crm.contact.upsert",
        "email.send",
        "automation.workflow.trigger",
    }
)
INTERNAL_ACTION_TYPES: frozenset[str] = frozenset(
    {
        "internal.http_forward",
        "internal.persist_to_entity",
    }
)

FAILURE_POLICY = "BEST_EFFORT"
MISSING_SOURCE = "omit"
EXPLICIT_NULL = "preserve"
IDEMPOTENCY = "submission_id_plus_action_key"
ORDERING = "declaration_list"
DEPENDENCIES = "independent"
HTTP_FORWARD_CLASSIFICATION = "internal_cloud_job_not_canonical_form_capability"

PLAN_ERROR_CODES: tuple[str, ...] = (
    "duplicate_action_key",
    "unknown_action_type",
    "unknown_source_field",
    "forbidden_plan_constant",
    "missing_workflow_constant",
    "persistence_failed",
)

FORMS_POST_SUBMIT_ACTIONS: dict[str, Any] = {
    "slice_id": "7h_post_submit_action_model",
    "status": "frozen",
    "not_form_definition": True,
    "acceptance_independent": True,
    "failure_policy": FAILURE_POLICY,
    "rollback": False,
    "missing_source": MISSING_SOURCE,
    "explicit_null": EXPLICIT_NULL,
    "idempotency": IDEMPOTENCY,
    "ordering": ORDERING,
    "dependencies": DEPENDENCIES,
    "http_forward": HTTP_FORWARD_CLASSIFICATION,
    "email_templates": False,
    "resolver_in_forms": False,
    "schema_migration": False,
    "queue": False,
    "public_bff": False,
    "form_submit_capability": False,
    "codegen": False,
    "docs": "docs/audits/FORMS_POST_SUBMIT_ACTIONS_7H.md",
}


class FormPostSubmitAction(BaseModel):
    """One declared effect. Not a provider binding and not FormDefinition."""

    model_config = ConfigDict(extra="forbid", frozen=True)
    key: StrictStr = Field(min_length=1, max_length=64, pattern=LOGICAL_KEY_PATTERN)
    action_type: StrictStr = Field(min_length=1, max_length=128)
    mapping: dict[str, StrictStr] = Field(default_factory=dict)
    constants: dict[str, Any] = Field(default_factory=dict)
    optional: StrictBool = False
    provider_specific: StrictBool = False
    template: EmailTemplateInvocation | None = None


class FormPostSubmitPlan(BaseModel):
    """Composition object. Not captured-data schema."""

    model_config = ConfigDict(extra="forbid", frozen=True)
    actions: tuple[FormPostSubmitAction, ...] = ()

    @model_validator(mode="after")
    def unique_action_keys(self):
        keys = [action.key for action in self.actions]
        if len(keys) != len(set(keys)):
            raise ValueError("duplicate_action_key")
        return self


class FormPostSubmitContext(BaseModel):
    """Project scope for capability invoke. Not form values."""

    model_config = ConfigDict(extra="forbid", frozen=True)
    project_id: StrictStr = Field(min_length=1, max_length=64)


class FormCapabilityInvokeResult(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)
    status: Literal["succeeded", "failed", "queued"]
    execution_ref: StrictStr | None = None
    error_code: StrictStr | None = None


class FormCapabilityInvoker(Protocol):
    def invoke(
        self,
        *,
        project_id: str,
        capability: str,
        payload: Mapping[str, Any],
        idempotency_key: str,
    ) -> FormCapabilityInvokeResult: ...


class FormPostSubmitActionResult(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)
    action_key: StrictStr
    action_type: StrictStr
    status: Literal["succeeded", "failed", "skipped", "queued"]
    execution_ref: StrictStr | None = None
    error_code: StrictStr | None = None
    idempotency_key: StrictStr


class FormPostSubmitResult(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)
    submission_id: StrictStr
    actions: tuple[FormPostSubmitActionResult, ...] = ()
    plan_error: StrictStr | None = None


class RecordingCapabilityInvoker:
    """Test double. Not a production resolver."""

    def __init__(
        self,
        outcomes: Mapping[str, FormCapabilityInvokeResult] | None = None,
    ) -> None:
        self.calls: list[dict[str, Any]] = []
        self.outcomes = dict(outcomes or {})

    def invoke(
        self,
        *,
        project_id: str,
        capability: str,
        payload: Mapping[str, Any],
        idempotency_key: str,
    ) -> FormCapabilityInvokeResult:
        self.calls.append(
            {
                "project_id": project_id,
                "capability": capability,
                "payload": dict(payload),
                "idempotency_key": idempotency_key,
            }
        )
        return self.outcomes.get(
            idempotency_key,
            self.outcomes.get(
                capability,
                FormCapabilityInvokeResult(status="succeeded", execution_ref="exec_ok"),
            ),
        )


def action_idempotency_key(submission_id: str, action_key: str) -> str:
    return f"{submission_id}:{action_key}"


def contact_compatible_plan() -> FormPostSubmitPlan:
    """Represent forms.contact composition without migrating the App Block.

    CRM maps email and phone only. Single ``name`` is not split into
    first_name/last_name. ``message`` is not a default CRM property.
    Email maps submitter ``email`` → ``to`` and ``message`` → ``text`` with a
    literal subject. Owner-notify formatting is
    ``contact_legacy_notification_payload``, not this generic action.
    Automation maps email/phone/message onto logical workflow ``lead-intake``.
    """
    from src.domain.forms.automation import automation_workflow_action
    from src.domain.forms.crm import crm_contact_upsert_action
    from src.domain.forms.email import email_send_action

    return FormPostSubmitPlan(
        actions=(
            crm_contact_upsert_action(),
            email_send_action(),
            automation_workflow_action(),
        )
    )


def _known_action_type(action: FormPostSubmitAction) -> bool:
    if action.action_type in CANONICAL_ACTION_TYPES:
        return True
    if action.action_type in INTERNAL_ACTION_TYPES:
        return True
    return action.provider_specific is True


def _forbidden_constant_key(key: str) -> bool:
    lowered = key.lower()
    if key in RESERVED_CONTEXT_KEYS or lowered in {item.lower() for item in RESERVED_CONTEXT_KEYS}:
        return True
    return any(token.lower() == lowered for token in FORBIDDEN_PROVIDER_TOKENS)


def validate_post_submit_plan(
    plan: FormPostSubmitPlan,
    definition: FormDefinition,
) -> tuple[str, ...]:
    errors: list[str] = []
    keys = [action.key for action in plan.actions]
    if len(keys) != len(set(keys)):
        errors.append("duplicate_action_key")
    field_keys = {field.key for field in definition.fields}
    for action in plan.actions:
        if not _known_action_type(action):
            errors.append("unknown_action_type")
        for source in action.mapping.values():
            if source not in field_keys:
                errors.append("unknown_source_field")
        for constant_key in action.constants:
            if _forbidden_constant_key(str(constant_key)):
                errors.append("forbidden_plan_constant")
        if action.action_type == "automation.workflow.trigger" and not action.provider_specific:
            from src.domain.forms.automation import validate_automation_mapping

            errors.extend(validate_automation_mapping(action, definition))
        elif action.action_type == "automation.workflow.trigger":
            workflow = action.constants.get("workflow")
            if not isinstance(workflow, str) or not workflow.strip():
                errors.append("missing_workflow_constant")
        if action.action_type == "crm.contact.upsert" and not action.provider_specific:
            from src.domain.forms.crm import validate_crm_contact_mapping

            errors.extend(validate_crm_contact_mapping(action, definition))
        if action.action_type == "email.send" and not action.provider_specific:
            from src.domain.forms.email import validate_email_mapping

            errors.extend(validate_email_mapping(action, definition))
        if action.action_type == "internal.http_forward" and not action.provider_specific:
            from src.domain.forms.forward import validate_http_forward_mapping

            errors.extend(validate_http_forward_mapping(action, definition))
    return tuple(dict.fromkeys(errors))


def _canonical_mapped_value(value: Any) -> Any:
    parsed = parse_form_file_ref(value)
    if parsed is not None:
        return {"file_id": parsed.file_id}
    if isinstance(value, FormFileRef):
        return {"file_id": value.file_id}
    return value


def map_action_payload(
    action: FormPostSubmitAction,
    values: Mapping[str, Any],
) -> dict[str, Any]:
    """Explicit source→target copy. Missing omitted; null/false/0 preserved."""
    mapped: dict[str, Any] = {}
    for target, source in action.mapping.items():
        if source not in values:
            continue
        mapped[target] = _canonical_mapped_value(values[source])
    payload = dict(mapped)
    payload.update(action.constants)
    if action.action_type == "automation.workflow.trigger":
        workflow = payload.pop("workflow", None)
        return {"workflow": workflow, "input": mapped}
    return payload


def execute_post_submit_actions(
    accepted: AcceptedFormSubmission,
    plan: FormPostSubmitPlan,
    *,
    context: FormPostSubmitContext,
    invoker: FormCapabilityInvoker,
    definition: FormDefinition,
    store: FormSubmissionStore | None = None,
    persist_first: bool = False,
    conditions: FormConditionSet | None = None,
    email_template_renderer: Any = None,
) -> FormPostSubmitResult:
    """BEST_EFFORT sequential invoke. Does not mutate acceptance or values."""
    submission_id = accepted.submission_id
    if persist_first:
        if store is None:
            return FormPostSubmitResult(
                submission_id=submission_id,
                plan_error="persistence_failed",
            )
        try:
            persist_form_submission(
                store,
                FormPersistenceContext(project_id=context.project_id),
                accepted,
            )
        except FormPersistenceError:
            return FormPostSubmitResult(
                submission_id=submission_id,
                plan_error="persistence_failed",
            )
    config_errors = validate_post_submit_plan(plan, definition)
    if config_errors:
        return FormPostSubmitResult(
            submission_id=submission_id,
            plan_error=config_errors[0],
        )
    declared_keys = tuple(action.key for action in plan.actions)
    rule_set = conditions or FormConditionSet()
    condition_errors = validate_condition_rules(
        rule_set,
        definition,
        action_keys=declared_keys,
    )
    if condition_errors:
        return FormPostSubmitResult(
            submission_id=submission_id,
            plan_error=condition_errors[0],
        )
    eligibility = evaluate_action_conditions(
        accepted.submission.values,
        rule_set,
        action_keys=declared_keys,
    )
    ineligible = set(eligibility.ineligible_actions)
    results: list[FormPostSubmitActionResult] = []
    values = accepted.submission.values
    for action in plan.actions:
        idem = action_idempotency_key(submission_id, action.key)
        if action.key in ineligible:
            results.append(
                FormPostSubmitActionResult(
                    action_key=action.key,
                    action_type=action.action_type,
                    status="skipped",
                    error_code=CONDITION_NOT_MET,
                    idempotency_key=idem,
                )
            )
            continue
        if action.action_type == "crm.contact.upsert" and not action.provider_specific:
            from src.domain.forms.crm import (
                NO_EMAIL_CODE,
                NO_EMAIL_STATUS,
                build_crm_contact_upsert_payload,
            )

            built = build_crm_contact_upsert_payload(action, values)
            if built.skipped:
                results.append(
                    FormPostSubmitActionResult(
                        action_key=action.key,
                        action_type=action.action_type,
                        status=NO_EMAIL_STATUS,
                        error_code=built.error_code or NO_EMAIL_CODE,
                        idempotency_key=idem,
                    )
                )
                continue
            if built.payload is None:
                results.append(
                    FormPostSubmitActionResult(
                        action_key=action.key,
                        action_type=action.action_type,
                        status="failed",
                        error_code=built.error_code or "crm_validation_failed",
                        idempotency_key=idem,
                    )
                )
                continue
            payload = built.payload
        elif action.action_type == "email.send" and not action.provider_specific:
            from src.domain.forms.email import (
                NO_RECIPIENT_CODE,
                NO_RECIPIENT_STATUS,
                build_email_send_payload,
            )

            built = build_email_send_payload(
                action,
                values,
                renderer=email_template_renderer,
                project_id=context.project_id,
            )
            if built.skipped:
                results.append(
                    FormPostSubmitActionResult(
                        action_key=action.key,
                        action_type=action.action_type,
                        status=NO_RECIPIENT_STATUS,
                        error_code=built.error_code or NO_RECIPIENT_CODE,
                        idempotency_key=idem,
                    )
                )
                continue
            if built.payload is None:
                results.append(
                    FormPostSubmitActionResult(
                        action_key=action.key,
                        action_type=action.action_type,
                        status="failed",
                        error_code=built.error_code or "email_content_missing",
                        idempotency_key=idem,
                    )
                )
                continue
            payload = built.payload
        elif (
            action.action_type == "automation.workflow.trigger"
            and not action.provider_specific
        ):
            from src.domain.forms.automation import build_automation_trigger_payload

            built = build_automation_trigger_payload(action, values)
            if built.payload is None:
                results.append(
                    FormPostSubmitActionResult(
                        action_key=action.key,
                        action_type=action.action_type,
                        status="failed",
                        error_code=built.error_code or "automation_mapping_failed",
                        idempotency_key=idem,
                    )
                )
                continue
            payload = built.payload
        elif action.action_type == "internal.http_forward" and not action.provider_specific:
            from src.domain.forms.forward import build_http_forward_payload

            built = build_http_forward_payload(action, values)
            if built.payload is None:
                results.append(
                    FormPostSubmitActionResult(
                        action_key=action.key,
                        action_type=action.action_type,
                        status="failed",
                        error_code=built.error_code or "forward_failed",
                        idempotency_key=idem,
                    )
                )
                continue
            payload = built.payload
        else:
            payload = map_action_payload(action, values)
        try:
            invoked = invoker.invoke(
                project_id=context.project_id,
                capability=action.action_type,
                payload=payload,
                idempotency_key=idem,
            )
        except Exception:
            invoked = FormCapabilityInvokeResult(
                status="failed",
                error_code="capability_invoke_failed",
            )
        results.append(
            FormPostSubmitActionResult(
                action_key=action.key,
                action_type=action.action_type,
                status=invoked.status,
                execution_ref=invoked.execution_ref,
                error_code=invoked.error_code,
                idempotency_key=idem,
            )
        )
    return FormPostSubmitResult(submission_id=submission_id, actions=tuple(results))
