"""Autopilot plan and receipt contracts (v1)."""

from __future__ import annotations

import hashlib
import json
from typing import Any

from pydantic import BaseModel, ConfigDict, Field, model_validator, computed_field


def _canonical_json(payload: Any) -> str:
    if isinstance(payload, dict):
        payload = {k: v for k, v in payload.items() if k != "$schema"}
    return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True)


class PlanSource(BaseModel):
    model_config = ConfigDict(extra="ignore")

    type: str
    scan_id: str | None = None

    @computed_field(alias="$schema")
    @property
    def schema(self) -> str:
        return "PlanSource/v1"


class PlanRisk(BaseModel):
    model_config = ConfigDict(extra="ignore")

    max_risk_level: int = 0
    irreversible: bool = False
    blast_radius: str | None = None
    estimated_count: int | None = None
    target_object: str | None = None

    @computed_field(alias="$schema")
    @property
    def schema(self) -> str:
        return "PlanRisk/v1"


class PlanStep(BaseModel):
    model_config = ConfigDict(extra="ignore")

    step_id: str
    tool: str
    diff_preview: dict[str, Any] | None = None
    args: dict[str, Any] = Field(default_factory=dict)
    risk_level: int = 0
    idempotency_key: str | None = None
    prechecks: list[str] = Field(default_factory=list)
    postchecks: list[str] = Field(default_factory=list)
    evidence_ids: list[str] = Field(default_factory=list)

    @computed_field(alias="$schema")
    @property
    def schema(self) -> str:
        return "PlanStep/v1"

    def to_hash_payload(self) -> dict[str, Any]:
        return self.model_dump(exclude_none=False)


class PlanV1(BaseModel):
    model_config = ConfigDict(extra="ignore")

    version: int = 1
    run_id: str
    source: PlanSource
    summary: str = ""
    risk: PlanRisk = Field(default_factory=PlanRisk)
    plan_hash: str | None = None
    policy_hash: str | None = None
    steps: list[PlanStep] = Field(default_factory=list)
    verification: list[str] = Field(default_factory=list)
    approvals_required: bool = False

    @computed_field(alias="$schema")
    @property
    def schema(self) -> str:
        return "Plan/v1"

    @staticmethod
    def compute_plan_hash(steps: list[PlanStep]) -> str:
        payload = []
        for step in steps:
            data = step.to_hash_payload()
            if isinstance(data, dict) and "$schema" in data:
                data = {k: v for k, v in data.items() if k != "$schema"}
            payload.append(data)
        digest = hashlib.sha256(_canonical_json(payload).encode("utf-8")).hexdigest()
        return f"sha256:{digest}"

    @model_validator(mode="after")
    def _ensure_plan_hash(self) -> "PlanV1":
        computed = self.compute_plan_hash(self.steps)
        if not self.plan_hash:
            self.plan_hash = computed
        elif self.plan_hash != computed:
            raise ValueError("plan_hash does not match computed hash")
        return self

    def to_dict(self) -> dict[str, Any]:
        return self.model_dump(exclude_none=False, by_alias=True)

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "PlanV1":
        return cls.model_validate(data)


class ReceiptApprovedBy(BaseModel):
    model_config = ConfigDict(extra="ignore")

    channel: str
    actor: str
    mapped_email: str | None = None

    @computed_field(alias="$schema")
    @property
    def schema(self) -> str:
        return "ReceiptApprovedBy/v1"


class ReceiptChange(BaseModel):
    model_config = ConfigDict(extra="ignore")

    tool: str
    targets: list[str] = Field(default_factory=list)
    before: dict[str, Any] | None = None
    after: dict[str, Any] | None = None

    @computed_field(alias="$schema")
    @property
    def schema(self) -> str:
        return "ReceiptChange/v1"


class ReceiptArtifact(BaseModel):
    model_config = ConfigDict(extra="ignore")

    type: str
    path: str

    @computed_field(alias="$schema")
    @property
    def schema(self) -> str:
        return "ReceiptArtifact/v1"


class ReceiptV1(BaseModel):
    model_config = ConfigDict(extra="ignore")

    version: int = 1
    run_id: str
    approved_by: ReceiptApprovedBy | None = None
    started_at: str | None = None
    completed_at: str | None = None
    outcome: str = "unknown"
    policy_hash: str | None = None
    plan_hash: str | None = None
    executor_version: str | None = None
    tool_versions: dict[str, str] = Field(default_factory=dict)
    changes: list[ReceiptChange] = Field(default_factory=list)
    verification: dict[str, Any] | None = None
    artifacts: list[ReceiptArtifact] = Field(default_factory=list)
    decision_path: list[str] = Field(default_factory=list)

    @computed_field(alias="$schema")
    @property
    def schema(self) -> str:
        return "Receipt/v1"

    def to_dict(self) -> dict[str, Any]:
        return self.model_dump(exclude_none=False, by_alias=True)

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "ReceiptV1":
        return cls.model_validate(data)


__all__ = [
    "PlanSource",
    "PlanRisk",
    "PlanStep",
    "PlanV1",
    "ReceiptApprovedBy",
    "ReceiptChange",
    "ReceiptArtifact",
    "ReceiptV1",
]
