"""Orchestrator configuration."""

from __future__ import annotations

import shutil
from dataclasses import dataclass, field
from pathlib import Path

from cleave.orchestrator.errors import ClaudeCliNotFoundError, OrchestratorError


def _parse_timeout(value: str | int) -> int:
    """Parse a timeout string like '8h', '30m', '3600' into seconds."""
    if isinstance(value, int):
        return value
    value = value.strip().lower()
    if value.endswith("h"):
        return int(float(value[:-1]) * 3600)
    if value.endswith("m"):
        return int(float(value[:-1]) * 60)
    if value.endswith("s"):
        return int(float(value[:-1]))
    return int(value)


@dataclass
class OrchestratorConfig:
    """Configuration for an orchestrator run."""

    directive: str
    repo_path: Path
    root_directive: str = ""
    success_criteria: list[str] = field(default_factory=list)
    model: str = "opus"
    planner_model: str = "sonnet"
    max_budget_usd: float = 50.0
    child_budget_usd: float = 15.0
    timeout_seconds: int = 28800  # 8h
    child_timeout_seconds: int = 7200  # 2h
    max_depth: int = 3
    circuit_breaker_threshold: int = 3
    max_parallel_children: int = 4
    permission_mode: str = "bypassPermissions"
    allowed_tools: str = "Bash Edit Read Write Glob Grep"
    mcp_config: str = ""  # JSON string or path; empty = no MCP for children
    dirty: bool = False
    dry_run: bool = False
    confirm: bool = False
    verbose: bool = False

    def __post_init__(self) -> None:
        if isinstance(self.repo_path, str):
            self.repo_path = Path(self.repo_path)
        if not self.root_directive:
            self.root_directive = self.directive

    def validate(self) -> None:
        """Validate configuration, raising OrchestratorError on problems."""
        if not self.directive.strip():
            raise OrchestratorError("Directive must not be empty")
        if not self.repo_path.is_dir():
            raise OrchestratorError(f"Repo path does not exist: {self.repo_path}")
        if not (self.repo_path / ".git").exists():
            raise OrchestratorError(f"Repo path is not a git repository: {self.repo_path}")
        if self.max_budget_usd <= 0:
            raise OrchestratorError("max_budget_usd must be positive")
        if self.child_budget_usd <= 0:
            raise OrchestratorError("child_budget_usd must be positive")
        if self.child_budget_usd > self.max_budget_usd:
            raise OrchestratorError("child_budget_usd cannot exceed max_budget_usd")
        if self.timeout_seconds <= 0:
            raise OrchestratorError("timeout_seconds must be positive")
        if self.child_timeout_seconds <= 0:
            raise OrchestratorError("child_timeout_seconds must be positive")
        if self.max_depth < 1 or self.max_depth > 10:
            raise OrchestratorError("max_depth must be between 1 and 10")
        if self.max_parallel_children < 1:
            raise OrchestratorError("max_parallel_children must be at least 1")

    def find_claude_cli(self) -> str:
        """Locate the claude CLI binary, raising if not found."""
        path = shutil.which("claude")
        if path is None:
            raise ClaudeCliNotFoundError(
                "Could not find 'claude' on PATH. Install Claude Code first."
            )
        return path

    def to_dict(self) -> dict:
        """Serialize config to a plain dict for state persistence."""
        return {
            "directive": self.directive,
            "repo_path": str(self.repo_path),
            "root_directive": self.root_directive,
            "success_criteria": self.success_criteria,
            "model": self.model,
            "planner_model": self.planner_model,
            "max_budget_usd": self.max_budget_usd,
            "child_budget_usd": self.child_budget_usd,
            "timeout_seconds": self.timeout_seconds,
            "child_timeout_seconds": self.child_timeout_seconds,
            "max_depth": self.max_depth,
            "circuit_breaker_threshold": self.circuit_breaker_threshold,
            "max_parallel_children": self.max_parallel_children,
            "permission_mode": self.permission_mode,
            "allowed_tools": self.allowed_tools,
            "mcp_config": self.mcp_config,
            "dirty": self.dirty,
            "dry_run": self.dry_run,
            "confirm": self.confirm,
            "verbose": self.verbose,
        }

    @classmethod
    def from_dict(cls, data: dict) -> OrchestratorConfig:
        """Reconstruct config from a serialized dict.

        Filters to known fields so that schema evolution (adding/removing
        fields between versions) doesn't break resume from saved state.
        """
        data = dict(data)  # shallow copy
        data["repo_path"] = Path(data["repo_path"])
        known = cls.__dataclass_fields__
        filtered = {k: v for k, v in data.items() if k in known}
        return cls(**filtered)
