#!/usr/bin/env python3
"""arq-git v0 · workspace-governance primitive · ARQERA runtime.

Per operator directive 2026-05-20 (and `arq://act/arqera_bug_branch_hygiene_unenforced/...`):
Twin/Claude has repeatedly created branches from stale-local-main, leaving
worktree drift + merge conflicts. arq-git v0 is the bounded substrate-
governed wrapper around local git operations.

Verbs (v0):
  branch <name>     - create branch from FRESH origin/main (fetches first)
  status            - git status + substrate-attest
  diff-scope        - diff main..HEAD with scope assertion
  commit <msg>      - git commit with substrate-attest
  rebase-main       - rebase onto fresh origin/main with conflict-attest
  clean-scope <f>   - reset unrelated worktree files to origin/main
  handoff <peer>    - emit handoff act + release claim

Authority scope (bounded per operator directive 2026-05-20):
  ALLOWED:    branch (off origin/main) · status · diff-scope · commit ·
              rebase-main · clean-scope · handoff (substrate-attested handoffs)
  DENIED:     merge/main · push --force to main · gh pr merge · kubectl ·
              gcloud · secret rotation · production deploy

Every mutating verb emits a signed act via `twin --use-keychain act emit`
so the substrate ledger sees every workspace-governance action.

Composes with:
  - arq-github (existing · remote git ops: PR/push/workflow/merge)
  - arq-cli-exec (existing · gated shell dispatch)
  - twin (existing · substrate primitive)
"""
from __future__ import annotations

import argparse
import json
import os
import shutil
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path

POLICY_VERSION = "arq-git-v0-2026-05-20"


def _resolve_twin_bin() -> str | None:
    """Find the twin CLI · env override → PATH lookup → None (best-effort emit_act tolerates absence)."""
    override = os.environ.get("TWIN_BIN")
    if override and Path(override).exists():
        return override
    return shutil.which("twin")


TWIN_BIN = _resolve_twin_bin()


def now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def now_ts_compact() -> str:
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H%M%SZ")


def emit_act(act_type: str, ref: str, payload: dict) -> None:
    """Best-effort substrate emission. Never raises. Silently no-ops if twin not on PATH."""
    if TWIN_BIN is None:
        return
    try:
        subprocess.run(
            [TWIN_BIN, "--use-keychain", "act", "emit", "act", act_type,
             f"{ref}-{now_ts_compact()}",
             "--payload", json.dumps({**payload, "policy": POLICY_VERSION, "issued_at": now_iso()})],
            check=False, timeout=10, capture_output=True,
        )
    except Exception as e:
        print(f"arq-git: emit_act warning: {e}", file=sys.stderr)


def run_git(args: list[str], check: bool = True, capture: bool = False) -> subprocess.CompletedProcess:
    """Run a `git` subprocess. Centralised so every git invocation has one path."""
    return subprocess.run(
        ["git"] + args,
        check=check,
        capture_output=capture,
        text=True,
    )


# ─── safety guards ────────────────────────────────────────────────────────


def deny_if_main_target(args: list[str]) -> None:
    """Refuse any operation that pushes/merges/force-pushes to main · per authority scope."""
    joined = " ".join(args)
    deny_patterns = [
        "push --force",
        "push -f",
        "push origin main",
        "merge --no-ff origin/main",
        "reset --hard origin/main",
        "pr merge",
    ]
    for pat in deny_patterns:
        if pat in joined:
            print(f"arq-git: DENIED — operation matches forbidden pattern: {pat}", file=sys.stderr)
            print("arq-git: merge/deploy authority is NOT granted to this primitive per operator directive.", file=sys.stderr)
            sys.exit(99)


def current_branch() -> str:
    r = run_git(["rev-parse", "--abbrev-ref", "HEAD"], capture=True)
    return r.stdout.strip()


# ─── verbs ────────────────────────────────────────────────────────────────


def cmd_branch(args) -> int:
    """Create branch from fresh origin/main. Always fetches first."""
    name = args.name
    if name in ("main", "master"):
        print("arq-git: refusing to overwrite main/master", file=sys.stderr)
        return 99

    print(f"arq-git branch: fetching origin/main ...")
    try:
        run_git(["fetch", "origin", "main"])
    except subprocess.CalledProcessError as e:
        emit_act("arq_git_branch_failed", name, {
            "branch": name, "stage": "fetch_origin_main", "exit_code": e.returncode,
        })
        print(f"arq-git: ✗ branch failed at fetch origin/main (exit {e.returncode})", file=sys.stderr)
        return e.returncode

    print(f"arq-git branch: creating {name} from origin/main")
    try:
        run_git(["checkout", "-b", name, "origin/main"])
    except subprocess.CalledProcessError as e:
        emit_act("arq_git_branch_failed", name, {
            "branch": name, "stage": "checkout_new_branch", "exit_code": e.returncode,
        })
        print(f"arq-git: ✗ branch failed at checkout (exit {e.returncode})", file=sys.stderr)
        return e.returncode

    emit_act("arq_git_branch_created", name, {
        "branch": name,
        "from": "origin/main",
        "fresh_fetch": True,
    })
    print(f"arq-git: ✓ created {name} from origin/main (fresh fetch)")
    return 0


def cmd_status(args) -> int:
    """git status + substrate-attest."""
    r = run_git(["status", "--porcelain=v1", "-b"], capture=True)
    print(r.stdout.rstrip())

    branch = current_branch()
    modified_count = sum(1 for line in r.stdout.splitlines()[1:] if line and line[0] != "?")
    untracked_count = sum(1 for line in r.stdout.splitlines()[1:] if line.startswith("??"))

    emit_act("arq_git_status_reported", branch, {
        "branch": branch,
        "modified_files": modified_count,
        "untracked_files": untracked_count,
    })
    return 0


def cmd_diff_scope(args) -> int:
    """Diff main..HEAD with scope assertion. Lists files + counts."""
    branch = current_branch()
    run_git(["fetch", "origin", "main"], check=False)

    r = run_git(["diff", "--stat", "origin/main..HEAD"], capture=True)
    print(r.stdout.rstrip())

    files_r = run_git(["diff", "--name-only", "origin/main..HEAD"], capture=True)
    files = [f for f in files_r.stdout.split("\n") if f.strip()]

    behind_r = run_git(["rev-list", "--count", "HEAD..origin/main"], capture=True)
    behind = int(behind_r.stdout.strip() or "0")

    print(f"\narq-git diff-scope: branch={branch} · files_changed={len(files)} · behind_main_by={behind}")

    if args.expect_max and len(files) > args.expect_max:
        print(f"arq-git: SCOPE VIOLATION — {len(files)} files exceeds --expect-max {args.expect_max}", file=sys.stderr)
        emit_act("arq_git_scope_violation", branch, {
            "branch": branch,
            "files_changed": len(files),
            "expected_max": args.expect_max,
        })
        return 2

    emit_act("arq_git_diff_scope_attested", branch, {
        "branch": branch,
        "files_changed": len(files),
        "files": files[:20],
        "behind_main_by": behind,
    })
    return 0


def cmd_commit(args) -> int:
    """git commit with substrate-attest. Refuses on main branch."""
    branch = current_branch()
    if branch in ("main", "master"):
        print(f"arq-git: refusing to commit directly to {branch}", file=sys.stderr)
        return 99

    cmd = ["commit", "-m", args.message]
    if args.no_verify:
        print("arq-git: --no-verify NOT honored — hooks are part of detection layer · per [[no-direct-shell-every-operation-via-arq-primitive-v1]]", file=sys.stderr)

    # NOTE: do NOT run deny_if_main_target on commit · commit is local-only.
    # The deny patterns target push/merge args, but commit -m args.message
    # can contain ANY text (including innocent references to push/force/merge
    # in the changelog itself). Dogfood-caught false-positive 2026-05-20 v0.1.
    r = run_git(cmd, check=False)
    if r.returncode != 0:
        emit_act("arq_git_commit_failed", branch, {"branch": branch, "exit_code": r.returncode})
        return r.returncode

    sha_r = run_git(["rev-parse", "HEAD"], capture=True)
    sha = sha_r.stdout.strip()[:12]
    emit_act("arq_git_commit_created", f"{branch}-{sha}", {
        "branch": branch,
        "sha": sha,
        "message_preview": args.message[:160],
    })
    print(f"arq-git: ✓ committed {sha} on {branch}")
    return 0


def cmd_rebase_main(args) -> int:
    """Rebase current branch onto fresh origin/main with conflict-attest."""
    branch = current_branch()
    if branch in ("main", "master"):
        print(f"arq-git: refusing to rebase {branch} onto itself", file=sys.stderr)
        return 99

    print("arq-git rebase-main: fetching origin/main ...")
    try:
        run_git(["fetch", "origin", "main"])
    except subprocess.CalledProcessError as e:
        emit_act("arq_git_rebase_failed", branch, {
            "branch": branch, "stage": "fetch_origin_main", "exit_code": e.returncode,
        })
        print(f"arq-git: ✗ rebase-main failed at fetch (exit {e.returncode})", file=sys.stderr)
        return e.returncode

    r = run_git(["rebase", "origin/main"], check=False)
    if r.returncode == 0:
        emit_act("arq_git_rebase_completed", branch, {"branch": branch, "from": "origin/main"})
        print(f"arq-git: ✓ rebased {branch} onto origin/main")
        return 0

    emit_act("arq_git_rebase_conflict", branch, {
        "branch": branch,
        "from": "origin/main",
        "exit_code": r.returncode,
        "operator_tier_surface_required": True,
    })
    print(f"arq-git: rebase conflict · use 'git rebase --abort' OR resolve manually then 'git rebase --continue'", file=sys.stderr)
    return r.returncode


def cmd_clean_scope(args) -> int:
    """Reset unrelated worktree files to origin/main version."""
    branch = current_branch()
    files = args.files
    if not files:
        print("arq-git clean-scope: no files specified", file=sys.stderr)
        return 64

    run_git(["fetch", "origin", "main"], check=False)
    for f in files:
        run_git(["checkout", "origin/main", "--", f], check=False)

    emit_act("arq_git_scope_cleaned", branch, {
        "branch": branch,
        "files_reset": files,
        "reset_from": "origin/main",
    })
    print(f"arq-git: ✓ cleaned {len(files)} files to origin/main version")
    return 0


def cmd_handoff(args) -> int:
    """Emit handoff act · release claim · per worker-continuity-supervisor handshake."""
    branch = current_branch()
    target_peer = args.target_peer
    note = args.note or ""

    emit_act("arq_git_workspace_handoff", f"{branch}-to-{target_peer.replace('/', '_').replace(':', '_')}", {
        "branch": branch,
        "from_peer": "current_session",
        "to_peer": target_peer,
        "note": note[:500],
    })
    print(f"arq-git: ✓ handoff act emitted · branch={branch} · target={target_peer}")
    return 0


# ─── main ────────────────────────────────────────────────────────────────


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    sub = parser.add_subparsers(dest="verb", required=True)

    p_branch = sub.add_parser("branch", help="Create branch from fresh origin/main")
    p_branch.add_argument("name")
    p_branch.set_defaults(func=cmd_branch)

    p_status = sub.add_parser("status", help="git status + substrate-attest")
    p_status.set_defaults(func=cmd_status)

    p_diff = sub.add_parser("diff-scope", help="Diff main..HEAD with scope assertion")
    p_diff.add_argument("--expect-max", type=int, default=None,
                        help="Fail if files_changed exceeds this count")
    p_diff.set_defaults(func=cmd_diff_scope)

    p_commit = sub.add_parser("commit", help="git commit with substrate-attest")
    p_commit.add_argument("-m", "--message", required=True)
    p_commit.add_argument("--no-verify", action="store_true",
                          help="Ignored (per no-direct-shell apex doctrine)")
    p_commit.set_defaults(func=cmd_commit)

    p_rebase = sub.add_parser("rebase-main", help="Rebase onto fresh origin/main with conflict-attest")
    p_rebase.set_defaults(func=cmd_rebase_main)

    p_clean = sub.add_parser("clean-scope", help="Reset unrelated files to origin/main")
    p_clean.add_argument("files", nargs="+")
    p_clean.set_defaults(func=cmd_clean_scope)

    p_handoff = sub.add_parser("handoff", help="Emit handoff act to another peer")
    p_handoff.add_argument("target_peer", help="arq:// peer address")
    p_handoff.add_argument("--note", default="")
    p_handoff.set_defaults(func=cmd_handoff)

    args = parser.parse_args()
    return args.func(args)


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