"""Optional hook system for the autobuilder daemon.

Hooks are executable scripts in ``.autobuilder/hooks/``.  They are entirely
optional — the daemon works identically when the hooks directory is absent.

Hook types
----------
Lifecycle (blocking):    on-start, pre-iteration
Lifecycle (non-block):   on-idle, on-error, on-stop
Prompt augmentation:     augment-review, augment-plan, augment-execute
Quality gates:           post-agent, pre-push
Deployment:              post-push
"""

from __future__ import annotations

import json
import os
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Optional

from common import AUTOBUILDER_DIR, log

HOOK_HELPERS_DIR = AUTOBUILDER_DIR / "hook-helpers"

# ---------------------------------------------------------------------------
# Default timeouts (seconds) per hook
# ---------------------------------------------------------------------------

DEFAULT_TIMEOUTS: Dict[str, int] = {
    "on-start": 120,
    "on-stop": 120,
    "pre-iteration": 120,
    "augment-review": 120,
    "augment-plan": 120,
    "augment-execute": 120,
    "post-agent": 600,
    "pre-push": 600,
    "post-push": 7200,
    "on-idle": 300,
    "on-error": 300,
}

HOOK_NAMES = sorted(DEFAULT_TIMEOUTS)


# ---------------------------------------------------------------------------
# Result type
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class HookResult:
    """Outcome of running a hook script."""

    name: str
    exit_code: int
    stdout: str = ""
    stderr: str = ""

    @property
    def ok(self) -> bool:
        return self.exit_code == 0


# ---------------------------------------------------------------------------
# Discovery
# ---------------------------------------------------------------------------

def find_hook(project_dir: Path, name: str) -> Optional[Path]:
    """Locate a hook script by *name* (bare, ``.py``, or ``.sh``)."""
    hooks_dir = project_dir / ".autobuilder" / "hooks"
    if not hooks_dir.is_dir():
        return None
    for suffix in ("", ".py", ".sh"):
        candidate = hooks_dir / (name + suffix)
        if candidate.is_file():
            return candidate
    return None


# ---------------------------------------------------------------------------
# Environment builder
# ---------------------------------------------------------------------------

def build_hook_env(
    *,
    project_dir: Path,
    project_id: str,
    port: Optional[int] = None,
    iteration: Optional[int] = None,
    phases: Optional[List[str]] = None,
    executor: Optional[str] = None,
    model: Optional[str] = None,
    agent_exit_code: Optional[int] = None,
    newly_executed: Optional[List[str]] = None,
    push_branch: Optional[str] = None,
    error_message: Optional[str] = None,
    consecutive_failures: Optional[int] = None,
) -> Dict[str, str]:
    """Build the environment-variables dict for a hook invocation."""
    env = dict(os.environ)
    env["AUTOBUILDER_PROJECT_DIR"] = str(project_dir)
    env["AUTOBUILDER_PROJECT_ID"] = project_id
    helpers_dir = str(HOOK_HELPERS_DIR)
    existing_pp = env.get("PYTHONPATH", "")
    env["PYTHONPATH"] = f"{helpers_dir}:{existing_pp}" if existing_pp else helpers_dir
    if port is not None:
        env["AUTOBUILDER_PORT"] = str(port)
    if iteration is not None:
        env["AUTOBUILDER_ITERATION"] = str(iteration)
    if phases is not None:
        env["AUTOBUILDER_PHASES"] = ",".join(phases)
    if executor is not None:
        env["AUTOBUILDER_EXECUTOR"] = executor
    if model is not None:
        env["AUTOBUILDER_MODEL"] = model
    if agent_exit_code is not None:
        env["AUTOBUILDER_AGENT_EXIT_CODE"] = str(agent_exit_code)
    if newly_executed is not None:
        env["AUTOBUILDER_NEWLY_EXECUTED"] = ",".join(newly_executed)
    if push_branch is not None:
        env["AUTOBUILDER_PUSH_BRANCH"] = push_branch
    if error_message is not None:
        env["AUTOBUILDER_ERROR"] = str(error_message)
    if consecutive_failures is not None:
        env["AUTOBUILDER_CONSECUTIVE_FAILURES"] = str(consecutive_failures)
    return env


# ---------------------------------------------------------------------------
# Execution
# ---------------------------------------------------------------------------

def run_hook(
    project_dir: Path,
    name: str,
    log_fh,
    *,
    env: Dict[str, str],
    stdin_data: Optional[str] = None,
    timeout: Optional[int] = None,
) -> Optional[HookResult]:
    """Run a hook if it exists.

    Returns ``None`` when the hook file is absent (silent skip).
    Returns a :class:`HookResult` for every execution attempt.
    """
    hook_path = find_hook(project_dir, name)
    if hook_path is None:
        return None

    if timeout is None:
        timeout = DEFAULT_TIMEOUTS.get(name, 300)

    suffix = hook_path.suffix
    if suffix == ".py":
        cmd = [sys.executable, str(hook_path)]
    elif suffix == ".sh":
        cmd = ["bash", str(hook_path)]
    else:
        cmd = [str(hook_path)]

    log(log_fh, f"Running hook: {name} ({hook_path.name})")

    try:
        result = subprocess.run(
            cmd,
            cwd=str(project_dir),
            env=env,
            input=stdin_data,
            capture_output=True,
            text=True,
            timeout=timeout,
        )
    except subprocess.TimeoutExpired:
        log(log_fh, f"Hook {name} timed out after {timeout}s")
        return HookResult(name=name, exit_code=124,
                          stderr=f"Timed out after {timeout}s")
    except FileNotFoundError:
        log(log_fh, f"Hook {name}: interpreter or file not found ({cmd[0]})")
        return HookResult(name=name, exit_code=126,
                          stderr=f"Not executable: {cmd[0]}")
    except Exception as exc:
        log(log_fh, f"Hook {name} execution error: {exc}")
        return HookResult(name=name, exit_code=1, stderr=str(exc))

    if result.stderr.strip():
        for line in result.stderr.strip().splitlines():
            log(log_fh, f"  [{name} stderr] {line}")

    if result.returncode == 0:
        log(log_fh, f"Hook {name} succeeded")
    else:
        log(log_fh, f"Hook {name} failed (exit code {result.returncode})")

    return HookResult(
        name=name,
        exit_code=result.returncode,
        stdout=result.stdout,
        stderr=result.stderr,
    )


# ---------------------------------------------------------------------------
# Prompt augmentation convenience
# ---------------------------------------------------------------------------

def run_augment_hook(
    project_dir: Path,
    name: str,
    log_fh,
    *,
    env: Dict[str, str],
    context: Dict[str, Any],
) -> str:
    """Run a prompt-augmentation hook and return its stdout text.

    The hook receives *context* as JSON on stdin and prints additional
    instructions on stdout.  Returns ``""`` when the hook is missing or
    fails (fail-safe — the prompt section stays unmodified).
    """
    result = run_hook(
        project_dir, name, log_fh,
        env=env,
        stdin_data=json.dumps(context),
    )
    if result is None:
        return ""
    if not result.ok:
        log(log_fh, f"Augment hook {name} failed — prompt section unchanged")
        return ""
    text = result.stdout.strip()
    if text:
        log(log_fh, f"Augment hook {name} injected {len(text)} chars")
    return text
