"""
Plan serialization — ExecutionPlan <-> JSON-safe dict.

Plans travel as JSON over HTTPS between the cloud and the runtime.
This module handles the conversion in both directions, with strict
type checking on deserialization to catch malformed payloads early.

Design notes:
    - No external deps (no Pydantic, no marshmallow). Plain dicts + dataclasses.
    - Deserialization is strict: missing fields, wrong types, and unknown
      step types all raise ValueError with clear messages.
    - The serialized form is a plain dict (not a JSON string). The caller
      decides how to encode it (json.dumps for HTTP, msgpack for WS, etc.).
"""

from __future__ import annotations

from typing import Any

from runtime.protocol.schema import ExecutionPlan, PlanStep


def _serialize_step(step: PlanStep) -> dict[str, Any]:
    """Convert a PlanStep to a JSON-safe dict."""
    d: dict[str, Any] = {
        "step_id": step.step_id,
        "type": step.type,
        "backend": step.backend,
        "model": step.model,
        "prompt": step.prompt,
        "context_refs": list(step.context_refs),
        "config": dict(step.config),
    }
    # Include agent fields only when set — keeps payload small for model steps
    if step.agent_id is not None:
        d["agent_id"] = step.agent_id
    if step.endpoint is not None:
        d["endpoint"] = step.endpoint
    if step.auth_type is not None:
        d["auth_type"] = step.auth_type
    return d


def _deserialize_step(data: dict[str, Any], index: int) -> PlanStep:
    """Parse a dict into a PlanStep with strict field validation.

    Args:
        data:  The raw dict from the serialized plan.
        index: Step index (for error messages).

    Returns:
        A validated PlanStep.

    Raises:
        ValueError: On missing or invalid fields.
    """
    required_fields = ("step_id", "type", "backend", "model", "prompt")
    for f in required_fields:
        if f not in data:
            raise ValueError(f"Step {index}: missing required field {f!r}")

    return PlanStep(
        step_id=str(data["step_id"]),
        type=str(data["type"]),
        backend=str(data["backend"]),
        model=str(data["model"]),
        prompt=str(data["prompt"]),
        context_refs=[str(r) for r in data.get("context_refs", [])],
        config=dict(data.get("config", {})),
        agent_id=data.get("agent_id"),
        endpoint=data.get("endpoint"),
        auth_type=data.get("auth_type"),
    )


def serialize_plan(plan: ExecutionPlan) -> dict[str, Any]:
    """Convert an ExecutionPlan to a JSON-safe dict for transport.

    Args:
        plan: The execution plan to serialize.

    Returns:
        A plain dict ready for json.dumps() or equivalent.
    """
    return {
        "plan_id": plan.plan_id,
        "user_id": plan.user_id,
        "steps": [_serialize_step(s) for s in plan.steps],
        "original_input": plan.original_input,
        "created_at": plan.created_at,
        "expires_at": plan.expires_at,
        "signed_at": plan.signed_at,
        "signature": plan.signature,
        "version": plan.version,
    }


def deserialize_plan(data: dict[str, Any]) -> ExecutionPlan:
    """Parse a dict into an ExecutionPlan with strict validation.

    This is the runtime's entry point for receiving plans from the cloud.
    Every field is validated; unknown or missing fields produce clear errors.

    Args:
        data: The raw dict received over the wire.

    Returns:
        A validated ExecutionPlan instance.

    Raises:
        ValueError: On missing fields, wrong types, or invalid step data.
        KeyError:   On completely missing top-level keys.
    """
    required_top = ("plan_id", "user_id", "steps", "original_input",
                    "created_at", "expires_at")
    for f in required_top:
        if f not in data:
            raise ValueError(f"Missing required top-level field {f!r}")

    raw_steps = data["steps"]
    if not isinstance(raw_steps, list):
        raise ValueError(f"'steps' must be a list, got {type(raw_steps).__name__}")
    if not raw_steps:
        raise ValueError("'steps' must contain at least one step")

    steps = [_deserialize_step(s, i) for i, s in enumerate(raw_steps)]

    return ExecutionPlan(
        plan_id=str(data["plan_id"]),
        user_id=str(data["user_id"]),
        steps=steps,
        original_input=str(data["original_input"]),
        created_at=float(data["created_at"]),
        expires_at=float(data["expires_at"]),
        signed_at=str(data.get("signed_at", "")),
        signature=str(data.get("signature", "")),
        version=int(data.get("version", 1)),
    )
