"""
Base class for benchmark suites and shared benchmark helpers.
"""

from __future__ import annotations

import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
import time
from abc import ABC, abstractmethod
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any


_SAFE_COMPONENT_RE = re.compile(r"[^A-Za-z0-9_.-]+")


def _safe_component(value: str) -> str:
    cleaned = _SAFE_COMPONENT_RE.sub("_", value).strip("._")
    return cleaned or "item"


def resolve_temp_root() -> Path:
    root = Path(tempfile.gettempdir()).expanduser().resolve()
    root.mkdir(parents=True, exist_ok=True)
    return root


def make_temp_workspace(prefix: str, suite_name: str, problem_id: str) -> str:
    temp_root = resolve_temp_root()
    safe_prefix = _safe_component(prefix)
    safe_suite = _safe_component(suite_name)
    safe_problem = _safe_component(problem_id)
    return tempfile.mkdtemp(
        prefix=f"{safe_prefix}_{safe_suite}_{safe_problem}_",
        dir=str(temp_root),
    )


@dataclass
class BenchmarkResult:
    problem_id: str
    passed: bool
    expected: str = ""
    actual: str = ""
    duration_sec: float = 0.0
    error: str = ""
    metadata: dict[str, Any] = field(default_factory=dict)


@dataclass
class SuiteReport:
    suite_name: str
    total: int
    passed: int
    failed: int
    score_pct: float
    duration_sec: float
    model: str
    results: list[BenchmarkResult]
    timestamp: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "suite_name": self.suite_name,
            "total": self.total,
            "passed": self.passed,
            "failed": self.failed,
            "score_pct": self.score_pct,
            "duration_sec": round(self.duration_sec, 2),
            "model": self.model,
            "timestamp": self.timestamp,
            "results": [asdict(r) for r in self.results],
        }


class BenchmarkSuite(ABC):
    name: str = "base"
    description: str = ""
    category: str = "general"

    def __init__(
        self,
        *,
        data_dir: str | None = None,
        limit: int | None = None,
        agent_timeout: float = 300.0,
        verbose: bool = False,
        artifacts_dir: str | None = None,
        save_passing_artifacts: bool = False,
    ) -> None:
        self.data_dir = data_dir or str(
            Path(__file__).resolve().parent.parent / "data"
        )
        self.limit = limit
        self.agent_timeout = agent_timeout
        self.verbose = verbose
        self.artifacts_dir = artifacts_dir
        self.save_passing_artifacts = save_passing_artifacts
        self.project_root = str(Path(__file__).resolve().parent.parent.parent)

    @abstractmethod
    def load_dataset(self) -> list[dict[str, Any]]:
        ...

    @abstractmethod
    def build_prompt(self, problem: dict[str, Any]) -> str:
        ...

    @abstractmethod
    def evaluate(self, problem: dict[str, Any], workspace: str) -> BenchmarkResult:
        ...

    def _run_shell(
        self,
        cmd: str,
        cwd: str,
        timeout: float = 30.0,
    ) -> tuple[int, str]:
        try:
            proc = subprocess.run(
                cmd,
                shell=True,
                cwd=cwd,
                capture_output=True,
                text=True,
                timeout=timeout,
            )
            return proc.returncode, (proc.stdout + proc.stderr).strip()
        except subprocess.TimeoutExpired:
            return 1, f"[TIMEOUT after {timeout}s]"
        except Exception as exc:
            return 1, str(exc)

    def run_agent(self, instruction: str, workspace: str) -> tuple[int, str, float]:
        import shlex

        agent_cmd = (
            f"{sys.executable} -m src.main agent "
            f"{shlex.quote(instruction)} "
            f"--cwd {shlex.quote(workspace)} "
            f"--allow-write "
            f"--allow-shell"
        )
        if self.verbose:
            print(f"  agent cmd: {agent_cmd[:160]}...")

        start = time.time()
        code, output = self._run_shell(
            agent_cmd,
            cwd=self.project_root,
            timeout=self.agent_timeout,
        )
        duration = time.time() - start

        if self.verbose:
            print(f"  agent exit={code}  duration={duration:.1f}s")

        return code, output, duration

    def setup_workspace(self, problem: dict[str, Any], workspace: str) -> None:
        del problem, workspace

    def recover_output_files(
        self,
        problem: dict[str, Any],
        workspace: str,
        agent_output: str,
        metadata: dict[str, Any],
    ) -> None:
        del problem, workspace, agent_output, metadata

    def _artifact_root(self, index: int, problem_id: str) -> Path | None:
        if not self.artifacts_dir:
            return None
        root = Path(self.artifacts_dir)
        root.mkdir(parents=True, exist_ok=True)
        return root / f"{index:03d}_{_safe_component(problem_id)}"

    def _save_artifacts(
        self,
        *,
        index: int,
        problem: dict[str, Any],
        prompt: str,
        agent_output: str,
        workspace: str,
        result: BenchmarkResult,
        agent_exit_code: int,
    ) -> None:
        artifact_root = self._artifact_root(index, result.problem_id)
        if artifact_root is None:
            return
        if result.passed and not self.save_passing_artifacts:
            return

        artifact_root.mkdir(parents=True, exist_ok=True)
        (artifact_root / "problem.json").write_text(
            json.dumps(problem, indent=2) + "\n",
            encoding="utf-8",
        )
        (artifact_root / "prompt.txt").write_text(prompt, encoding="utf-8")
        (artifact_root / "agent_output.txt").write_text(agent_output, encoding="utf-8")

        workspace_dst = artifact_root / "workspace"
        if workspace_dst.exists():
            shutil.rmtree(workspace_dst, ignore_errors=True)
        shutil.copytree(workspace, workspace_dst)

        result_payload = asdict(result)
        result_payload["agent_exit_code"] = agent_exit_code
        result_payload["workspace"] = workspace
        (artifact_root / "result.json").write_text(
            json.dumps(result_payload, indent=2) + "\n",
            encoding="utf-8",
        )
        result.metadata["artifact_path"] = str(artifact_root)

    def run_all(self) -> SuiteReport:
        problems = self.load_dataset()
        if self.limit is not None:
            problems = problems[: self.limit]

        print()
        print("=" * 72)
        print(f"  {self.name} BENCHMARK")
        print(f"  {self.description}")
        print("=" * 72)
        model = os.environ.get("OPENAI_MODEL", "unknown")
        print(f"  Model:    {model}")
        print(f"  Problems: {len(problems)}")
        print(f"  Timeout:  {self.agent_timeout}s per problem")
        print("=" * 72)
        print()

        suite_start = time.time()
        all_results: list[BenchmarkResult] = []

        for index, problem in enumerate(problems, 1):
            pid = str(problem.get("id", problem.get("task_id", f"problem-{index}")))
            print(f"[{index}/{len(problems)}] {pid}")

            workspace = make_temp_workspace("claw", self.name, pid)
            prompt = ""
            agent_output = ""
            agent_exit_code = -1
            try:
                self.setup_workspace(problem, workspace)
                prompt = self.build_prompt(problem)
                agent_exit_code, agent_output, duration = self.run_agent(prompt, workspace)

                result_metadata: dict[str, Any] = {"agent_exit_code": agent_exit_code}
                self.recover_output_files(problem, workspace, agent_output, result_metadata)
                result = self.evaluate(problem, workspace)
                result.duration_sec = duration
                result.metadata.update(result_metadata)

                self._save_artifacts(
                    index=index,
                    problem=problem,
                    prompt=prompt,
                    agent_output=agent_output,
                    workspace=workspace,
                    result=result,
                    agent_exit_code=agent_exit_code,
                )

                status = "PASS ✅" if result.passed else "FAIL ❌"
                print(f"  -> {status}  ({duration:.1f}s)")
            except Exception as exc:
                result = BenchmarkResult(
                    problem_id=pid,
                    passed=False,
                    error=str(exc),
                    metadata={"agent_exit_code": agent_exit_code},
                )
                self._save_artifacts(
                    index=index,
                    problem=problem,
                    prompt=prompt,
                    agent_output=agent_output,
                    workspace=workspace,
                    result=result,
                    agent_exit_code=agent_exit_code,
                )
                print(f"  -> ERROR ❌  {exc}")
            finally:
                shutil.rmtree(workspace, ignore_errors=True)

            all_results.append(result)
            print()

        suite_duration = time.time() - suite_start
        passed = sum(1 for item in all_results if item.passed)
        total = len(all_results)
        report = SuiteReport(
            suite_name=self.name,
            total=total,
            passed=passed,
            failed=total - passed,
            score_pct=round(100.0 * passed / total, 1) if total else 0.0,
            duration_sec=suite_duration,
            model=model,
            results=all_results,
            timestamp=time.strftime("%Y-%m-%dT%H:%M:%S"),
        )
        self._print_report(report)
        return report

    @staticmethod
    def _print_report(report: SuiteReport) -> None:
        print()
        print("=" * 72)
        print(f"  {report.suite_name} — RESULTS")
        print("=" * 72)
        print()
        for result in report.results:
            icon = "✅" if result.passed else "❌"
            print(f"  {icon} {result.problem_id:<40} {result.duration_sec:.1f}s")
        print()
        print("─" * 72)
        print(
            f"  Total: {report.total}  |  Passed: {report.passed}  "
            f"|  Failed: {report.failed}  |  Score: {report.score_pct:.1f}%"
        )
        print(f"  Total time: {report.duration_sec:.1f}s")
        print("─" * 72)
        print()

    @staticmethod
    def save_report(report: SuiteReport, path: str) -> None:
        with open(path, "w", encoding="utf-8") as fh:
            json.dump(report.to_dict(), fh, indent=2)
        print(f"  Report saved to {path}")
