#!/usr/bin/env python3
"""pr-checklist.py -- Automated PR quality gate for Gitea CI.

Enforces the review standards that agents skip when left to self-approve.
Runs in CI on every pull_request event. Exits non-zero on any failure.

Checks:
  1. PR has >0 file changes (no empty PRs)
  2. PR branch is not behind base branch
  3. PR does not bundle >3 unrelated issues
  4. Changed .py files pass syntax check (python -c import)
  5. Changed .sh files are executable
  6. PR body references an issue number
  7. At least 1 non-author review exists (warning only)

Refs: #393 (PERPLEXITY-08), Epic #385
"""
from __future__ import annotations

import json
import os
import re
import subprocess
import sys
from pathlib import Path


def fail(msg: str) -> None:
    print(f"FAIL: {msg}", file=sys.stderr)


def warn(msg: str) -> None:
    print(f"WARN: {msg}", file=sys.stderr)


def ok(msg: str) -> None:
    print(f"  OK: {msg}")


def get_changed_files() -> list[str]:
    """Return list of files changed in this PR vs base branch."""
    base = os.environ.get("GITHUB_BASE_REF", "main")
    try:
        result = subprocess.run(
            ["git", "diff", "--name-only", f"origin/{base}...HEAD"],
            capture_output=True, text=True, check=True,
        )
        return [f for f in result.stdout.strip().splitlines() if f]
    except subprocess.CalledProcessError:
        # Fallback: diff against HEAD~1
        result = subprocess.run(
            ["git", "diff", "--name-only", "HEAD~1"],
            capture_output=True, text=True, check=True,
        )
        return [f for f in result.stdout.strip().splitlines() if f]


def check_has_changes(files: list[str]) -> bool:
    """Check 1: PR has >0 file changes."""
    if not files:
        fail("PR has 0 file changes. Empty PRs are not allowed.")
        return False
    ok(f"PR changes {len(files)} file(s)")
    return True


def check_not_behind_base() -> bool:
    """Check 2: PR branch is not behind base."""
    base = os.environ.get("GITHUB_BASE_REF", "main")
    try:
        result = subprocess.run(
            ["git", "rev-list", "--count", f"HEAD..origin/{base}"],
            capture_output=True, text=True, check=True,
        )
        behind = int(result.stdout.strip())
        if behind > 0:
            fail(f"Branch is {behind} commit(s) behind {base}. Rebase or merge.")
            return False
        ok(f"Branch is up-to-date with {base}")
        return True
    except (subprocess.CalledProcessError, ValueError):
        warn("Could not determine if branch is behind base (git fetch may be needed)")
        return True  # Don't block on CI fetch issues


def check_issue_bundling(pr_body: str) -> bool:
    """Check 3: PR does not bundle >3 unrelated issues."""
    issue_refs = set(re.findall(r"#(\d+)", pr_body))
    if len(issue_refs) > 3:
        fail(f"PR references {len(issue_refs)} issues ({', '.join(sorted(issue_refs))}). "
             "Max 3 per PR to prevent bundling. Split into separate PRs.")
        return False
    ok(f"PR references {len(issue_refs)} issue(s) (max 3)")
    return True


def check_python_syntax(files: list[str]) -> bool:
    """Check 4: Changed .py files have valid syntax."""
    py_files = [f for f in files if f.endswith(".py") and Path(f).exists()]
    if not py_files:
        ok("No Python files changed")
        return True

    all_ok = True
    for f in py_files:
        result = subprocess.run(
            [sys.executable, "-c", f"import ast; ast.parse(open('{f}').read())"],
            capture_output=True, text=True,
        )
        if result.returncode != 0:
            fail(f"Syntax error in {f}: {result.stderr.strip()[:200]}")
            all_ok = False

    if all_ok:
        ok(f"All {len(py_files)} Python file(s) pass syntax check")
    return all_ok


def check_shell_executable(files: list[str]) -> bool:
    """Check 5: Changed .sh files are executable."""
    sh_files = [f for f in files if f.endswith(".sh") and Path(f).exists()]
    if not sh_files:
        ok("No shell scripts changed")
        return True

    all_ok = True
    for f in sh_files:
        if not os.access(f, os.X_OK):
            fail(f"{f} is not executable. Run: chmod +x {f}")
            all_ok = False

    if all_ok:
        ok(f"All {len(sh_files)} shell script(s) are executable")
    return all_ok


def check_issue_reference(pr_body: str) -> bool:
    """Check 6: PR body references an issue number."""
    if re.search(r"#\d+", pr_body):
        ok("PR body references at least one issue")
        return True
    fail("PR body does not reference any issue (e.g. #123). "
         "Every PR must trace to an issue.")
    return False


def main() -> int:
    print("=" * 60)
    print("PR Checklist — Automated Quality Gate")
    print("=" * 60)
    print()

    # Get PR body from env or git log
    pr_body = os.environ.get("PR_BODY", "")
    if not pr_body:
        try:
            result = subprocess.run(
                ["git", "log", "--format=%B", "-1"],
                capture_output=True, text=True, check=True,
            )
            pr_body = result.stdout
        except subprocess.CalledProcessError:
            pr_body = ""

    files = get_changed_files()
    failures = 0

    checks = [
        check_has_changes(files),
        check_not_behind_base(),
        check_issue_bundling(pr_body),
        check_python_syntax(files),
        check_shell_executable(files),
        check_issue_reference(pr_body),
    ]

    failures = sum(1 for c in checks if not c)

    print()
    print("=" * 60)
    if failures:
        print(f"RESULT: {failures} check(s) FAILED")
        print("Fix the issues above and push again.")
        return 1
    else:
        print("RESULT: All checks passed")
        return 0


if __name__ == "__main__":
    sys.exit(main())