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

Mirrors ``src.domain.projects.form_requirements`` for business-entity schemas:
same "Core decides; Codegen renders" invariant (see project memory
``project_capability_provisioner_architecture``), same reasons for existing.

Reuses ``ProvisionField`` (``src.domain.codegen.provision_manifest``) rather
than inventing a second entity-field vocabulary -- it is already the shared
canonical field type for both agent-authored ``p2hs/provision.json`` entities
and ``compose`` entries in ``p2hs/feature-packs.json``
(``src.domain.feature_packs.requirements_spec.CompositionRequirement.fields``).

Unlike Forms, an entity has no canonical default schema to fall back to, so
the resolution policy differs by necessity: the FIRST time an entity_key is
seen, Core *adopts* whatever was declared (there is nothing to override yet);
only from then on does Core's resolved value win over a later declaration.
See ``src.domain.projects.entity_requirement_sync.resolve_entity_fields``.
"""
from __future__ import annotations

from pydantic import BaseModel, ConfigDict, Field

from src.domain.codegen.provision_manifest import ProvisionField

_ENTITY_KEY_PATTERN = r"^[a-z][a-z0-9_]{0,62}$"


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

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

    entity_key: str = Field(min_length=1, max_length=64, pattern=_ENTITY_KEY_PATTERN)
    fields: tuple[ProvisionField, ...] = Field(default_factory=tuple, max_length=64)


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

    Extracted from a single build prompt (e.g. "add a budget field to the
    quote entity"); never applied directly -- always merged into the prior
    ``EntityFieldRequirement`` first via ``apply_entity_field_requirement_delta``,
    so the resolved state stays the single thing every provisioning mechanism
    reads.
    """

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

    entity_key: str = Field(min_length=1, max_length=64, pattern=_ENTITY_KEY_PATTERN)
    add_fields: tuple[ProvisionField, ...] = Field(default_factory=tuple, max_length=32)
    remove_fields: tuple[str, ...] = Field(default_factory=tuple, max_length=32)
    update_fields: tuple[ProvisionField, ...] = Field(default_factory=tuple, max_length=32)


class EntityFieldRequirementExtractionResult(BaseModel):
    """Structured-output contract for the per-prompt extractor. Same semantics
    as ``src.domain.projects.form_requirements.FormRequirementExtractionResult``:
    ``applies=False`` -- not about an entity's fields at all; no-op.
    ``applies=True, confident=False`` -- fail closed, never guess.
    ``applies=True, confident=True`` -- ``delta`` is the extracted change.
    """

    model_config = ConfigDict(extra="forbid")

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


def apply_entity_field_requirement_delta(
    existing: tuple[EntityFieldRequirement, ...], delta: EntityFieldRequirementDelta
) -> tuple[EntityFieldRequirement, ...]:
    """Return a new requirement tuple with ``delta`` merged in.

    Identical merge semantics to ``apply_form_requirement_delta``:
    ``add_fields``/``update_fields`` both upsert by ``key`` (replace in place,
    preserving position, or append), ``remove_fields`` applied last so a key in
    both an upsert list and ``remove_fields`` in the same delta ends up
    removed, never resurrected. A field removed here stops being part of the
    entity's Core-resolved contract for codegen's purposes; it does not by
    itself delete the underlying live Cloud column (today's provisioning
    mechanisms are additive-only -- see ``provision_manifest.merged_entity_schema``).
    """
    by_key = {requirement.entity_key: requirement for requirement in existing}
    current = by_key.get(delta.entity_key)
    fields: list[ProvisionField] = list(current.fields) if current else []

    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 = EntityFieldRequirement(entity_key=delta.entity_key, fields=tuple(fields))
    return tuple(req for req in existing if req.entity_key != delta.entity_key) + (merged,)


def resolved_entity_fields_config(
    requirements: tuple[EntityFieldRequirement, ...] | list[EntityFieldRequirement],
) -> dict[str, list[dict]]:
    """JSON-safe projection, keyed by ``entity_key``, of each resolved field list --
    the shape every provisioning mechanism's ``declaration["fields"]``/``req.fields``
    already uses (a plain list of ``ProvisionField``-shaped dicts).
    """
    return {
        requirement.entity_key: [field.model_dump(mode="json") for field in requirement.fields]
        for requirement in requirements
        if requirement.fields
    }


__all__ = [
    "EntityFieldRequirement",
    "EntityFieldRequirementDelta",
    "EntityFieldRequirementExtractionResult",
    "apply_entity_field_requirement_delta",
    "resolved_entity_fields_config",
]
