"""utils/review_thread_guard.py — Gemini review thread 임의 resolve 차단 + audit.

task-2472 구현 1: resolveReviewThread 호출 경로 통제.
medium/high/critical severity thread는 chairman approval evidence 없으면 resolve 불가.
fail-closed: 거부 시 GraphQL mutation 절대 호출 안 함. audit 누락 시도 시 fail.
"""
from __future__ import annotations

import hashlib
import json
import os
import subprocess
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

# --------------------------------------------------------------------------
# 상수
# --------------------------------------------------------------------------

# chairman approval 필요한 severity
APPROVAL_REQUIRED_SEVERITIES = frozenset({"medium", "high", "critical"})

# audit jsonl 경로 (workspace root 상대)
AUDIT_JSONL_REL = Path("memory/orchestration-audit/review-thread-resolution.jsonl")

# GraphQL mutation: resolveReviewThread
_RESOLVE_MUTATION = """
mutation ResolveThread($threadId: ID!) {
  resolveReviewThread(input: {threadId: $threadId}) {
    thread {
      id
      isResolved
    }
  }
}
"""

# --------------------------------------------------------------------------
# 내부 헬퍼
# --------------------------------------------------------------------------


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


def _evidence_hash_str(record: dict) -> str:
    """sha256(json.dumps(record, sort_keys=True)) 반환."""
    serialized = json.dumps(record, sort_keys=True, ensure_ascii=False)
    return hashlib.sha256(serialized.encode("utf-8")).hexdigest()


def _workspace_root(workspace: Optional[Path] = None) -> Path:
    return workspace or Path(os.environ.get("WORKSPACE_ROOT", "/home/jay/workspace"))


def _validate_approval_evidence(evidence: object) -> tuple[bool, str]:
    """approval_evidence 형식 검증.

    필수 키: approved_by, evidence_path, ts
    Returns (ok, reason).
    """
    if not isinstance(evidence, dict):
        return False, "approval_evidence must be a dict"
    required = {"approved_by", "evidence_path", "ts"}
    missing = required - set(evidence.keys())
    if missing:
        return False, f"approval_evidence 누락 필드: {sorted(missing)}"
    if not evidence.get("approved_by", "").strip():
        return False, "approval_evidence.approved_by 빈 문자열"
    if not evidence.get("evidence_path", "").strip():
        return False, "approval_evidence.evidence_path 빈 문자열"
    if not evidence.get("ts", "").strip():
        return False, "approval_evidence.ts 빈 문자열"
    return True, "ok"


# --------------------------------------------------------------------------
# 공개 API
# --------------------------------------------------------------------------


def can_resolve_thread(
    thread_id: str,
    severity: str,
    *,
    actor: str,
    approval_evidence: Optional[dict] = None,
) -> dict:
    """review thread resolve 가능 여부 판정.

    Parameters
    ----------
    thread_id:
        GitHub review thread ID.
    severity:
        "low" / "medium" / "high" / "critical"
    actor:
        요청자 식별자 (bot name, user login 등).
    approval_evidence:
        chairman approval evidence dict.
        형식: {"approved_by": str, "evidence_path": str, "ts": str}

    Returns
    -------
    dict
        {"ok": bool, "reason": str, "detail": {...}}

    규칙:
    - severity in ("medium", "high", "critical") → approval_evidence 필수.
      없거나 형식 부적합 → ok=False.
    - severity == "low" → evidence 없어도 OK (audit은 별도로 필수).
    """
    severity = (severity or "").lower().strip()
    valid_severities = {"low", "medium", "high", "critical"}
    if severity not in valid_severities:
        return {
            "ok": False,
            "reason": f"알 수 없는 severity: '{severity}'. 허용: {valid_severities}",
            "detail": {"thread_id": thread_id, "severity": severity, "actor": actor},
        }

    if severity in APPROVAL_REQUIRED_SEVERITIES:
        if approval_evidence is None:
            return {
                "ok": False,
                "reason": (
                    f"severity='{severity}' 스레드 resolve는 chairman approval_evidence 필수. "
                    "evidence 없이 resolve 시도 → fail-closed"
                ),
                "detail": {
                    "thread_id": thread_id,
                    "severity": severity,
                    "actor": actor,
                    "approval_evidence": None,
                },
            }
        ev_ok, ev_reason = _validate_approval_evidence(approval_evidence)
        if not ev_ok:
            return {
                "ok": False,
                "reason": f"approval_evidence 형식 불량: {ev_reason}",
                "detail": {
                    "thread_id": thread_id,
                    "severity": severity,
                    "actor": actor,
                    "approval_evidence": approval_evidence,
                },
            }

    return {
        "ok": True,
        "reason": "resolve 허용",
        "detail": {
            "thread_id": thread_id,
            "severity": severity,
            "actor": actor,
            "approval_evidence": approval_evidence,
        },
    }


def record_resolution_audit(
    *,
    task_id: str,
    pr_number: int,
    thread_id: str,
    severity: str,
    actor: str,
    approval_evidence: Optional[dict],
    result: str,
    reason: str,
    workspace: Optional[Path] = None,
) -> Path:
    """resolve 시도 audit 기록 (JSONL atomic append).

    memory/orchestration-audit/review-thread-resolution.jsonl 에 line append.
    필수 필드 10개: task_id, pr_number, thread_id, severity, actor,
    approval_evidence, result, reason, timestamp, evidence_hash
    """
    timestamp = _now_iso()
    # evidence_hash 계산 전 record (evidence_hash 제외)
    base_record: dict = {
        "task_id": task_id,
        "pr_number": pr_number,
        "thread_id": thread_id,
        "severity": severity,
        "actor": actor,
        "approval_evidence": approval_evidence,
        "result": result,
        "reason": reason,
        "timestamp": timestamp,
    }
    # evidence_hash = sha256(base_record)
    ev_hash = _evidence_hash_str(base_record)
    record = {**base_record, "evidence_hash": ev_hash}

    work_root = _workspace_root(workspace)
    target = work_root / AUDIT_JSONL_REL
    target.parent.mkdir(parents=True, exist_ok=True)

    line = json.dumps(record, ensure_ascii=False) + "\n"
    fd = os.open(str(target), os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o644)
    try:
        os.write(fd, line.encode("utf-8"))
    finally:
        os.close(fd)

    return target


def resolve_thread_via_graphql(
    thread_id: str,
    severity: str,
    *,
    task_id: str,
    pr_number: int,
    actor: str,
    approval_evidence: Optional[dict] = None,
    repo: Optional[str] = None,
    dry_run: bool = False,
    workspace: Optional[Path] = None,
) -> dict:
    # Gemini 리뷰 medium: GH_REPO env 우선 (taskctl와 일관). 미지정 시 default.
    if repo is None:
        repo = os.environ.get("GH_REPO", "Jeon-Jonghyuk/dev_workspace")
    """can_resolve_thread() 통과 시에만 GraphQL resolveReviewThread mutation 호출.

    fail-closed:
    - 거부 시 mutation 절대 호출 안 함.
    - mutation 호출 후에도 audit 누락 시 fail.
    - 모든 경로에서 audit 기록 필수.

    Returns
    -------
    dict
        {"ok": bool, "reason": str, "detail": {...}}
    """
    # 1. 허용 여부 판정
    gate = can_resolve_thread(
        thread_id, severity, actor=actor, approval_evidence=approval_evidence
    )

    if not gate["ok"]:
        # 거부 → audit 기록 후 반환
        audit_path = record_resolution_audit(
            task_id=task_id,
            pr_number=pr_number,
            thread_id=thread_id,
            severity=severity,
            actor=actor,
            approval_evidence=approval_evidence,
            result="rejected",
            reason=gate["reason"],
            workspace=workspace,
        )
        return {
            "ok": False,
            "reason": gate["reason"],
            "detail": {**gate["detail"], "audit_path": str(audit_path)},
        }

    # 2. dry_run 모드: mutation 실제 호출 안 함
    if dry_run:
        audit_path = record_resolution_audit(
            task_id=task_id,
            pr_number=pr_number,
            thread_id=thread_id,
            severity=severity,
            actor=actor,
            approval_evidence=approval_evidence,
            result="allowed",
            reason="dry_run=True, mutation 호출 생략",
            workspace=workspace,
        )
        return {
            "ok": True,
            "reason": "dry_run: mutation 호출 생략",
            "detail": {"thread_id": thread_id, "audit_path": str(audit_path)},
        }

    # 3. GraphQL mutation 호출
    # 주의: `gh api graphql`은 --repo 플래그를 받지 않음 (graphql endpoint는 repo-agnostic).
    # threadId 자체에 PR/repo 정보가 인코딩되어 있음.
    # repo 인자는 audit logging 용도로만 사용 (mutation 호출에는 불필요).
    _ = repo  # audit context 보존
    try:
        proc = subprocess.run(
            [
                "gh", "api", "graphql",
                "-f", f"query={_RESOLVE_MUTATION}",
                "-F", f"threadId={thread_id}",
            ],
            capture_output=True,
            text=True,
            timeout=30,
            shell=False,
            check=False,
        )
        mutation_ok = proc.returncode == 0
        mutation_out = proc.stdout.strip()
        mutation_err = proc.stderr.strip()
    except Exception as exc:
        mutation_ok = False
        mutation_out = ""
        mutation_err = str(exc)

    if mutation_ok:
        mut_result = "allowed"
        mut_reason = "resolveReviewThread mutation 성공"
    else:
        mut_result = "rejected"
        mut_reason = f"resolveReviewThread mutation 실패: {mutation_err}"

    # 4. audit 기록 (mutation 성공/실패 모두)
    audit_path = record_resolution_audit(
        task_id=task_id,
        pr_number=pr_number,
        thread_id=thread_id,
        severity=severity,
        actor=actor,
        approval_evidence=approval_evidence,
        result=mut_result,
        reason=mut_reason,
        workspace=workspace,
    )

    return {
        "ok": mutation_ok,
        "reason": mut_reason,
        "detail": {
            "thread_id": thread_id,
            "mutation_output": mutation_out,
            "audit_path": str(audit_path),
        },
    }
