"""Public Forms submit runtime. One final submission. Not a connector capability.

Trusted configuration is resolved from (project_id, form_key). The caller
supplies values (plus 7G transport keys), never definition/actions/policy.
"""
from __future__ import annotations

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

from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictInt, StrictStr, ValidationError

from src.domain.forms.actions import (
    FormCapabilityInvoker,
    FormPostSubmitContext,
    FormPostSubmitPlan,
    execute_post_submit_actions,
    validate_post_submit_plan,
)
from src.domain.forms.anti_abuse import (
    CHALLENGE_FAILED,
    CHALLENGE_MISSING,
    CHALLENGE_UNAVAILABLE,
    HONEYPOT_TRIGGERED,
    RATE_LIMITED,
    ChallengeVerificationResult,
    FormAntiAbusePolicy,
    FormAttemptLimiter,
    FormChallengeVerifier,
    InMemoryAttemptLimiter,
    process_form_submission_request,
)
from src.domain.forms.canonical import FormDefinition
from src.domain.forms.conditions import (
    FormConditionSet,
    validate_condition_rules,
)
from src.domain.forms.files import FormFileDirectory, InMemoryFormFileDirectory
from src.domain.forms.persistence import (
    FormPersistenceContext,
    FormPersistenceError,
    FormSubmissionStore,
    InMemoryFormSubmissionStore,
    persist_form_submission,
)
from src.domain.forms.runtime import new_submission_id, platform_now
from src.domain.forms.steps import FormStepPlan, validate_step_plan

PUBLIC_FORMS_SUBMIT_ROUTE = (
    "POST /api/projects/{project_id}/forms/{form_key}/submissions"
)
PUBLIC_FORMS_SUBMIT_KIND = "product_domain_runtime"
NOT_A_CAPABILITY = True
HONEYPOT_PUBLIC = "fake_accepted_without_id"
DEFINITION_SOURCE = "project_form_runtime_store_port"

FORBIDDEN_ENVELOPE_KEYS: frozenset[str] = frozenset(
    {
        "definition",
        "actions",
        "plan",
        "persist_first",
        "workflow",
        "provider_hint",
        "binding",
        "target",
        "target_url",
        "client_identity",
        "project_id",
        "workspace_id",
        "user_id",
        "current_step",
        "completed_steps",
        "step_plan",
        "step_index",
        "draft_id",
        "challenge_required",
        "skip_rate_limit",
    }
)

FORMS_PUBLIC_RUNTIME: dict[str, Any] = {
    "slice_id": "7n_public_bff_runtime_truth",
    "status": "frozen",
    "public_operation": "final_submit_only",
    "route": PUBLIC_FORMS_SUBMIT_ROUTE,
    "route_kind": PUBLIC_FORMS_SUBMIT_KIND,
    "not_capability": NOT_A_CAPABILITY,
    "form_submit_capability": False,
    "host_relative": True,
    "definition_authority": "server",
    "definition_source": DEFINITION_SOURCE,
    "lookup": "project_id_plus_form_key",
    "step_plan_in_submit": False,
    "drafts": False,
    "honeypot_public": HONEYPOT_PUBLIC,
    "duplicate_post": "two_fsb",
    "action_failure": "still_accepted",
    "persist_required_failure": "submission_failed",
    "public_get": False,
    "contact_adapter": "unchanged",
    "codegen": False,
    "app_block_migrated": False,
    "schema_migration": False,
    "docs": "docs/audits/FORMS_PUBLIC_RUNTIME_7N.md",
}


class ResolvedFormRuntime(BaseModel):
    """Request-scoped trusted snapshot. Not a public DTO."""

    model_config = ConfigDict(extra="forbid", frozen=True)
    definition: FormDefinition
    conditions: FormConditionSet = Field(default_factory=FormConditionSet)
    anti_abuse: FormAntiAbusePolicy = Field(default_factory=FormAntiAbusePolicy)
    plan: FormPostSubmitPlan = Field(default_factory=FormPostSubmitPlan)
    persist_first: StrictBool = False
    step_plan: FormStepPlan | None = None


class FormPublicSubmitRequest(BaseModel):
    """Transport envelope. extra=forbid rejects client policy/control fields."""

    model_config = ConfigDict(extra="forbid")
    values: dict[str, Any] = Field(default_factory=dict)
    challenge: StrictStr | None = None


class FormRuntimeStore(Protocol):
    def get(self, project_id: str, form_key: str) -> ResolvedFormRuntime | None: ...


class InMemoryFormRuntimeStore:
    """Test/reference resolution. Not a Forms table."""

    def __init__(self) -> None:
        self._items: dict[tuple[str, str], ResolvedFormRuntime] = {}

    def put(self, project_id: str, runtime: ResolvedFormRuntime) -> None:
        self._items[(project_id, runtime.definition.key)] = runtime

    def get(self, project_id: str, form_key: str) -> ResolvedFormRuntime | None:
        return self._items.get((project_id, form_key))


class UnwiredChallengeVerifier:
    """Fail closed when a form requires a challenge and no verifier is wired."""

    def verify(self, token: str | None) -> ChallengeVerificationResult:
        if not token:
            return ChallengeVerificationResult(
                passed=False, failure_code=CHALLENGE_MISSING
            )
        return ChallengeVerificationResult(
            passed=False, failure_code=CHALLENGE_UNAVAILABLE
        )


class SilentCapabilityInvoker:
    """Production placeholder. BEST_EFFORT failures stay internal."""

    def invoke(self, **kwargs: Any):
        from src.domain.forms.actions import FormCapabilityInvokeResult

        return FormCapabilityInvokeResult(
            status="failed",
            error_code="capability_invoke_unwired",
        )


class FormPublicResult(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)
    http_status: StrictInt
    body: dict[str, Any]
    submission_id: StrictStr | None = None
    internal_action_error_codes: tuple[str, ...] = ()
    honeypot_triggered: StrictBool = False


def _field_map(errors: list[Any]) -> dict[str, list[str]]:
    mapped: dict[str, list[str]] = {}
    for err in errors:
        mapped.setdefault(err.field, []).append(err.code)
    return mapped


def _validate_runtime(runtime: ResolvedFormRuntime) -> tuple[str, ...]:
    errors: list[str] = []
    declared = tuple(action.key for action in runtime.plan.actions)
    errors.extend(
        validate_condition_rules(
            runtime.conditions, runtime.definition, action_keys=declared
        )
    )
    errors.extend(validate_post_submit_plan(runtime.plan, runtime.definition))
    if runtime.step_plan is not None:
        errors.extend(validate_step_plan(runtime.definition, runtime.step_plan))
    return tuple(dict.fromkeys(errors))


def parse_public_submit_request(payload: Any) -> FormPublicSubmitRequest | FormPublicResult:
    if not isinstance(payload, dict):
        return FormPublicResult(
            http_status=400,
            body=project_public_forms_response(
                ok=False, http_status=400, error="invalid_request"
            ),
        )
    try:
        return FormPublicSubmitRequest.model_validate(payload)
    except ValidationError:
        return FormPublicResult(
            http_status=400,
            body=project_public_forms_response(
                ok=False, http_status=400, error="invalid_request"
            ),
        )


def form_runtime_is_callable(
    runtimes: FormRuntimeStore,
    project_id: str,
    form_key: str,
) -> bool:
    runtime = runtimes.get(project_id, form_key)
    if runtime is None or runtime.definition.key != form_key:
        return False
    return not _validate_runtime(runtime)


def project_public_forms_response(
    *,
    ok: bool,
    http_status: int,
    error: str | None = None,
    submission_id: str | None = None,
    fields: dict[str, list[str]] | None = None,
) -> dict[str, Any]:
    """Explicit public projection. Never dump 7H/provider internals."""
    body: dict[str, Any] = {"ok": ok}
    if ok:
        body["status"] = "accepted"
        if submission_id:
            body["submission_id"] = submission_id
        return body
    body["error"] = error or "submission_failed"
    if fields:
        body["fields"] = fields
    return body


def submit_form(
    *,
    project_id: str,
    form_key: str,
    values: Mapping[str, Any],
    client_identity: str,
    challenge: str | None = None,
    runtimes: FormRuntimeStore,
    invoker: FormCapabilityInvoker,
    submissions: FormSubmissionStore | None = None,
    files: FormFileDirectory | None = None,
    verifier: FormChallengeVerifier | None = None,
    limiter: FormAttemptLimiter | None = None,
    clock: Callable[[], datetime] = platform_now,
    submission_id_factory: Callable[[], str] = new_submission_id,
    email_template_renderer: Any = None,
    persist_required: bool = False,
) -> FormPublicResult:
    """Private final-submit entry. Resolves trusted runtime; BFF does not supply it."""
    runtime = runtimes.get(project_id, form_key)
    if runtime is None or runtime.definition.key != form_key:
        return FormPublicResult(
            http_status=404,
            body=project_public_forms_response(
                ok=False, http_status=404, error="form_unavailable"
            ),
        )
    config_errors = _validate_runtime(runtime)
    if config_errors:
        return FormPublicResult(
            http_status=503,
            body=project_public_forms_response(
                ok=False, http_status=503, error="form_unavailable"
            ),
        )
    raw = dict(values)
    if challenge:
        raw.setdefault("turnstile_token", challenge)
    processed = process_form_submission_request(
        runtime.definition,
        raw,
        policy=runtime.anti_abuse,
        project_id=project_id,
        client_identity=client_identity,
        verifier=verifier or UnwiredChallengeVerifier(),
        limiter=limiter or InMemoryAttemptLimiter(),
        files=files or InMemoryFormFileDirectory(),
        conditions=runtime.conditions,
        clock=clock,
        submission_id_factory=submission_id_factory,
    )
    if not processed.anti_abuse.allowed:
        code = processed.anti_abuse.code
        if code == HONEYPOT_TRIGGERED:
            return FormPublicResult(
                http_status=200,
                body=project_public_forms_response(ok=True, http_status=200),
                honeypot_triggered=True,
            )
        if code == RATE_LIMITED:
            return FormPublicResult(
                http_status=429,
                body=project_public_forms_response(
                    ok=False, http_status=429, error="rate_limited"
                ),
            )
        public_challenge = {
            CHALLENGE_MISSING: "challenge_required",
            CHALLENGE_FAILED: "challenge_failed",
            CHALLENGE_UNAVAILABLE: "challenge_unavailable",
        }.get(code or "", "challenge_failed")
        return FormPublicResult(
            http_status=400,
            body=project_public_forms_response(
                ok=False, http_status=400, error=public_challenge
            ),
        )
    runtime_result = processed.runtime
    if runtime_result is None or not runtime_result.accepted:
        fields = _field_map(list(runtime_result.errors) if runtime_result else [])
        return FormPublicResult(
            http_status=422,
            body=project_public_forms_response(
                ok=False,
                http_status=422,
                error="validation_failed",
                fields=fields,
            ),
        )
    accepted = runtime_result.submission
    assert accepted is not None
    store = submissions or InMemoryFormSubmissionStore()
    if runtime.persist_first or persist_required:
        try:
            persist_form_submission(
                store,
                FormPersistenceContext(project_id=project_id),
                accepted,
            )
        except FormPersistenceError:
            return FormPublicResult(
                http_status=500,
                body=project_public_forms_response(
                    ok=False, http_status=500, error="submission_failed"
                ),
                submission_id=accepted.submission_id,
            )
    post = execute_post_submit_actions(
        accepted,
        runtime.plan,
        context=FormPostSubmitContext(project_id=project_id),
        invoker=invoker,
        definition=runtime.definition,
        store=store,
        persist_first=False,
        conditions=runtime.conditions,
        email_template_renderer=email_template_renderer,
    )
    action_codes = tuple(
        item.error_code for item in post.actions if item.error_code
    )
    return FormPublicResult(
        http_status=200,
        body=project_public_forms_response(
            ok=True,
            http_status=200,
            submission_id=accepted.submission_id,
        ),
        submission_id=accepted.submission_id,
        internal_action_error_codes=action_codes,
    )


def client_identity_from_request(
    *,
    forwarded: str | None,
    peer: str | None,
) -> str:
    text = (forwarded or peer or "unknown").strip()
    return text[:256] or "unknown"
