#!/usr/bin/env python3
"""Run one command, retain its complete log, and print only a bounded result."""

from __future__ import annotations

import argparse
import contextlib
import io
import json
import os
import re
import shlex
import subprocess
import sys
import tempfile
import time
import tomllib
from collections import deque
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Iterable, Sequence


WORKFLOW_PATH = Path(".codex/workflow.toml")
ANSI_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
ACTIONABLE_PATTERNS = (
    re.compile(r"^\s*error(?:\[[A-Z0-9]+\])?:", re.IGNORECASE),
    re.compile(r"^\s*error\b", re.IGNORECASE),
    re.compile(r"\bpanicked at\b", re.IGNORECASE),
    re.compile(r"^\s*thread .+ panicked", re.IGNORECASE),
    re.compile(r"^\s*Traceback \(most recent call last\):"),
    re.compile(r"^\s*(?:AssertionError|RuntimeError|ValueError|TypeError):"),
    re.compile(r"^\s*failures:\s*$", re.IGNORECASE),
    re.compile(r"^\s*test result: FAILED", re.IGNORECASE),
    re.compile(r"^\s*FAILED(?:\s|$)", re.IGNORECASE),
    re.compile(r"^\s*FAIL(?:\s|$)", re.IGNORECASE),
    re.compile(r"^\s*Caused by:\s*\S", re.IGNORECASE),
)
TEST_RESULT_RE = re.compile(
    r"test result: ok\.\s*(\d+) passed;\s*(\d+) failed;\s*(\d+) ignored",
    re.IGNORECASE,
)
SAFE_NAME_RE = re.compile(r"[^a-zA-Z0-9_.-]+")


class SummaryError(RuntimeError):
    """The wrapper cannot locate its configuration or output paths."""


def find_repository(start: Path | None = None) -> Path:
    override = os.environ.get("BLACKSITE_WORKSPACE_ROOT")
    configured = None
    config_path = (start or Path.cwd()) / ".codex/config.toml"
    if not override and config_path.is_file():
        with config_path.open("rb") as handle:
            configured = tomllib.load(handle).get("workspace_root")
    candidates = [Path(override or configured)] if (override or configured) else [start or Path.cwd()]
    if start is None and not override:
        candidates.append(Path(__file__).parent)
    visited: set[Path] = set()
    for candidate in candidates:
        candidate = candidate.expanduser().absolute()
        for current in (candidate, *candidate.parents):
            canonical = current.resolve(strict=False)
            if canonical in visited:
                continue
            visited.add(canonical)
            if (current / WORKFLOW_PATH).is_file():
                return current
    searched = ", ".join(str(path) for path in candidates)
    raise SummaryError(f"could not locate {WORKFLOW_PATH} from {searched}")


def load_workflow(root: Path) -> dict[str, Any]:
    path = root / WORKFLOW_PATH
    try:
        with path.open("rb") as handle:
            workflow = tomllib.load(handle)
    except OSError as error:
        raise SummaryError(f"cannot read workflow configuration {path}: {error}") from error
    except tomllib.TOMLDecodeError as error:
        raise SummaryError(f"invalid workflow configuration {path}: {error}") from error
    if workflow.get("version") != 1:
        raise SummaryError(f"unsupported or missing workflow version in {path}")
    return workflow


def output_settings(workflow: dict[str, Any]) -> tuple[str, int]:
    output = workflow.get("output", {})
    if not isinstance(output, dict):
        raise SummaryError("[output] must be a TOML table")
    log_dir = output.get("log_dir", ".codex/logs")
    max_lines = output.get("max_failure_lines", 120)
    if not isinstance(log_dir, str) or not log_dir:
        raise SummaryError("output.log_dir must be a non-empty path string")
    if not isinstance(max_lines, int) or not 1 <= max_lines <= 1000:
        raise SummaryError("output.max_failure_lines must be an integer from 1 to 1000")
    return log_dir, max_lines


def resolve_path(root: Path, raw: str) -> Path:
    path = Path(os.path.expandvars(os.path.expanduser(raw)))
    if not path.is_absolute():
        path = root / path
    return path.absolute()


def relative_display(root: Path, path: Path) -> str:
    try:
        return path.relative_to(root.resolve(strict=False)).as_posix()
    except ValueError:
        return str(path)


def safe_name(value: str) -> str:
    cleaned = SAFE_NAME_RE.sub("-", value.strip()).strip("-._")
    return cleaned[:80] or "command"


def default_log_path(root: Path, log_dir: str, gate: str) -> Path:
    timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S.%fZ")
    return resolve_path(root, str(Path(log_dir) / f"{timestamp}-{safe_name(gate)}.log"))


def strip_ansi(value: str) -> str:
    return ANSI_RE.sub("", value).rstrip("\r\n")


def duration_text(seconds: float) -> str:
    if seconds < 10:
        return f"{seconds:.2f}s"
    if seconds < 60:
        return f"{seconds:.1f}s"
    minutes, remainder = divmod(seconds, 60)
    return f"{int(minutes)}m {remainder:.0f}s"


def is_actionable(line: str) -> bool:
    plain = strip_ansi(line)
    return any(pattern.search(plain) for pattern in ACTIONABLE_PATTERNS)


def failure_excerpt(path: Path, limit: int) -> tuple[list[str], str | None]:
    """Return bounded context around the first actionable failure.

    The log is streamed rather than loaded wholesale, keeping the wrapper useful
    for very large compiler output.
    """

    before_count = min(12, max(0, limit // 4))
    before: deque[str] = deque(maxlen=before_count)
    excerpt: list[str] = []
    first_actionable: str | None = None
    matched = False

    with path.open("r", encoding="utf-8", errors="replace") as handle:
        for raw_line in handle:
            line = strip_ansi(raw_line)
            if not matched:
                if is_actionable(line):
                    matched = True
                    first_actionable = line.strip() or None
                    excerpt.extend(before)
                    excerpt.append(line)
                else:
                    before.append(line)
            elif len(excerpt) < limit:
                excerpt.append(line)
            else:
                break

    if not matched:
        # With no recognizable error, the command's tail is normally the most
        # useful bounded evidence (signal termination, tool-specific errors).
        tail: deque[str] = deque(maxlen=limit)
        with path.open("r", encoding="utf-8", errors="replace") as handle:
            for raw_line in handle:
                tail.append(strip_ansi(raw_line))
        excerpt = list(tail)
        first_actionable = next((line.strip() for line in excerpt if line.strip()), None)

    while excerpt and not excerpt[0].strip():
        excerpt.pop(0)
    while excerpt and not excerpt[-1].strip():
        excerpt.pop()
    return excerpt[:limit], first_actionable


def success_summary(path: Path) -> str:
    last_nonempty = ""
    test_result: tuple[str, str, str] | None = None
    explicit_pass = ""
    with path.open("r", encoding="utf-8", errors="replace") as handle:
        for raw_line in handle:
            line = strip_ansi(raw_line).strip()
            if not line:
                continue
            last_nonempty = line
            match = TEST_RESULT_RE.search(line)
            if match:
                test_result = match.groups()
            if line.startswith("PASS "):
                explicit_pass = line
    if test_result is not None:
        passed, failed, ignored = test_result
        return f"{passed} passed, {failed} failed, {ignored} ignored"
    if explicit_pass:
        return explicit_pass[:240]
    if last_nonempty.startswith("Finished "):
        return last_nonempty[:240]
    return "exit 0"


def write_result(path: Path, result: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    temporary: Path | None = None
    try:
        with tempfile.NamedTemporaryFile(
            "w",
            encoding="utf-8",
            dir=path.parent,
            prefix=f".{path.name}.",
            suffix=".tmp",
            delete=False,
        ) as handle:
            json.dump(result, handle, indent=2, sort_keys=True)
            handle.write("\n")
            handle.flush()
            os.fsync(handle.fileno())
            temporary = Path(handle.name)
        os.replace(temporary, path)
    finally:
        if temporary is not None and temporary.exists():
            temporary.unlink()


def normalized_exit_code(returncode: int) -> int:
    return 128 + abs(returncode) if returncode < 0 else returncode


def execute(
    *,
    root: Path,
    workflow: dict[str, Any],
    gate: str,
    command: Sequence[str],
    log: Path | None = None,
    json_result: Path | None = None,
    cwd: Path | None = None,
) -> int:
    if not command:
        raise SummaryError("a command is required after --")
    configured_log_dir, max_lines = output_settings(workflow)
    log_path = log or default_log_path(root, configured_log_dir, gate)
    result_path = json_result or log_path.with_suffix(log_path.suffix + ".json")
    if result_path == log_path:
        raise SummaryError("the JSON result path must differ from the full log path")
    command_cwd = cwd or Path.cwd()
    if not command_cwd.is_dir():
        raise SummaryError(f"command working directory does not exist: {command_cwd}")
    log_path.parent.mkdir(parents=True, exist_ok=True)

    started_wall = datetime.now(timezone.utc)
    started = time.monotonic()
    returncode = 127
    launch_error: str | None = None
    interrupted = False
    with log_path.open("wb") as log_handle:
        try:
            process = subprocess.Popen(
                list(command),
                cwd=command_cwd,
                stdout=log_handle,
                stderr=subprocess.STDOUT,
                shell=False,
            )
        except OSError as error:
            launch_error = f"error: unable to execute {command[0]!r}: {error}"
            log_handle.write((launch_error + "\n").encode("utf-8", errors="replace"))
        else:
            try:
                returncode = normalized_exit_code(process.wait())
            except KeyboardInterrupt:
                interrupted = True
                process.terminate()
                try:
                    process.wait(timeout=5)
                except subprocess.TimeoutExpired:
                    process.kill()
                    process.wait()
                returncode = 130
                log_handle.write(b"\nerror: command interrupted by user\n")

    duration = time.monotonic() - started
    status = "PASS" if returncode == 0 else "FAIL"
    summary = success_summary(log_path) if returncode == 0 else "command failed"
    excerpt: list[str] = []
    actionable: str | None = launch_error
    if returncode != 0:
        excerpt, detected = failure_excerpt(log_path, max_lines)
        actionable = actionable or detected

    result = {
        "schema_version": 1,
        "gate": gate,
        "status": status,
        "exit_code": returncode,
        "duration_seconds": round(duration, 6),
        "log": relative_display(root, log_path),
        "result": relative_display(root, result_path),
        "command": list(command),
        "command_display": shlex.join(command),
        "cwd": str(command_cwd.resolve(strict=False)),
        "started_at": started_wall.isoformat(timespec="seconds"),
        "finished_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
        "summary": summary,
        "first_actionable_failure": actionable,
        "interrupted": interrupted,
    }
    write_result(result_path, result)

    if returncode == 0:
        print(f"PASS {gate} — {duration_text(duration)} — {summary}")
    else:
        print(f"FAIL {gate} — {duration_text(duration)}")
        for line in excerpt:
            print(line)
        print(f"Full log: {relative_display(root, log_path)}")
    return returncode


def parse_arguments(argv: Sequence[str]) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--gate", required=True, help="short verification gate name")
    parser.add_argument("--json-result", help="machine-readable result path")
    parser.add_argument("--log", help="complete combined-output log path")
    parser.add_argument("--cwd", help="command working directory; defaults to the invocation directory")
    parser.add_argument("command", nargs=argparse.REMAINDER)
    args = parser.parse_args(argv)
    if args.command and args.command[0] == "--":
        args.command = args.command[1:]
    if not args.command:
        parser.error("a command is required after --")
    return args


def self_test() -> int:
    with tempfile.TemporaryDirectory(prefix="blacksite-summary-self-test-") as temporary:
        root = Path(temporary)
        (root / ".codex").mkdir()
        (root / ".codex/workflow.toml").write_text(
            """version = 1
[output]
log_dir = ".codex/logs"
max_failure_lines = 4
""",
            encoding="utf-8",
        )
        nested = root / "nested"
        nested.mkdir()
        found = find_repository(nested)
        workflow = load_workflow(found)
        parsed = parse_arguments(
            [
                "--gate",
                "parser-fixture",
                "--json-result",
                ".codex/logs/parser.json",
                "--",
                sys.executable,
                "--version",
            ]
        )
        if parsed.command != [sys.executable, "--version"]:
            raise AssertionError("command arguments after -- were not preserved")

        success_log = root / ".codex/logs/success.log"
        success_json = root / ".codex/logs/success.json"
        captured = io.StringIO()
        with contextlib.redirect_stdout(captured):
            success = execute(
                root=root,
                workflow=workflow,
                gate="self-test-pass",
                command=[
                    sys.executable,
                    "-c",
                    "print('test result: ok. 3 passed; 0 failed; 1 ignored')",
                ],
                log=success_log,
                json_result=success_json,
                cwd=root,
            )
        if success != 0:
            raise AssertionError("successful fixture returned a failure")
        if not captured.getvalue().startswith("PASS self-test-pass"):
            raise AssertionError("successful fixture did not emit a compact PASS summary")
        success_data = json.loads(success_json.read_text(encoding="utf-8"))
        for key in ("status", "exit_code", "duration_seconds", "log"):
            if key not in success_data:
                raise AssertionError(f"result JSON omitted required key {key}")
        if success_data["status"] != "PASS" or success_data["exit_code"] != 0:
            raise AssertionError("successful result JSON is incorrect")

        failure_log = root / ".codex/logs/failure.log"
        failure_json = root / ".codex/logs/failure.json"
        captured = io.StringIO()
        with contextlib.redirect_stdout(captured):
            failure = execute(
                root=root,
                workflow=workflow,
                gate="self-test-fail",
                command=[
                    sys.executable,
                    "-c",
                    "print('noise'); print('error[E0001]: actionable'); print('detail'); raise SystemExit(7)",
                ],
                log=failure_log,
                json_result=failure_json,
                cwd=root,
            )
        if failure != 7:
            raise AssertionError("failing fixture did not preserve exit code 7")
        visible_lines = captured.getvalue().splitlines()
        if not visible_lines or not visible_lines[0].startswith("FAIL self-test-fail"):
            raise AssertionError("failing fixture did not emit a compact FAIL summary")
        if len(visible_lines[1:-1]) > 4:
            raise AssertionError("failure excerpt exceeded output.max_failure_lines")
        failure_data = json.loads(failure_json.read_text(encoding="utf-8"))
        if failure_data["status"] != "FAIL" or failure_data["exit_code"] != 7:
            raise AssertionError("failure result JSON is incorrect")
        if "error[E0001]" not in failure_log.read_text(encoding="utf-8"):
            raise AssertionError("full failure output was not captured")
    print("PASS summarize-command-self-test — logs, bounded failure, JSON, exit preservation")
    return 0


def main(argv: Sequence[str] | None = None) -> int:
    arguments = list(sys.argv[1:] if argv is None else argv)
    if arguments in (["--self-test"], ["self-test"]):
        try:
            return self_test()
        except (AssertionError, OSError, SummaryError, subprocess.SubprocessError, json.JSONDecodeError) as error:
            print(f"FAIL summarize-command-self-test — {error}", file=sys.stderr)
            return 1

    try:
        args = parse_arguments(arguments)
        root = find_repository()
        workflow = load_workflow(root)
        log = resolve_path(root, args.log) if args.log else None
        result = resolve_path(root, args.json_result) if args.json_result else None
        cwd = resolve_path(root, args.cwd) if args.cwd else Path.cwd()
        return execute(
            root=root,
            workflow=workflow,
            gate=args.gate,
            command=args.command,
            log=log,
            json_result=result,
            cwd=cwd,
        )
    except (OSError, SummaryError) as error:
        print(f"FAIL summarize-command — {error}", file=sys.stderr)
        return 2


if __name__ == "__main__":
    raise SystemExit(main())
