"""Spec Manager — 执行计划规格文档管理器

将 PlanExecEngine 生成的执行计划持久化为 Spec 文档，
用户可在执行前查看、编辑和确认 Spec，作为人与 AI 之间的契约。
"""

from __future__ import annotations

import logging
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

import yaml

logger = logging.getLogger(__name__)


@dataclass
class SpecStep:
    """A single step in a spec."""

    step_id: str
    name: str
    description: str
    dependencies: list[str] = field(default_factory=list)
    status: str = "pending"  # pending | confirmed | executing | completed | failed


@dataclass
class Spec:
    """A specification document for a planned task."""

    spec_id: str
    goal: str
    steps: list[SpecStep] = field(default_factory=list)
    status: str = "draft"  # draft | confirmed | executing | completed | failed
    created_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
    confirmed_at: str | None = None
    metadata: dict[str, Any] = field(default_factory=dict)


class SpecManager:
    """Manages Spec documents as first-class citizens.

    Specs are persisted to .agentkit/specs/ directory as YAML files.
    Users can view, edit, and confirm specs before execution.
    """

    def __init__(self, specs_dir: str | None = None):
        """
        Args:
            specs_dir: Directory for spec files. Default: .agentkit/specs/
        """
        self._specs_dir = Path(specs_dir or ".agentkit/specs")
        self._specs_dir.mkdir(parents=True, exist_ok=True)
        self._cache: dict[str, Spec] = {}

    def create(self, spec: Spec) -> Path:
        """Persist a Spec to disk. Returns the file path."""
        path = self._specs_dir / f"{spec.spec_id}.yaml"
        data = asdict(spec)
        path.write_text(yaml.dump(data, allow_unicode=True, default_flow_style=False), encoding="utf-8")
        self._cache[spec.spec_id] = spec
        logger.info(f"Spec created: {spec.spec_id} -> {path}")
        return path

    def get(self, spec_id: str) -> Spec | None:
        """Load a Spec by ID."""
        if spec_id in self._cache:
            return self._cache[spec_id]

        path = self._specs_dir / f"{spec_id}.yaml"
        if not path.exists():
            return None

        try:
            data = yaml.safe_load(path.read_text(encoding="utf-8"))
            spec = self._dict_to_spec(data)
            self._cache[spec_id] = spec
            return spec
        except Exception as e:
            logger.error(f"Failed to load spec {spec_id}: {e}")
            return None

    def update(self, spec_id: str, **kwargs: Any) -> Spec | None:
        """Update spec fields and persist."""
        spec = self.get(spec_id)
        if spec is None:
            return None

        for key, value in kwargs.items():
            if key == "steps" and isinstance(value, list):
                spec.steps = [self._dict_to_step(s) if isinstance(s, dict) else s for s in value]
            elif hasattr(spec, key):
                setattr(spec, key, value)

        self.create(spec)  # re-persist
        return spec

    def confirm(self, spec_id: str) -> Spec | None:
        """Mark a spec as confirmed (user approved execution)."""
        spec = self.get(spec_id)
        if spec is None:
            return None

        spec.status = "confirmed"
        spec.confirmed_at = datetime.now(timezone.utc).isoformat()

        # Mark all steps as confirmed
        for step in spec.steps:
            if step.status == "pending":
                step.status = "confirmed"

        self.create(spec)  # re-persist
        logger.info(f"Spec confirmed: {spec_id}")
        return spec

    def list_specs(self, status: str | None = None) -> list[Spec]:
        """List all specs, optionally filtered by status. Sorted by created_at desc."""
        specs: list[Spec] = []
        for path in self._specs_dir.glob("*.yaml"):
            try:
                data = yaml.safe_load(path.read_text(encoding="utf-8"))
                spec = self._dict_to_spec(data)
                if status is None or spec.status == status:
                    specs.append(spec)
            except Exception as e:
                logger.warning(f"Failed to load spec from {path}: {e}")

        specs.sort(key=lambda s: s.created_at, reverse=True)
        return specs

    def delete(self, spec_id: str) -> bool:
        """Delete a spec file."""
        path = self._specs_dir / f"{spec_id}.yaml"
        if not path.exists():
            return False

        path.unlink()
        self._cache.pop(spec_id, None)
        logger.info(f"Spec deleted: {spec_id}")
        return True

    @staticmethod
    def _dict_to_spec(data: dict[str, Any]) -> Spec:
        """Convert a dict to a Spec instance."""
        steps = [SpecManager._dict_to_step(s) for s in data.get("steps", [])]
        return Spec(
            spec_id=data["spec_id"],
            goal=data["goal"],
            steps=steps,
            status=data.get("status", "draft"),
            created_at=data.get("created_at", ""),
            confirmed_at=data.get("confirmed_at"),
            metadata=data.get("metadata", {}),
        )

    @staticmethod
    def _dict_to_step(data: dict[str, Any] | SpecStep) -> SpecStep:
        """Convert a dict to a SpecStep instance."""
        if isinstance(data, SpecStep):
            return data
        return SpecStep(
            step_id=data["step_id"],
            name=data["name"],
            description=data["description"],
            dependencies=data.get("dependencies", []),
            status=data.get("status", "pending"),
        )
