#!/usr/bin/env python3
"""Deterministic agent workflow quality gates."""

from __future__ import annotations

import argparse
from datetime import datetime, timedelta, timezone
import fnmatch
import hashlib
import json
import os
import re
import shlex
import subprocess
import sys
import uuid
from pathlib import Path


ROOT = Path(os.environ.get("AGENT_QUALITY_ROOT", Path(__file__).resolve().parents[1])).resolve()
DEFAULT_MANIFEST = ROOT / ".agents" / "quality.json"
DEFAULT_INVENTORY = ROOT / "docs" / "agent-quality.md"
DEFAULT_RECEIPT_ROOT = (
    Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local" / "state"))
    / "dotfiles-agent-runs"
)
WORKLOG_SECTIONS = (
    "Objective",
    "Decisions",
    "Evidence",
    "Reviews",
    "Feedback",
    "Remaining work",
    "Commits",
)


def load_manifest(path: Path) -> dict:
    with path.open() as handle:
        return json.load(handle)


def render_inventory(manifest: dict) -> str:
    lines = [
        "---",
        "purpose: Generated inventory of agent quality gate capabilities.",
        "applies_to: Agent finish, audit, and capability discovery workflows.",
        "entrypoint: Edit .agents/quality.json, then run python3 bin/agent-quality inventory.",
        "verification: python3 bin/agent-quality inventory --check",
        "update_when: .agents/quality.json checks or inventory generator change.",
        "---",
        "",
        "# Agent quality capabilities",
        "",
        "Generated by `bin/agent-quality inventory`. Edit `.agents/quality.json`, not this file.",
        "",
    ]
    for check in manifest.get("checks", []):
        paths = ", ".join(check.get("paths", ["always"])) or "always"
        lines.extend(
            [
                f"## {check['id']}",
                "",
                f"- Kind: {check['kind']}",
                f"- Applies to: `{paths}`",
                f"- Command: `{check['command']}`",
                f"- Purpose: {check['description']}",
                "",
            ]
        )
    lines.extend(
        [
            "## Operating rules",
            "",
            "- `PASS` means the command ran and succeeded.",
            "- `FAIL` means the command ran and failed.",
            "- `NOT_APPLICABLE` means changed paths did not select the check; it is never a pass.",
            "- `SKIP` means a required prerequisite was unavailable; it is never a pass.",
            "- Add subsystem checks to the manifest with narrow path globs and non-interactive commands.",
            "",
        ]
    )
    return "\n".join(lines)


def inventory(args: argparse.Namespace) -> int:
    manifest = load_manifest(Path(args.manifest))
    rendered = render_inventory(manifest)
    output = Path(args.output)
    if args.check:
        current = output.read_text() if output.exists() else ""
        if current != rendered:
            print(f"stale generated inventory: {output}", file=sys.stderr)
            return 1
        print(f"PASS inventory {output}")
        return 0
    output.parent.mkdir(parents=True, exist_ok=True)
    output.write_text(rendered)
    print(output)
    return 0


def validate_worklog(args: argparse.Namespace) -> int:
    path = Path(args.path)
    if not path.exists():
        print(f"missing worklog: {path}", file=sys.stderr)
        return 1
    text = path.read_text()
    missing = [section for section in WORKLOG_SECTIONS if f"## {section}" not in text]
    if not re.search(r"^Status: (active|blocked|complete)$", text, re.MULTILINE):
        missing.append("Status: active|blocked|complete")
    if missing:
        print("missing worklog fields: " + ", ".join(missing), file=sys.stderr)
        return 1
    print(f"PASS worklog {path}")
    return 0


def test_files(paths: list[str]) -> list[Path]:
    found: set[Path] = set()
    for raw in paths:
        path = Path(raw)
        if path.is_file():
            found.add(path)
        elif path.is_dir():
            for pattern in ("test_*.py", "*_test.py", "*.test.ts", "*.test.mjs", "*.test.js"):
                found.update(path.rglob(pattern))
    return sorted(path for path in found if "node_modules" not in path.parts)


def audit_tests(args: argparse.Namespace) -> int:
    findings: list[str] = []
    for path in test_files(args.paths or ["tests"]):
        text = path.read_text(errors="replace")
        if re.search(r"def test_[^(]+\([^)]*\):\s*(?:#.*\s*)?pass(?:\s|$)", text):
            findings.append(f"{path}: empty test body")
        if re.search(
            r"\b(?:assert|expect)\b\s*(?:\(?\s*)?(?:True|true|1)(?:\s*\)?)", text
        ):
            findings.append(f"{path}: constant truth assertion")
        if re.search(r"@pytest\.mark\.skip\b|\b(?:describe|it|test)\.skip\s*\(", text):
            findings.append(f"{path}: skipped test requires audit")
    if findings:
        print("\n".join(findings))
        return 1
    print("PASS test-confidence")
    return 0


def changed_paths(args: argparse.Namespace) -> list[str]:
    if args.changed:
        return args.changed
    result = subprocess.run(
        ["git", "diff", "--name-only", "HEAD"], text=True, capture_output=True, check=False
    )
    untracked = subprocess.run(
        ["git", "ls-files", "--others", "--exclude-standard"],
        text=True,
        capture_output=True,
        check=False,
    )
    return sorted({line for line in (result.stdout + untracked.stdout).splitlines() if line})


def applies(check: dict, changed: list[str]) -> bool:
    patterns = check.get("paths")
    if not patterns:
        return True
    return any(fnmatch.fnmatch(path, pattern) for path in changed for pattern in patterns)


def finish(args: argparse.Namespace) -> int:
    manifest = load_manifest(Path(args.manifest))
    changed = changed_paths(args)
    failed = False
    if args.worklog:
        failed = validate_worklog(argparse.Namespace(path=args.worklog)) != 0
    for check in manifest.get("checks", []):
        if not applies(check, changed):
            print(f"NOT_APPLICABLE {check['id']}")
            continue
        if args.dry_run:
            print(f"RUN {check['id']} {check['command']}")
            continue
        result = subprocess.run(check["command"], shell=True, cwd=ROOT, check=False)
        status = "PASS" if result.returncode == 0 else "FAIL"
        print(f"{status} {check['id']}")
        failed = failed or result.returncode != 0
    return 1 if failed else 0


def file_digest(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def visual_compare(args: argparse.Namespace) -> int:
    baseline = Path(args.baseline)
    current = Path(args.current)
    baseline_files = {path.relative_to(baseline) for path in baseline.rglob("*.png")}
    current_files = {path.relative_to(current) for path in current.rglob("*.png")}
    if not baseline_files or baseline_files != current_files:
        print("visual baseline/current PNG sets differ or are empty", file=sys.stderr)
        return 1
    failures: list[str] = []
    for relative in sorted(baseline_files):
        expected = baseline / relative
        actual = current / relative
        if file_digest(expected) == file_digest(actual):
            continue
        result = subprocess.run(
            ["magick", "compare", "-metric", "AE", str(expected), str(actual), "null:"],
            text=True,
            capture_output=True,
            check=False,
        )
        try:
            changed_pixels = int(result.stderr.strip())
        except ValueError:
            print("ImageMagick comparison unavailable or invalid", file=sys.stderr)
            return 2
        if changed_pixels > args.max_changed_pixels:
            failures.append(f"{relative}: {changed_pixels} changed pixels")
    if failures:
        print("\n".join(failures))
        return 1
    print(f"PASS visual-regression {len(baseline_files)} images")
    return 0


def review(args: argparse.Namespace) -> int:
    active = args.active_model_family.lower()
    reviewer = args.reviewer.lower()
    if active == reviewer:
        print("reviewer must use a different model family", file=sys.stderr)
        return 2
    prompt = (
        f"Perform the {args.stage} risk-gate review for this repository. "
        "Read AGENTS.md, AGENT_WORKFLOW.md, the task worklog, and relevant changes. "
        "Return findings first with file references. Review maintainability, correctness, "
        "security, performance, test confidence, documentation drift, and AI-generated code smells. "
        f"Worklog: {args.worklog}"
    )
    command = ["acpx", reviewer, "exec", prompt]
    if args.dry_run:
        print(" ".join(shlex.quote(part) for part in command))
        return 0
    result = subprocess.run(command, cwd=ROOT, check=False)
    return result.returncode


def run_vcs(command: list[str], cwd: Path) -> subprocess.CompletedProcess[str]:
    try:
        return subprocess.run(
            command, cwd=cwd, text=True, capture_output=True, check=False
        )
    except FileNotFoundError:
        return subprocess.CompletedProcess(
            command,
            127,
            stdout="",
            stderr=f"{command[0]}: command not found",
        )


def output_line(command: list[str], cwd: Path) -> str | None:
    result = run_vcs(command, cwd)
    value = result.stdout.strip()
    return value if result.returncode == 0 and value else None


def git_worktree(repo: Path) -> bool:
    git_dir = output_line(
        ["git", "rev-parse", "--path-format=absolute", "--git-dir"], repo
    )
    common_dir = output_line(
        ["git", "rev-parse", "--path-format=absolute", "--git-common-dir"], repo
    )
    return bool(
        git_dir and common_dir and Path(git_dir).resolve() != Path(common_dir).resolve()
    )


def receipt_root(path: str | None) -> Path:
    return Path(path).expanduser().resolve() if path else DEFAULT_RECEIPT_ROOT


def write_receipt(receipt: dict, state_dir: Path) -> Path:
    repository = receipt["repositoryRoot"]
    repository_key = hashlib.sha256(repository.encode()).hexdigest()[:12]
    destination = state_dir / repository_key / f"{receipt['runId']}.json"
    destination.parent.mkdir(parents=True, exist_ok=True)
    receipt["receiptPath"] = str(destination)
    destination.write_text(json.dumps(receipt, indent=2, sort_keys=True) + "\n")
    return destination


def jj_ids(workspace: Path) -> dict[str, str | None]:
    revision = output_line(
        [
            "jj",
            "log",
            "--ignore-working-copy",
            "-r",
            "@",
            "--no-graph",
            "-T",
            'change_id ++ "\\n" ++ commit_id ++ "\\n"',
        ],
        workspace,
    )
    parts = revision.splitlines() if revision else []
    operation = output_line(
        ["jj", "op", "log", "--limit", "1", "--no-graph", "-T", 'id ++ "\\n"'],
        workspace,
    )
    return {
        "changeId": parts[0] if parts else None,
        "commitId": parts[1] if len(parts) > 1 else None,
        "operationId": operation,
    }


def start(args: argparse.Namespace) -> int:
    repo = Path(args.repo).expanduser().resolve()
    if not repo.is_dir():
        print(f"repository path does not exist: {repo}", file=sys.stderr)
        return 2

    jj_root = output_line(["jj", "root", "--ignore-working-copy"], repo)
    git_root = output_line(["git", "rev-parse", "--show-toplevel"], repo)
    if not jj_root and not git_root:
        print(f"not a jj or Git repository: {repo}", file=sys.stderr)
        return 2

    backend = "jj" if jj_root else "git"
    repository_root = Path(jj_root or git_root or repo).resolve()
    workspace_root = repo
    if args.workspace:
        destination = Path(args.workspace).expanduser().resolve()
        if backend != "jj":
            boundary = "Git worktree" if git_worktree(repo) else "Git-only repository"
            print(
                f"{boundary}: refusing to invent nested jj metadata; initialize jj from the primary checkout, then rerun agent-start there",
                file=sys.stderr,
            )
            return 2
        destination.parent.mkdir(parents=True, exist_ok=True)
        command = [
            "jj",
            "workspace",
            "add",
            "-R",
            str(repository_root),
            "--name",
            re.sub(r"[^a-zA-Z0-9._-]+", "-", args.task).strip("-") or "agent-run",
            str(destination),
        ]
        if args.base:
            command.extend(["-r", args.base])
        created = run_vcs(command, repository_root)
        if created.returncode != 0:
            print(created.stderr.rstrip(), file=sys.stderr)
            return created.returncode
        workspace_root = destination
    elif backend == "jj":
        current_workspace = output_line(["jj", "workspace", "root"], repo)
        if current_workspace:
            workspace_root = Path(current_workspace).resolve()

    now = datetime.now(timezone.utc)
    receipt: dict = {
        "schemaVersion": 1,
        "runId": f"{now.strftime('%Y%m%dT%H%M%SZ')}-{uuid.uuid4().hex[:12]}",
        "task": args.task,
        "runtime": args.runtime,
        "model": args.model,
        "repositoryRoot": str(repository_root),
        "workspaceRoot": str(workspace_root),
        "backend": backend,
        "status": "active",
        "startedAt": now.isoformat().replace("+00:00", "Z"),
        "metrics": {"retries": 0, "userCorrections": 0},
    }
    if backend == "jj":
        receipt["vcs"] = jj_ids(workspace_root)
    else:
        receipt["vcs"] = {
            "commitId": output_line(["git", "rev-parse", "HEAD"], repository_root),
            "branch": output_line(["git", "branch", "--show-current"], repository_root),
            "isWorktree": git_worktree(repo),
        }
    write_receipt(receipt, receipt_root(args.state_dir))
    print(json.dumps(receipt, sort_keys=True))
    return 0


def complete(args: argparse.Namespace) -> int:
    path = Path(args.receipt).expanduser().resolve()
    try:
        receipt = json.loads(path.read_text())
    except (OSError, json.JSONDecodeError) as error:
        print(f"cannot read receipt {path}: {error}", file=sys.stderr)
        return 2
    if receipt.get("schemaVersion") != 1:
        print(
            f"unsupported receipt schema: {receipt.get('schemaVersion')}",
            file=sys.stderr,
        )
        return 2

    aligned = bool(
        args.local_tip and args.remote_tip and args.local_tip == args.remote_tip
    )
    receipt["status"] = "complete" if aligned else "false_done"
    receipt["completedAt"] = (
        datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
    )
    receipt["landing"] = {
        "revision": args.revision,
        "localTip": args.local_tip,
        "remoteTip": args.remote_tip,
        "remoteAligned": aligned,
    }
    receipt["metrics"] = {
        "retries": args.retries,
        "userCorrections": args.user_corrections,
    }
    if args.error:
        receipt["error"] = args.error
    path.write_text(json.dumps(receipt, indent=2, sort_keys=True) + "\n")
    print(json.dumps(receipt, sort_keys=True))
    return 0 if aligned else 1


def sweep(args: argparse.Namespace) -> int:
    state_dir = receipt_root(args.state_dir)
    cutoff = datetime.now(timezone.utc) - timedelta(days=args.since_days)
    candidates = sorted(
        state_dir.rglob("*.json") if state_dir.exists() else [],
        key=lambda path: path.stat().st_mtime,
        reverse=True,
    )[: args.limit]
    receipts: list[dict] = []
    invalid = 0
    for path in candidates:
        try:
            receipt = json.loads(path.read_text())
            started = datetime.fromisoformat(
                receipt["startedAt"].replace("Z", "+00:00")
            )
            if receipt.get("schemaVersion") == 1 and started >= cutoff:
                receipts.append(receipt)
        except (OSError, KeyError, ValueError, json.JSONDecodeError):
            invalid += 1
    summary = {
        "schemaVersion": 1,
        "runs": len(receipts),
        "active": sum(item.get("status") == "active" for item in receipts),
        "complete": sum(item.get("status") == "complete" for item in receipts),
        "falseDone": sum(item.get("status") == "false_done" for item in receipts),
        "retries": sum(item.get("metrics", {}).get("retries", 0) for item in receipts),
        "userCorrections": sum(
            item.get("metrics", {}).get("userCorrections", 0) for item in receipts
        ),
        "errors": sum(bool(item.get("error")) for item in receipts),
        "invalidReceipts": invalid,
        "stateDirectory": str(state_dir),
    }
    if args.json:
        print(json.dumps(summary, sort_keys=True))
    else:
        print(
            "agent sweep: "
            f"runs={summary['runs']} complete={summary['complete']} "
            f"active={summary['active']} false_done={summary['falseDone']} "
            f"retries={summary['retries']} corrections={summary['userCorrections']} "
            f"errors={summary['errors']} invalid={summary['invalidReceipts']}"
        )
        print(f"receipts: {state_dir}")
    return 1 if summary["falseDone"] or summary["errors"] or invalid else 0


def parser() -> argparse.ArgumentParser:
    root = argparse.ArgumentParser()
    sub = root.add_subparsers(dest="command", required=True)

    inv = sub.add_parser("inventory")
    inv.add_argument("--manifest", default=str(DEFAULT_MANIFEST))
    inv.add_argument("--output", default=str(DEFAULT_INVENTORY))
    inv.add_argument("--check", action="store_true")
    inv.set_defaults(func=inventory)

    worklog = sub.add_parser("validate-worklog")
    worklog.add_argument("path")
    worklog.set_defaults(func=validate_worklog)

    audit = sub.add_parser("audit-tests")
    audit.add_argument("paths", nargs="*")
    audit.set_defaults(func=audit_tests)

    done = sub.add_parser("finish")
    done.add_argument("--manifest", default=str(DEFAULT_MANIFEST))
    done.add_argument("--changed", action="append")
    done.add_argument("--worklog")
    done.add_argument("--dry-run", action="store_true")
    done.set_defaults(func=finish)

    visual = sub.add_parser("visual-compare")
    visual.add_argument("baseline")
    visual.add_argument("current")
    visual.add_argument("--max-changed-pixels", type=int, default=0)
    visual.set_defaults(func=visual_compare)

    gate = sub.add_parser("review")
    gate.add_argument("stage", choices=("plan", "landing"))
    gate.add_argument("--active-model-family", required=True)
    gate.add_argument("--reviewer", default="claude")
    gate.add_argument("--worklog", required=True)
    gate.add_argument("--dry-run", action="store_true")
    gate.set_defaults(func=review)

    begin = sub.add_parser("start")
    begin.add_argument("--repo", default=".")
    begin.add_argument("--workspace")
    begin.add_argument("--base")
    begin.add_argument("--task", required=True)
    begin.add_argument("--runtime", default="unknown")
    begin.add_argument("--model")
    begin.add_argument("--state-dir")
    begin.set_defaults(func=start)

    completed = sub.add_parser("complete")
    completed.add_argument("receipt")
    completed.add_argument("--revision")
    completed.add_argument("--local-tip", required=True)
    completed.add_argument("--remote-tip", required=True)
    completed.add_argument("--retries", type=int, default=0)
    completed.add_argument("--user-corrections", type=int, default=0)
    completed.add_argument("--error")
    completed.set_defaults(func=complete)

    recent = sub.add_parser("sweep")
    recent.add_argument("--state-dir")
    recent.add_argument("--limit", "--commits", dest="limit", type=int, default=200)
    recent.add_argument("--since-days", type=int, default=30)
    recent.add_argument("--json", action="store_true")
    recent.set_defaults(func=sweep)
    return root


def main() -> int:
    args = parser().parse_args()
    return args.func(args)


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