"""Compatibility shims for legacy contract field names.

Spec source: contract-alignment-integration.fix-seam-proof-blockers (B1)
requires explicit quarantine of legacy field aliases and transitional fields.

Sunset criteria:
- ``system_prompt`` alias: remove when all ingress paths send ``prompt`` only.
- ``tools_profile`` compatibility: retain only for ``CapabilityToken`` payloads.
- ``spec_digest`` runtime state field: remove together with state-schema migration
  that drops ``AgentStateData.spec_digest`` and ``create_agent_state(..., spec_digest=...)``.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Mapping

from anima.core.contracts.specs import JobSpec
from anima.core.execution.contracts import (
    ExecutionIntent,
    ExecutionIntentDefaults,
    ExecutionIntentOverrides,
    ResolvedExecutionContract,
)
from anima.core.execution.resolver import resolve_execution_contract
from anima.core.execution.state import AgentStateData
from anima.core.schemas.models import PersonaSpec


@dataclass(frozen=True)
class TransitionalFieldPolicy:
    """Policy describing a temporary compatibility field."""

    field: str
    allowed_scope: str
    sunset_criteria: str


@dataclass(frozen=True)
class ExecutionContractCompatibilityPolicy:
    """Compatibility policy for execution-contract consumers during migration."""

    contract_version: str
    resolver_version_field: str
    spawnability_source: str
    legacy_spawnability_field: str
    consumer_rule: str


TRANSITIONAL_FIELD_ALLOWLIST: dict[str, TransitionalFieldPolicy] = {
    "system_prompt": TransitionalFieldPolicy(
        field="system_prompt",
        allowed_scope="persona payload ingress normalization only",
        sunset_criteria="Delete when all callers send prompt and telemetry shows no legacy alias usage.",
    ),
    "tools_profile": TransitionalFieldPolicy(
        field="tools_profile",
        allowed_scope="CapabilityToken compatibility only",
        sunset_criteria="Delete when token contract migration removes tools_profile field.",
    ),
    "spec_digest": TransitionalFieldPolicy(
        field="spec_digest",
        allowed_scope="agent state persistence compatibility only",
        sunset_criteria=(
            "Delete in the same migration that removes AgentStateData.spec_digest "
            "and create_agent_state(..., spec_digest=...)."
        ),
    ),
}


EXECUTION_CONTRACT_COMPATIBILITY = ExecutionContractCompatibilityPolicy(
    contract_version="0.1.0",
    resolver_version_field="identity.resolver.resolver_version",
    spawnability_source="persona_provenance.spawnability",
    legacy_spawnability_field="persona_ref",
    consumer_rule=(
        "Control consumers must read child-agent spawnability from resolved "
        "contract persona provenance and must not infer spawnability from raw "
        "persona_ref presence."
    ),
)


# @invar:allow shell_result: Compatibility normalizer returns transformed mapping
def normalize_legacy_persona_payload(payload: Any) -> Any:
    """Normalize legacy persona aliases to canonical PersonaSpec keys.

    Canonical precedence rule (spec source: B1 stale-definition precedence):
    if both canonical and legacy keys exist, canonical key wins and legacy key is
    dropped without overwriting.
    """
    if not isinstance(payload, dict):
        return payload

    normalized = dict(payload)

    if "id" not in normalized and "name" in normalized:
        normalized["id"] = normalized.pop("name")

    if "prompt" not in normalized and "system_prompt" in normalized:
        normalized["prompt"] = normalized.pop("system_prompt")
    elif "system_prompt" in normalized:
        normalized.pop("system_prompt")

    if "description" not in normalized:
        persona_id = normalized.get("id")
        if isinstance(persona_id, str) and persona_id.strip():
            normalized["description"] = f"Persona {persona_id.strip()}"
        else:
            normalized["description"] = "Persona"

    return normalized


# @invar:allow shell_result: Compatibility metadata lookup is pure and side-effect free.
def transitional_field_policy(field: str) -> TransitionalFieldPolicy | None:
    """Return compatibility policy metadata for a transitional field."""
    return TRANSITIONAL_FIELD_ALLOWLIST.get(field)


# @invar:allow shell_result: Compatibility filter returns a transformed mapping without I/O.
def keep_allowed_transitional_fields(
    values: Mapping[str, Any],
) -> dict[str, Any]:
    """Return only transitional fields that are explicitly allowlisted."""
    return {
        key: value
        for key, value in values.items()
        if key in TRANSITIONAL_FIELD_ALLOWLIST
    }


# @invar:allow shell_result: Compatibility synthesizer returns resolved contract for legacy fallback path.
# @shell_orchestration: Legacy fallback assembly bridges shell state and core resolver compatibility.
def synthesize_contract_from_legacy_state(
    state_data: AgentStateData,
    *,
    persona_spec: PersonaSpec | None = None,
    job_spec: JobSpec | None = None,
    defaults: ExecutionIntentDefaults | None = None,
) -> ResolvedExecutionContract:
    """Synthesize resolved contract when no persisted snapshot exists.

    Spec source:
    docs/proposals/execution-architecture-refactor.md#tests-required/resolution-tests
    backward compatibility requires legacy agents (without snapshot files) to
    remain controllable.
    """
    fallback_job = job_spec
    if fallback_job is None:
        fallback_job = JobSpec(
            persona_ref=state_data.persona_ref or state_data.persona_name,
            instance_id=state_data.instance_id,
            lifecycle=state_data.lifecycle,
            max_turns=state_data.max_turns,
            max_tokens=state_data.budget_total,
        )

    fallback_persona = persona_spec
    if fallback_persona is None:
        fallback_persona = PersonaSpec(
            id=state_data.persona_name,
            description=f"Legacy persona {state_data.persona_name}",
            prompt="Legacy persona prompt unavailable in persisted state.",
            model=state_data.model or "legacy-model",
        )

    intent = ExecutionIntent(
        persona_spec=fallback_persona,
        job_spec=fallback_job,
        overrides=ExecutionIntentOverrides(),
        defaults=defaults or ExecutionIntentDefaults(),
    )
    return resolve_execution_contract(intent, persona_source="file")


# @invar:allow shell_result: Compatibility loader returns resolved contract object for downstream consumers.
# @shell_orchestration: Snapshot-vs-legacy selection is adapter orchestration, not core resolution.
def load_persisted_or_legacy_contract(
    state_data: AgentStateData,
    *,
    persona_spec: PersonaSpec | None = None,
    job_spec: JobSpec | None = None,
    defaults: ExecutionIntentDefaults | None = None,
) -> ResolvedExecutionContract:
    """Load persisted contract snapshot, or synthesize legacy fallback.

    Snapshot precedence rule:
    - If persisted snapshot exists, it is authoritative runtime truth.
    - Legacy mutable state fields are fallback only when snapshot is absent.
    """
    from anima.shell.state import read_contract_snapshot

    persisted = read_contract_snapshot(state_data.instance_id)
    from returns.result import Success

    if isinstance(persisted, Success):
        return persisted.unwrap()

    return synthesize_contract_from_legacy_state(
        state_data,
        persona_spec=persona_spec,
        job_spec=job_spec,
        defaults=defaults,
    )


__all__ = [
    "EXECUTION_CONTRACT_COMPATIBILITY",
    "TRANSITIONAL_FIELD_ALLOWLIST",
    "ExecutionContractCompatibilityPolicy",
    "TransitionalFieldPolicy",
    "load_persisted_or_legacy_contract",
    "keep_allowed_transitional_fields",
    "normalize_legacy_persona_payload",
    "synthesize_contract_from_legacy_state",
    "transitional_field_policy",
]
