"""
Runtime-side plan validation — the trust gate.

Before the runtime executes a single step, every plan must pass through
validate_plan(). This is the ONLY entry point for plans on the user's machine.

Validation checks (in order):
    1. Deserialization — malformed JSON / missing fields → PlanValidationError
    2. Protocol version — unknown version → PlanValidationError
    3. TTL — expired plan → PlanExpiredError
    4. Signature — HMAC mismatch → PlanTamperedError

If any check fails, the plan is rejected entirely. No partial execution.

Error hierarchy:
    PlanValidationError (base)
    ├── PlanExpiredError
    └── PlanTamperedError
"""

from __future__ import annotations

from runtime.protocol.schema import PROTOCOL_VERSION, ExecutionPlan
from runtime.protocol.serializer import deserialize_plan
from runtime.protocol.signer import is_expired, verify_plan


# ── Exceptions ──────────────────────────────────────────────────────────

class PlanValidationError(Exception):
    """Base exception for plan validation failures."""

    def __init__(self, reason: str, plan_id: str | None = None) -> None:
        self.reason = reason
        self.plan_id = plan_id
        detail = f" (plan_id={plan_id})" if plan_id else ""
        super().__init__(f"Plan validation failed{detail}: {reason}")


class PlanExpiredError(PlanValidationError):
    """The plan's TTL has elapsed."""

    def __init__(self, plan_id: str) -> None:
        super().__init__("plan has expired", plan_id=plan_id)


class PlanTamperedError(PlanValidationError):
    """The plan's HMAC signature does not match."""

    def __init__(self, plan_id: str) -> None:
        super().__init__("signature verification failed", plan_id=plan_id)


# ── Validator ───────────────────────────────────────────────────────────

def validate_plan(plan_data: dict, secret: str) -> ExecutionPlan:
    """Deserialize, verify, and return a validated ExecutionPlan.

    This is the runtime's single entry point for processing plans received
    from the cloud. It enforces every security invariant before allowing
    execution to proceed.

    Args:
        plan_data: Raw dict received over the wire (parsed from JSON).
        secret:    Shared HMAC secret for signature verification.

    Returns:
        A fully validated ExecutionPlan ready for execution.

    Raises:
        PlanValidationError: Deserialization or version mismatch.
        PlanExpiredError:    Plan TTL has elapsed.
        PlanTamperedError:   HMAC signature does not match.
    """
    # 1. Deserialize — catches malformed payloads
    try:
        plan = deserialize_plan(plan_data)
    except (ValueError, KeyError, TypeError) as exc:
        raise PlanValidationError(f"deserialization failed: {exc}") from exc

    # 2. Protocol version — hard reject unknown versions
    if plan.version != PROTOCOL_VERSION:
        raise PlanValidationError(
            f"unsupported protocol version {plan.version} "
            f"(expected {PROTOCOL_VERSION})",
            plan_id=plan.plan_id,
        )

    # 3. TTL — reject expired plans (no replay attacks)
    if is_expired(plan):
        raise PlanExpiredError(plan.plan_id)

    # 4. Signature — reject tampered plans
    if not plan.signature:
        raise PlanTamperedError(plan.plan_id)
    if not verify_plan(plan, plan.signature, secret):
        raise PlanTamperedError(plan.plan_id)

    return plan
