"""Typed payload hints for ProviderAdapter.execute.

Phase C of the engine-architectural-audit fix sprint (2026-04-30):
collapses the untyped `payload.hints: dict` escape-hatch on
ProviderAdapter into a typed Pydantic model with one variant per
provider that uses it. Resolves T2.21 / MF-10.

Three provider adapters carry load-bearing hints:
- Wan (i2v/r2v Fal model — non-HTTP via direct subscribe)
- Google (Veo + image — non-HTTP via genai SDK)
- Kling (HTTP but with endpoint/mode hints)

Plus StepRunner-side hints used during dispatch routing
(modality, multi_shots, parts, elements). Adapters that use only
typed fields on UnifiedVideoPayload (Fal video, Atlas, PiAPI) accept
hints=None.

Tenet 6 / Tenet 2 (modularity): adding a new hint key requires adding
a typed field to the appropriate subclass — no more silent dict-typo
drift. Phase C ships extra="allow" (the Pydantic default); the strict
extra="forbid" transition is deferred to a follow-on sprint after a
deliberate adapter-key inventory.
"""

from __future__ import annotations

from typing import Any, Literal, Optional

try:
    from pydantic import BaseModel, ConfigDict

    _HAS_PYDANTIC = True
except ImportError:
    _HAS_PYDANTIC = False

    class BaseModel:  # type: ignore[no-redef]
        ...

    class ConfigDict(dict):  # type: ignore[no-redef]
        ...


# Canonical home: recoil/core/exceptions.py::PayloadHintsValidationError.
# Re-exported here for one-cycle backward compatibility (Phase E.5).

from recoil.core.exceptions import PayloadHintsValidationError  # noqa: F401  # DEPRECATED: Phase E.5 migration


class PayloadHints(BaseModel):
    """Base class for typed provider hints. Provider variants subclass.

    NOTE: extra="allow" in Phase C (Pydantic default). Transition to
    extra="forbid" deferred to follow-on sprint per locked 2026-04-30
    spec-review decision.
    """

    if _HAS_PYDANTIC:
        model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)

    def as_dict(self) -> dict:
        """Backward-compat: dump to plain dict for legacy callers using
        `(payload.hints or {}).get(KEY)` patterns."""
        if _HAS_PYDANTIC:
            return self.model_dump(exclude_none=True)
        return {k: v for k, v in self.__dict__.items() if v is not None}


class WanHints(PayloadHints):
    """Wan-specific hints (Fal i2v / r2v / multi-shot)."""

    enable_prompt_expansion: bool = False
    enable_safety_checker: bool = True
    seed: Optional[int] = None
    audio_url: Optional[str] = None
    video_url: Optional[str] = None
    multi_shots: bool = False


class KlingHints(PayloadHints):
    """Kling-specific hints (Fal endpoint disambiguation + elements)."""

    endpoint: Optional[str] = None
    mode: Literal["standard", "professional"] = "standard"
    elements: Optional[list[Any]] = None
    image_url: Optional[str] = None


class GoogleHints(PayloadHints):
    """Google-specific hints (Veo video + Gemini image)."""

    modality: Literal["image", "video"] = "video"
    parts: Optional[list[Any]] = None
    genai_config: Optional[dict] = None


class StepRunnerHints(PayloadHints):
    """Hints carried by step_runner before provider routing.

    Extended in convergence-enforcement-v1 to own the full typed payload shape.
    refs_used, start_frame, elements_payload added with extra="allow" transition.
    TODO[converge-v0.2]: tighten extra to "forbid" after Tartarus EP001 ships (est. 2026-06-30).
    """

    modality: Optional[Literal["image", "video", "audio"]] = None
    multi_shots: Optional[list[Any]] = None
    parts: Optional[list[Any]] = None
    elements: Optional[list[Any]] = None
    o3_elements: Optional[Any] = None
    endpoint: Optional[str] = None  # provider endpoint override (e.g. "o3_edit_pro")
    keep_audio: Optional[bool] = None  # v2v edit: preserve source audio
    seed: Optional[int] = None  # REC-38: retry CHANGE_SEED → flora params.update(hints)
    tier: Optional[str] = None  # REC-38: retry tier switch → VideoModelClient(tier=)
    project: Optional[str] = None
    episode: Optional[int] = None

    # Unified payload fields (convergence-enforcement-v1, 2026-05-23)
    refs_used: Optional[list[Any]] = None          # list of {"path": "..."} ref dicts
    start_frame: Optional[str] = None              # path to start frame image
    elements_payload: Optional[dict[str, Any]] = None  # elements dict for multi-element shots


def coerce_to_dict(hints: Any) -> dict:
    """Helper for adapters: returns dict from hints (None → {}, dict passthrough, model dump otherwise)."""
    if hints is None:
        return {}
    if isinstance(hints, dict):
        return hints
    if isinstance(hints, PayloadHints):
        return hints.as_dict()
    # Defensive: any other shape → empty (preserves legacy "or {}" behavior).
    return {}


__all__ = [
    "PayloadHintsValidationError",
    "PayloadHints",
    "WanHints",
    "KlingHints",
    "GoogleHints",
    "StepRunnerHints",
    "coerce_to_dict",
]
