"""
Plan signing and verification — HMAC-SHA256 over the security-critical fields.

The cloud signs every ExecutionPlan before sending it to the runtime.
The runtime verifies the signature before executing anything.

What is signed (the "canonical payload"):
    plan_id  + user_id + steps (JSON-serialized) + expires_at + version

What is NOT signed:
    original_input — large, variable-length user content.
    The runtime doesn't need to trust original_input integrity because
    it only uses it for context resolution (no security decisions).

Signing with HMAC-SHA256 (symmetric):
    For the v1 protocol, the cloud and runtime share a secret.
    In production this is a per-user or per-session secret derived
    from the user's JWT, never hardcoded. The signer module is
    agnostic to key management — it takes a secret string.
"""

from __future__ import annotations

import hashlib
import hmac
import json
import time
from typing import Any

from runtime.protocol.schema import ExecutionPlan, PlanStep


def _step_to_canonical(step: PlanStep) -> dict[str, Any]:
    """Convert a PlanStep to a deterministic dict for signing.

    Keys are sorted, None values are omitted, and config is sorted
    recursively. This ensures the same step always produces the same
    bytes regardless of Python dict ordering or serialization quirks.
    """
    d: dict[str, Any] = {
        "step_id": step.step_id,
        "type": step.type,
        "backend": step.backend,
        "model": step.model,
        "prompt": step.prompt,
        "context_refs": step.context_refs,
        "config": step.config,
    }
    # Only include agent fields when set — keeps canonical form stable
    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 _canonical_payload(plan: ExecutionPlan) -> bytes:
    """Build the canonical byte string that gets signed.

    Uses JSON with sorted keys and no whitespace for determinism.
    """
    obj = {
        "plan_id": plan.plan_id,
        "user_id": plan.user_id,
        "steps": [_step_to_canonical(s) for s in plan.steps],
        "expires_at": plan.expires_at,
        "signed_at": plan.signed_at,
        "version": plan.version,
    }
    return json.dumps(obj, sort_keys=True, separators=(",", ":")).encode("utf-8")


def _compute_hmac(plan: ExecutionPlan, secret: str) -> str:
    """Compute HMAC-SHA256 over the plan's current canonical payload."""
    payload = _canonical_payload(plan)
    return hmac.new(
        secret.encode("utf-8"),
        payload,
        hashlib.sha256,
    ).hexdigest()


def sign_plan(plan: ExecutionPlan, secret: str) -> str:
    """Compute HMAC-SHA256 over the plan's security-critical fields.

    Args:
        plan:   The execution plan to sign.
        secret: Shared secret between cloud and runtime.

    Returns:
        Hex-encoded HMAC-SHA256 digest.
    """
    return _compute_hmac(plan, secret)


def verify_plan(plan: ExecutionPlan, signature: str, secret: str) -> bool:
    """Verify a plan's HMAC signature.

    Uses constant-time comparison to prevent timing attacks.
    Recomputes the HMAC from the plan's existing signed_at (does not
    overwrite it).

    Args:
        plan:      The execution plan to verify.
        signature: The hex-encoded HMAC to check.
        secret:    Shared secret between cloud and runtime.

    Returns:
        True if the signature is valid.
    """
    expected = _compute_hmac(plan, secret)
    return hmac.compare_digest(expected, signature)


def is_expired(plan: ExecutionPlan) -> bool:
    """Check whether a plan has exceeded its TTL.

    Args:
        plan: The execution plan to check.

    Returns:
        True if the current time is past plan.expires_at.
    """
    return time.time() > plan.expires_at
