"""Sandbox runner — execute skills in isolated subprocess with timeout."""

from __future__ import annotations

import json
import os
import shutil
import subprocess
import tempfile
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any

from alive.sandbox.dependency import DependencyManager, get_dependency_manager

DEFAULT_TIMEOUT = 60


@dataclass
class SkillResult:
    """Result of a sandboxed skill execution."""

    success: bool
    output: dict[str, Any] | None
    error: str | None
    duration_ms: int


# ── Runner script template ──────────────────────────────────────────────────

_RUNNER_SCRIPT = """\
import sys
import json
import traceback

input_data = json.loads(sys.argv[1])

try:
    from skill import run
    result = run(**input_data)
    if not isinstance(result, dict):
        print(json.dumps({{"success": False, "error": "run() must return a dict"}}))
        sys.exit(1)
    print(json.dumps(result, default=str))
except Exception:
    tb = traceback.format_exc()
    print(json.dumps({{"success": False, "error": tb}}))
    sys.exit(1)
"""


def run_skill(
    name: str,
    input_data: dict[str, Any] | None = None,
    *,
    timeout: int = DEFAULT_TIMEOUT,
    skills_dir: Path | None = None,
    dep_manager: DependencyManager | None = None,
) -> SkillResult:
    """Execute a named skill in a sandboxed subprocess.

    Args:
        name: Skill name (must exist in skills_dir).
        input_data: Dict of kwargs to pass to the skill's run().
        timeout: Max execution time in seconds.
        skills_dir: Override for skill directory lookup.
        dep_manager: Override for dependency manager.

    Returns:
        SkillResult with success, output, error, and duration_ms.
    """
    from alive.config import SKILLS_DIR

    skills_dir = skills_dir or SKILLS_DIR
    skill_dir = skills_dir / name
    skill_file = skill_dir / "skill.py"

    if not skill_file.exists():
        return SkillResult(
            success=False,
            output=None,
            error=f"Skill '{name}' not found at {skill_dir}",
            duration_ms=0,
        )

    # Ensure dependencies are installed
    meta_file = skill_dir / "metadata.json"
    if meta_file.exists():
        try:
            meta = json.loads(meta_file.read_text())
            deps = meta.get("dependencies", [])
            if deps:
                dm = dep_manager or get_dependency_manager()
                dm.install(deps)
        except (json.JSONDecodeError, RuntimeError) as exc:
            import logging
            logging.getLogger("alive").debug("Dependency install failed for %s: %s", name, exc)

    dm = dep_manager or get_dependency_manager()
    dm.ensure_venv()
    venv_python = dm.python

    input_data = input_data or {}
    tmpdir = tempfile.mkdtemp(prefix="alive_run_")

    try:
        # Copy skill.py into sandbox
        shutil.copy2(str(skill_file), str(Path(tmpdir) / "skill.py"))

        # Write runner script
        (Path(tmpdir) / "run_skill.py").write_text(_RUNNER_SCRIPT)

        # Serialize input as JSON argument
        input_json = json.dumps(input_data, default=str)

        start = time.monotonic()
        proc = subprocess.run(
            [str(venv_python), "run_skill.py", input_json],
            capture_output=True,
            text=True,
            timeout=timeout,
            cwd=tmpdir,
            env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"},
        )
        duration_ms = int((time.monotonic() - start) * 1000)

        if proc.returncode != 0:
            # Try to parse structured error from stdout
            error = _parse_error(proc.stdout, proc.stderr)
            return SkillResult(
                success=False,
                output=None,
                error=error,
                duration_ms=duration_ms,
            )

        # Parse JSON output
        try:
            output = json.loads(proc.stdout)
        except json.JSONDecodeError:
            return SkillResult(
                success=False,
                output=None,
                error=f"Skill returned invalid JSON: {proc.stdout[:500]}",
                duration_ms=duration_ms,
            )

        return SkillResult(
            success=True,
            output=output,
            error=None,
            duration_ms=duration_ms,
        )

    except subprocess.TimeoutExpired:
        duration_ms = int((time.monotonic() - start) * 1000)
        return SkillResult(
            success=False,
            output=None,
            error=f"Skill '{name}' timed out after {timeout}s",
            duration_ms=duration_ms,
        )
    finally:
        shutil.rmtree(tmpdir, ignore_errors=True)


def run_skill_code(
    code: str,
    input_data: dict[str, Any] | None = None,
    *,
    timeout: int = DEFAULT_TIMEOUT,
    dep_manager: DependencyManager | None = None,
) -> SkillResult:
    """Execute raw skill code in a sandboxed subprocess.

    Useful for testing generated code before saving it as a skill.

    Args:
        code: Python source code containing a run() function.
        input_data: Dict of kwargs to pass to run().
        timeout: Max execution time in seconds.
        dep_manager: Override for dependency manager.

    Returns:
        SkillResult with success, output, error, and duration_ms.
    """
    dm = dep_manager or get_dependency_manager()
    dm.ensure_venv()
    venv_python = dm.python

    input_data = input_data or {}
    tmpdir = tempfile.mkdtemp(prefix="alive_run_")

    try:
        (Path(tmpdir) / "skill.py").write_text(code)
        (Path(tmpdir) / "run_skill.py").write_text(_RUNNER_SCRIPT)

        input_json = json.dumps(input_data, default=str)

        start = time.monotonic()
        proc = subprocess.run(
            [str(venv_python), "run_skill.py", input_json],
            capture_output=True,
            text=True,
            timeout=timeout,
            cwd=tmpdir,
            env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"},
        )
        duration_ms = int((time.monotonic() - start) * 1000)

        if proc.returncode != 0:
            error = _parse_error(proc.stdout, proc.stderr)
            return SkillResult(
                success=False,
                output=None,
                error=error,
                duration_ms=duration_ms,
            )

        try:
            output = json.loads(proc.stdout)
        except json.JSONDecodeError:
            return SkillResult(
                success=False,
                output=None,
                error=f"Skill returned invalid JSON: {proc.stdout[:500]}",
                duration_ms=duration_ms,
            )

        return SkillResult(success=True, output=output, error=None, duration_ms=duration_ms)

    except subprocess.TimeoutExpired:
        duration_ms = int((time.monotonic() - start) * 1000)
        return SkillResult(
            success=False,
            output=None,
            error=f"Skill timed out after {timeout}s",
            duration_ms=duration_ms,
        )
    finally:
        shutil.rmtree(tmpdir, ignore_errors=True)


def _parse_error(stdout: str, stderr: str) -> str:
    """Extract the best error message from subprocess output."""
    # Try to parse structured error from stdout (our runner writes JSON errors)
    if stdout.strip():
        try:
            data = json.loads(stdout)
            if isinstance(data, dict) and "error" in data:
                return data["error"]
        except json.JSONDecodeError:
            pass
    # Fall back to stderr
    return stderr.strip() or stdout.strip() or "Unknown error"
