"""Structured, per-form field requirements captured from build prompts.

``DiscoverySpec.forms`` stays the historical customer-wording form-name list
(``["quote request"]``). ``FormRequirement`` is the typed, field-level
requirement a later build prompt can create and extend incrementally -- the
thing ``compile_generation_plan`` projects into
``GenerationComposition.form_definitions``, never ``p2hs/feature-packs.json``
or an ``AppBlock`` slot. Fields are a runtime data-contract requirement, not
presentation customization, and generated files never become authoritative.

See project memory ``project_capability_provisioner_architecture`` for the
locked "Core decides; Codegen renders" invariant this exists to serve.
"""
from __future__ import annotations

from typing import Literal

from pydantic import BaseModel, ConfigDict, Field

from src.domain.forms.canonical import FormDefinition, FormField

FormAccess = Literal["public_anonymous", "authenticated"]


class FormRequirement(BaseModel):
    """One form's Core-resolved, full (not incremental) field requirement.

    Deliberately excludes notification/pack/anti-abuse/review policy -- those
    already have owners (``contact-capture`` pack config, ``FormAntiAbusePolicy``)
    and must not be decided twice.
    """

    model_config = ConfigDict(extra="forbid", frozen=True)

    form_key: str = Field(min_length=1, max_length=64)
    fields: tuple[FormField, ...] = Field(default_factory=tuple, max_length=32)
    access: FormAccess = "public_anonymous"


class FormRequirementDelta(BaseModel):
    """A structured, incremental change to one form's fields.

    Extracted from a single build prompt (e.g. "add a phone field to the quote
    form"); never applied directly to runtime -- always merged into the prior
    ``FormRequirement`` first, so the resolved state stays the single source
    ``compile_generation_plan`` reads.
    """

    model_config = ConfigDict(extra="forbid", frozen=True)

    form_key: str = Field(min_length=1, max_length=64)
    add_fields: tuple[FormField, ...] = Field(default_factory=tuple, max_length=32)
    remove_fields: tuple[str, ...] = Field(default_factory=tuple, max_length=32)
    update_fields: tuple[FormField, ...] = Field(default_factory=tuple, max_length=32)
    access: FormAccess | None = Field(
        default=None,
        description="Set only when the prompt explicitly asks to switch who can submit "
        "(e.g. 'require sign-in to submit this form'). None leaves access unchanged.",
    )
    replace_fields: bool = False


class FormRequirementExtractionResult(BaseModel):
    """Structured-output contract for the per-prompt extractor.

    ``applies=False`` -- the prompt does not concern any form; no-op.
    ``applies=True, confident=False`` -- the prompt concerns a form but the
    extractor could not confidently resolve a delta; fail closed (no
    mutation), never guess.
    ``applies=True, confident=True`` -- ``delta`` is the extracted change.
    """

    model_config = ConfigDict(extra="forbid")

    applies: bool = False
    confident: bool = False
    delta: FormRequirementDelta | None = None


def _canonical_seed_fields(form_key: str) -> tuple[FormField, ...]:
    """The fields a form_key already has by the Forms runtime's own canonical
    fallback (``src.domain.forms.canonical``), used to seed a brand-new
    ``FormRequirement`` -- never an empty list.

    Unlike entities, a form always has a non-destructive default schema
    already serving real submissions before any delta is ever applied. The
    FIRST delta for a form_key must start from that schema, not from nothing:
    starting from empty would make the first-ever customization silently
    replace (not extend) the fields the generated UI and prior submissions
    already depend on -- the exact class of mismatch this module exists to
    prevent, just at the bootstrap step instead of a later run.
    """
    from src.domain.forms.canonical import (
        contact_block_compatible_definition,
        quote_request_block_compatible_definition,
    )

    if form_key == "contact":
        return tuple(contact_block_compatible_definition().fields)
    if form_key == "quote_request":
        return tuple(quote_request_block_compatible_definition().fields)
    return ()


def apply_form_requirement_delta(
    existing: tuple[FormRequirement, ...], delta: FormRequirementDelta
) -> tuple[FormRequirement, ...]:
    """Return a new requirement tuple with ``delta`` merged in.

    ``add_fields``/``update_fields`` both upsert by ``key`` -- a key already
    present is replaced in place (preserving position), a new key is appended.
    Treating ``add_fields`` as an upsert too (rather than erroring on a
    collision) mirrors the "re-applying is safe" idempotency convention
    elsewhere in this codebase (e.g. feature pack installs).
    ``remove_fields`` is applied last, so a key present in both an upsert list
    and ``remove_fields`` in the same delta ends up removed, never resurrected.
    A form not previously declared starts from its canonical fallback fields
    (see ``_canonical_seed_fields``), not an empty list, and the default
    ``public_anonymous`` access. ``delta.access`` only overrides access when
    explicitly set (not ``None``) -- a delta that only touches fields must
    never silently reset access to the default.
    """
    by_key = {requirement.form_key: requirement for requirement in existing}
    current = by_key.get(delta.form_key)
    # An explicit field list is the contract. The canonical fallback is only
    # the starting point when the prompt did not enumerate fields.
    fields: list[FormField] = (
        []
        if delta.replace_fields
        else list(current.fields)
        if current
        else list(_canonical_seed_fields(delta.form_key))
    )
    access = current.access if current else "public_anonymous"
    if delta.access is not None:
        access = delta.access

    index_by_key = {field.key: position for position, field in enumerate(fields)}
    for incoming in (*delta.update_fields, *delta.add_fields):
        if incoming.key in index_by_key:
            fields[index_by_key[incoming.key]] = incoming
        else:
            index_by_key[incoming.key] = len(fields)
            fields.append(incoming)

    remove = set(delta.remove_fields)
    if remove:
        fields = [field for field in fields if field.key not in remove]

    merged = FormRequirement(form_key=delta.form_key, fields=tuple(fields), access=access)
    return tuple(req for req in existing if req.form_key != delta.form_key) + (merged,)


def resolved_form_definitions_config(
    requirements: tuple[FormRequirement, ...] | list[FormRequirement],
) -> dict[str, dict]:
    """JSON-safe projection of the resolved requirements, for direct persistence
    into ``ProjectFeaturePack.config["form_definitions"]``.

    Mirrors ``src.domain.codegen.generation_plan``'s internal
    ``_form_definitions_from_requirements`` but returns plain JSON-able dicts
    rather than typed ``FormDefinition`` objects, and omits a form with no
    fields yet for the same reason: an empty schema is worse than no override,
    since it would stop the Forms runtime's canonical-default fallback.
    """
    result: dict[str, dict] = {}
    for requirement in requirements:
        if not requirement.fields:
            continue
        definition = FormDefinition(key=requirement.form_key, fields=list(requirement.fields))
        result[requirement.form_key] = definition.model_dump(mode="json")
    return result


__all__ = [
    "FormAccess",
    "FormRequirement",
    "FormRequirementDelta",
    "FormRequirementExtractionResult",
    "apply_form_requirement_delta",
    "resolved_form_definitions_config",
]
