#!/usr/bin/env python3
"""gemini_feedback_loop.py — Gemini failure 시 자동 수정 루프 강제.

흐름:
  1) gemini_review_gate.py 결과 (`failure`) 후 호출
  2) 카운트 파일 `memory/events/<task-id>.gemini-loop-count` 증가
  3) 안전한 자동 수정 적용 (linter/typo 한정)
       - black/ruff/isort 등이 있으면 차례로 실행 (있는 것만)
  4) 변경 있으면 새 commit + push (force-with-lease 금지)
  5) 카운트 > MAX_LOOPS 시:
       - PR에 `human_review_required` 라벨 추가 (gh api POST issues/<num>/labels)
       - PR comment "@회장 리뷰 필요"
       - exit_code=2 (queue 등록 차단 시그널)

CLI:
    python3 scripts/gemini_feedback_loop.py \
        --task-id task-XXXX --pr-number 123 --repo OWNER/REPO [--max-loops 3] [--dry-run]
"""

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

WORKSPACE = Path(os.environ.get("WORKSPACE", str(Path(__file__).resolve().parent.parent)))
EVENTS_DIR = WORKSPACE / "memory" / "events"
HUMAN_LABEL = "human_review_required"
MAX_LOOPS_DEFAULT = 3


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


def _read_count(task_id: str) -> int:
    p = EVENTS_DIR / f"{task_id}.gemini-loop-count"
    if not p.exists():
        return 0
    try:
        return int(p.read_text().strip())
    except ValueError:
        return 0


def _write_count(task_id: str, value: int) -> None:
    EVENTS_DIR.mkdir(parents=True, exist_ok=True)
    (EVENTS_DIR / f"{task_id}.gemini-loop-count").write_text(str(value))


def _run(cmd: list[str], cwd: Path | None = None, timeout: int = 60) -> tuple[int, str, str]:
    proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, cwd=str(cwd) if cwd else None)
    return proc.returncode, proc.stdout, proc.stderr


def apply_auto_fixes(workspace: Path) -> dict:
    """Run available auto-formatters in workspace. Returns summary."""
    summary: dict = {"ran": [], "skipped": []}
    candidates = [
        (["ruff", "--version"], ["ruff", "check", "--fix", str(workspace)]),
        (["black", "--version"], ["black", str(workspace)]),
        (["isort", "--version"], ["isort", str(workspace)]),
    ]
    for probe, action in candidates:
        if shutil.which(probe[0]) is None:
            summary["skipped"].append(probe[0])
            continue
        rc, out, err = _run(action, cwd=workspace, timeout=120)
        summary["ran"].append({"tool": probe[0], "rc": rc, "stdout_tail": out[-200:], "stderr_tail": err[-200:]})
    return summary


def push_changes(workspace: Path, message: str) -> dict:
    _, status_out, _ = _run(["git", "-C", str(workspace), "status", "--porcelain"])
    if not status_out.strip():
        return {"changed": False}
    _run(["git", "-C", str(workspace), "add", "-A"])
    rc, _, err = _run(["git", "-C", str(workspace), "commit", "-m", message])
    if rc != 0:
        return {"changed": False, "commit_err": err}
    push_rc, _, push_err = _run(["git", "-C", str(workspace), "push"], timeout=60)
    return {"changed": True, "push_rc": push_rc, "push_err": push_err.strip()[-300:]}


def gh_add_label(repo: str, pr_number: int, label: str) -> dict:
    body = json.dumps({"labels": [label]})
    proc = subprocess.run(
        ["gh", "api", "-X", "POST", f"repos/{repo}/issues/{pr_number}/labels", "--input", "-"],
        capture_output=True, text=True, input=body, timeout=30,
    )
    return {"rc": proc.returncode, "stdout": proc.stdout, "stderr": proc.stderr}


def gh_comment(repo: str, pr_number: int, comment: str) -> dict:
    body = json.dumps({"body": comment})
    proc = subprocess.run(
        ["gh", "api", "-X", "POST", f"repos/{repo}/issues/{pr_number}/comments", "--input", "-"],
        capture_output=True, text=True, input=body, timeout=30,
    )
    return {"rc": proc.returncode, "stdout": proc.stdout, "stderr": proc.stderr}


def loop(args: argparse.Namespace) -> int:
    task_id = args.task_id
    if not task_id:
        print(json.dumps({"error": "--task-id required"}), file=sys.stderr)
        return 2
    count = _read_count(task_id) + 1
    out: dict = {"task_id": task_id, "loop_count": count, "ts": _now_iso(), "max_loops": args.max_loops}

    if count > args.max_loops:
        out["state"] = "exhausted"
        if args.pr_number and not args.dry_run:
            out["label"] = gh_add_label(args.repo, args.pr_number, HUMAN_LABEL)
            out["comment"] = gh_comment(args.repo, args.pr_number, "@회장 자동 수정 루프 한계 초과 — 직접 리뷰 부탁드립니다.")
        print(json.dumps(out, ensure_ascii=False))
        _write_count(task_id, count)
        return 2

    if not args.dry_run:
        _write_count(task_id, count)

    fix_summary = apply_auto_fixes(WORKSPACE) if not args.dry_run else {"ran": [], "skipped": ["dry-run"]}
    out["fixes"] = fix_summary

    if args.dry_run:
        out["state"] = "dry-run"
        print(json.dumps(out, ensure_ascii=False))
        return 0

    push_result = push_changes(WORKSPACE, f"[{task_id}] gemini auto-fix loop {count}")
    out["push"] = push_result
    out["state"] = "pushed" if push_result.get("changed") else "no-changes"
    print(json.dumps(out, ensure_ascii=False))
    return 0


def main() -> int:
    ap = argparse.ArgumentParser(description="Gemini failure feedback auto-fix loop")
    ap.add_argument("--task-id", required=True)
    ap.add_argument("--pr-number", type=int, default=0)
    ap.add_argument("--repo", default=os.environ.get("GH_REPO", "JonghyukJeon/dev_workspace"))
    ap.add_argument("--max-loops", type=int, default=MAX_LOOPS_DEFAULT)
    ap.add_argument("--dry-run", action="store_true")
    args = ap.parse_args()
    return loop(args)


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