"""Canonical Forms persistence/retrieval.

Existing stores (`contact_submission`, composed entities, `contact_inquiries`)
cannot preserve `fsb_*` as identity. The domain contract remains hosting-neutral;
the production adapter stores the canonical envelope in Core Postgres while the
in-memory adapter remains available for tests and local development.
"""
from __future__ import annotations

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

from pydantic import BaseModel, ConfigDict, Field, StrictStr, field_validator

from src.domain.forms.runtime import AcceptedFormSubmission, platform_now

PERSISTENCE_OPTIONAL = False
DEFINITION_SNAPSHOT = "form_key_and_values_only"
LIST_SEARCH = True
SCHEMA_MIGRATION = True
CANONICAL_STORE_REQUIRED = True
PHYSICAL_SCHEMA = "core_postgres.form_submissions"

ERROR_CODES: tuple[str, ...] = (
    "submission_identity_conflict",
    "submission_persistence_failed",
)

FORMS_PERSISTENCE: dict[str, Any] = {
    "slice_id": "7e_persistence_and_retrieval",
    "status": "frozen",
    "durable_identity": "AcceptedFormSubmission.submission_id",
    "persistence_optional": PERSISTENCE_OPTIONAL,
    "definition_snapshot": DEFINITION_SNAPSHOT,
    "list_search": LIST_SEARCH,
    "canonical_store_required": CANONICAL_STORE_REQUIRED,
    "physical_schema": PHYSICAL_SCHEMA,
    "schema_migration": SCHEMA_MIGRATION,
    "contact_submission_is_canonical": False,
    "composed_entity_id_is_forms_identity": False,
    "contact_inquiries_is_canonical": False,
    "form_forward_persists": False,
    "anti_abuse": False,
    "files": False,
    "post_submit": False,
    "public_bff": False,
    "form_submit_capability": False,
    "codegen": False,
    "docs": "docs/audits/FORMS_PERSISTENCE_7E.md",
}


class FormPersistenceError(ValueError):
    """Infrastructure/integrity failure — not a 7C field error."""

    def __init__(self, code: str) -> None:
        if code not in ERROR_CODES:
            raise ValueError("unknown_persistence_error_code")
        self.code = code
        super().__init__(code)


class FormPersistenceContext(BaseModel):
    """Ownership scope for persist/get. Not submitted form values."""

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


class FormSubmissionStorageRef(BaseModel):
    """Internal adapter metadata. Not canonical Forms identity."""

    model_config = ConfigDict(extra="forbid", frozen=True)
    backend: StrictStr
    native_id: StrictStr


class PersistedFormSubmission(BaseModel):
    """Durable envelope. Wraps 7D acceptance; does not mutate it."""

    model_config = ConfigDict(extra="forbid", frozen=True)
    project_id: StrictStr = Field(min_length=1, max_length=64)
    accepted: AcceptedFormSubmission
    persisted_at: datetime
    status: StrictStr = Field(default="accepted", max_length=32)

    @field_validator("persisted_at")
    @classmethod
    def utc_aware(cls, value: datetime) -> datetime:
        if value.tzinfo is None:
            raise ValueError("persisted_at_must_be_timezone_aware")
        return value.astimezone(UTC)

    @property
    def submission_id(self) -> str:
        return self.accepted.submission_id


class FormSubmissionStore(Protocol):
    """Technology-neutral put/get. No Postgres/Cloud/entity assumptions."""

    def persist(
        self,
        context: FormPersistenceContext,
        accepted: AcceptedFormSubmission,
        *,
        clock: Callable[[], datetime] = platform_now,
    ) -> PersistedFormSubmission: ...

    def get(
        self,
        context: FormPersistenceContext,
        submission_id: str,
    ) -> PersistedFormSubmission | None: ...


def _accepted_fingerprint(accepted: AcceptedFormSubmission) -> dict[str, Any]:
    return accepted.model_dump(mode="python")


class InMemoryFormSubmissionStore:
    """Reference store proving 7E invariants. Not a production backend."""

    def __init__(self) -> None:
        self._by_id: dict[str, PersistedFormSubmission] = {}

    def persist(
        self,
        context: FormPersistenceContext,
        accepted: AcceptedFormSubmission,
        *,
        clock: Callable[[], datetime] = platform_now,
    ) -> PersistedFormSubmission:
        existing = self._by_id.get(accepted.submission_id)
        snapshot = accepted.model_copy(deep=True)
        if existing is not None:
            if existing.project_id != context.project_id:
                raise FormPersistenceError("submission_identity_conflict")
            if _accepted_fingerprint(existing.accepted) != _accepted_fingerprint(snapshot):
                raise FormPersistenceError("submission_identity_conflict")
            return existing
        row = PersistedFormSubmission(
            project_id=context.project_id,
            accepted=snapshot,
            persisted_at=clock(),
        )
        self._by_id[accepted.submission_id] = row
        return row

    def get(
        self,
        context: FormPersistenceContext,
        submission_id: str,
    ) -> PersistedFormSubmission | None:
        row = self._by_id.get(submission_id)
        if row is None or row.project_id != context.project_id:
            return None
        return row


def persist_form_submission(
    store: FormSubmissionStore,
    context: FormPersistenceContext,
    accepted: AcceptedFormSubmission,
    *,
    clock: Callable[[], datetime] = platform_now,
) -> PersistedFormSubmission:
    """Idempotent put by submission_id. Does not revalidate or invoke actions."""
    return store.persist(context, accepted, clock=clock)


def get_form_submission(
    store: FormSubmissionStore,
    context: FormPersistenceContext,
    submission_id: str,
) -> PersistedFormSubmission | None:
    """Retrieve by canonical submission_id. Side-effect free. No revalidation."""
    return store.get(context, submission_id)


async def persist_form_submission_async(
    db: Any,
    context: FormPersistenceContext,
    accepted: AcceptedFormSubmission,
) -> PersistedFormSubmission:
    """Persist through the production SQLAlchemy adapter.

    Kept separate from the synchronous protocol so the domain/runtime tests can
    continue using the deterministic in-memory adapter.
    """
    from src.domain.forms.repository import FormSubmissionRepository

    return await FormSubmissionRepository(db).persist(context, accepted)
