"""Additive Forms consumption of the 8F Email Template invocation contract.

Literal 7J email.send mapping remains in email.py. This module is template
content mode only: bind vars, call Cloud render, then concrete email.send.
"""
from __future__ import annotations

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

from pydantic import BaseModel, ConfigDict, StrictStr, ValidationError

from src.domain.codegen.email_template_consumption import (
    INVALID_VARIABLE_BINDING,
    MISSING_REQUIRED_VALUE,
    MISSING_REQUIRED_VARIABLE_BINDING,
    SOURCE_FIELD_MISSING,
    UNKNOWN_TEMPLATE_KEY,
    UNKNOWN_TEMPLATE_VARIABLE,
    EmailTemplateInvocation,
    concrete_email_send_payload,
    resolve_email_template_variables,
)
from src.domain.forms.actions import FormPostSubmitAction
from src.domain.forms.canonical import STRING_FIELD_TYPES, FormDefinition
from src.domain.forms.email import (
    FORBIDDEN_EMAIL_CONSTANT_KEYS,
    NO_RECIPIENT_CODE,
    EmailSendBuild,
    _is_file_value,
    _source_field,
    _usable_recipient,
)

ADDRESSING_TARGETS: frozenset[str] = frozenset({"to", "reply_to"})
CONTENT_TARGETS: frozenset[str] = frozenset({"subject", "text", "html"})
CONFLICTING_CONTENT_MODE = "conflicting_email_content_mode"
TEMPLATE_RENDERER_MISSING = "template_renderer_missing"
TEMPLATE_RENDER_FAILED = "template_render_failed"
UNKNOWN_TEMPLATE_FIELD = "unknown_source_field"

FORMS_EMAIL_TEMPLATE_CONSUMPTION: dict[str, Any] = {
    "slice_id": "8g_forms_email_template_consumption",
    "status": "complete",
    "literal_mode": "preserved_7j",
    "template_mode": "additive",
    "exclusivity": "literal_xor_template",
    "invocation": "EmailTemplateInvocation",
    "interpolation": False,
    "new_email_verb": False,
    "forms_table": False,
    "contact_capture_migrated": False,
    "legacy_contact": "deferred",
    "docs": "docs/audits/EMAIL_TEMPLATES_DOMAIN_8G.md",
}


class FormEmailTemplateRenderResult(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)
    ok: bool
    rendered: dict[str, Any] | None = None
    error_code: StrictStr | None = None


class FormEmailTemplateRenderer(Protocol):
    """Port to canonical Cloud render. Forms does not interpolate."""

    def render(
        self,
        *,
        project_id: str,
        template_key: str,
        vars: Mapping[str, str],
    ) -> FormEmailTemplateRenderResult: ...


class RecordingEmailTemplateRenderer:
    """Test double. Returns configured RenderedEmail; does not escape."""

    def __init__(
        self,
        rendered: Mapping[str, Any] | None = None,
        *,
        error_code: str | None = None,
    ) -> None:
        self.calls: list[dict[str, Any]] = []
        self.rendered = dict(
            rendered
            or {
                "subject": "Rendered subject",
                "text": "Rendered text",
                "html": "<p>Rendered</p>",
            }
        )
        self.error_code = error_code

    def render(
        self,
        *,
        project_id: str,
        template_key: str,
        vars: Mapping[str, str],
    ) -> FormEmailTemplateRenderResult:
        self.calls.append(
            {
                "project_id": project_id,
                "template_key": template_key,
                "vars": dict(vars),
            }
        )
        if self.error_code:
            return FormEmailTemplateRenderResult(ok=False, error_code=self.error_code)
        return FormEmailTemplateRenderResult(ok=True, rendered=dict(self.rendered))


def email_send_template_action(
    *,
    key: str,
    invocation: EmailTemplateInvocation,
    mapping: Mapping[str, str] | None = None,
    constants: Mapping[str, str] | None = None,
    optional: bool = False,
) -> FormPostSubmitAction:
    return FormPostSubmitAction(
        key=key,
        action_type="email.send",
        mapping=dict(mapping or {}),
        constants=dict(constants or {}),
        optional=optional,
        template=invocation,
    )


def _invocation_error_code(exc: ValidationError) -> str:
    text = str(exc).lower()
    for code in (
        UNKNOWN_TEMPLATE_KEY,
        UNKNOWN_TEMPLATE_VARIABLE,
        MISSING_REQUIRED_VARIABLE_BINDING,
        INVALID_VARIABLE_BINDING,
    ):
        if code in text:
            return code
    return INVALID_VARIABLE_BINDING


def parse_email_template_invocation(action: FormPostSubmitAction) -> tuple[EmailTemplateInvocation | None, str | None]:
    if action.template is None:
        return None, None
    if isinstance(action.template, EmailTemplateInvocation):
        return action.template, None
    try:
        return EmailTemplateInvocation.model_validate(action.template), None
    except ValidationError as exc:
        return None, _invocation_error_code(exc)


def validate_email_template_mapping(
    action: FormPostSubmitAction,
    definition: FormDefinition,
) -> tuple[str, ...]:
    invocation, parse_error = parse_email_template_invocation(action)
    if parse_error:
        return (parse_error,)
    if invocation is None:
        return ()
    errors: list[str] = []
    configured: set[str] = set()
    for target, source in action.mapping.items():
        if target in CONTENT_TARGETS:
            errors.append(CONFLICTING_CONTENT_MODE)
            continue
        if target not in ADDRESSING_TARGETS:
            errors.append("unknown_email_target")
            continue
        configured.add(target)
        field = _source_field(definition, source)
        if field is None:
            errors.append("unknown_source_field")
            continue
        if field.type == "file":
            errors.append("unsupported_email_file_mapping")
            continue
        if field.type not in STRING_FIELD_TYPES:
            errors.append("unsupported_email_source_type")
    for key, value in action.constants.items():
        if key in CONTENT_TARGETS:
            errors.append(CONFLICTING_CONTENT_MODE)
            continue
        if key in FORBIDDEN_EMAIL_CONSTANT_KEYS or key not in ADDRESSING_TARGETS:
            errors.append("unexpected_email_constant")
            continue
        if not isinstance(value, str):
            errors.append("unsupported_email_literal_type")
            continue
        configured.add(key)
    if "to" not in configured:
        errors.append("missing_email_content_config")
    for name, binding in invocation.variables.items():
        if binding.source != "field":
            continue
        field = _source_field(definition, binding.field or "")
        if field is None:
            errors.append(UNKNOWN_TEMPLATE_FIELD)
            continue
        if field.type == "file" or field.type not in STRING_FIELD_TYPES:
            errors.append("unsupported_email_source_type")
    return tuple(dict.fromkeys(errors))


def _addressing_payload(
    action: FormPostSubmitAction,
    values: Mapping[str, Any],
) -> EmailSendBuild:
    payload: dict[str, Any] = {}
    mapped_to = "to" in action.mapping
    for target, source in action.mapping.items():
        if target not in ADDRESSING_TARGETS or source not in values:
            continue
        value = values[source]
        if _is_file_value(value):
            return EmailSendBuild(skipped=False, error_code="unsupported_email_file_mapping")
        if value is None or not isinstance(value, str):
            if target == "to":
                return EmailSendBuild(skipped=True, error_code=NO_RECIPIENT_CODE)
            return EmailSendBuild(skipped=False, error_code="unsupported_email_source_type")
        payload[target] = value
    for key, value in action.constants.items():
        if key in ADDRESSING_TARGETS and isinstance(value, str):
            payload[key] = value
    if mapped_to and not _usable_recipient(payload.get("to")):
        return EmailSendBuild(skipped=True, error_code=NO_RECIPIENT_CODE)
    if not _usable_recipient(payload.get("to")):
        return EmailSendBuild(skipped=True, error_code=NO_RECIPIENT_CODE)
    return EmailSendBuild(skipped=False, payload=payload)


def build_email_send_payload_from_template(
    action: FormPostSubmitAction,
    values: Mapping[str, Any],
    *,
    renderer: FormEmailTemplateRenderer | None,
    project_id: str | None,
) -> EmailSendBuild:
    invocation, parse_error = parse_email_template_invocation(action)
    if parse_error or invocation is None:
        return EmailSendBuild(skipped=False, error_code=parse_error or UNKNOWN_TEMPLATE_KEY)
    addressing = _addressing_payload(action, values)
    if addressing.skipped or addressing.payload is None:
        return addressing
    resolved = resolve_email_template_variables(invocation, values)
    if not resolved.ok:
        return EmailSendBuild(
            skipped=False,
            error_code=resolved.error_code or MISSING_REQUIRED_VALUE,
        )
    if renderer is None or not project_id:
        return EmailSendBuild(skipped=False, error_code=TEMPLATE_RENDERER_MISSING)
    rendered = renderer.render(
        project_id=project_id,
        template_key=invocation.template_key,
        vars=resolved.vars,
    )
    if not rendered.ok or rendered.rendered is None:
        return EmailSendBuild(
            skipped=False,
            error_code=rendered.error_code or TEMPLATE_RENDER_FAILED,
        )
    try:
        payload = concrete_email_send_payload(
            to=addressing.payload["to"],
            rendered=rendered.rendered,
            reply_to=addressing.payload.get("reply_to"),
        )
    except Exception:
        return EmailSendBuild(skipped=False, error_code="email_payload_invalid")
    return EmailSendBuild(skipped=False, payload=payload)
