"""GitHub merge_queue 진입 클라이언트. admin override / force merge / bypass 차단."""
from __future__ import annotations

import json
import os
import subprocess
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

WORKSPACE = Path(os.environ.get("WORKSPACE_ROOT", "/home/jay/workspace"))
MERGE_QUEUE_LOG = WORKSPACE / "memory" / "orchestration-audit" / "merge-queue.jsonl"

ADMIN_FLAG_KEYWORDS: tuple[str, ...] = ("--admin", "admin override", "force-merge", "bypass")


class MergeQueueError(Exception):
    """merge queue 진입 실패 또는 admin override 차단 시 raise."""


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


def _default_runner(cmd: list[str], **kwargs: Any) -> subprocess.CompletedProcess:
    """기본 subprocess.run 래퍼."""
    return subprocess.run(cmd, capture_output=True, text=True, **kwargs)


def detect_admin_override_attempt(args: list[str] | None) -> str | None:
    """args에 admin override 흔적이 있으면 사유 string 반환, 없으면 None.

    ADMIN_FLAG_KEYWORDS 중 하나라도 args 내 문자열에 포함되면 차단.
    """
    if not args:
        return None
    for arg in args:
        arg_lower = arg.lower()
        for kw in ADMIN_FLAG_KEYWORDS:
            if kw.lower() in arg_lower:
                return f"admin override 키워드 감지: '{arg}' (keyword='{kw}')"
    return None


def enqueue_pr(
    pr_number: int,
    *,
    bot_token: str | None = None,
    no_admin_override: bool = True,
    dry_run: bool = False,
    runner=None,
) -> dict[str, Any]:
    """PR을 merge queue에 진입시킨다. admin override / force merge 절대 금지.

    1) no_admin_override=True 검증 (False면 즉시 MergeQueueError raise)
    2) gh pr view로 PR state, mergeable, mergeStateStatus, author, latestReviews 조회
    3) PR author=bot, latestReviews에 사람 approved 1건 이상 검증
    4) gh pr merge --merge --delete-branch로 merge_queue 진입
       - --admin 플래그 절대 사용 금지
    5) audit jsonl append
    6) 반환: {pr_number, action, outcome, audit_entry}
    """
    if runner is None:
        runner = _default_runner

    # 1) no_admin_override 검증 — fail-closed
    if not no_admin_override:
        raise MergeQueueError(
            "enqueue_pr: no_admin_override=False 차단 — admin override는 허용되지 않습니다."
        )

    # 테스트 hook: TASKCTL_PR_REVIEWS_OVERRIDE / TASKCTL_PR_AUTHOR_OVERRIDE
    # ★ 보안: TASKCTL_TEST_MODE=1 일 때만 활성. 프로덕션에서는 무조건 GitHub API 실조회 (fail-closed).
    _test_mode = os.environ.get("TASKCTL_TEST_MODE") == "1"
    reviews_override_raw = os.environ.get("TASKCTL_PR_REVIEWS_OVERRIDE") if _test_mode else None
    author_override = os.environ.get("TASKCTL_PR_AUTHOR_OVERRIDE") if _test_mode else None

    # 2) PR 정보 조회
    env_with_token: dict[str, str] = {}
    if bot_token:
        env_with_token["GH_TOKEN"] = bot_token

    if author_override:
        pr_author = author_override
        pr_state = "OPEN"
        mergeable = "MERGEABLE"
        merge_state_status = "CLEAN"
        head_sha = ""
        latest_reviews: list[dict[str, Any]] = []
    else:
        proc = runner(
            [
                "gh", "pr", "view", str(pr_number),
                "--json", "state,mergeable,mergeStateStatus,author,latestReviews,headRefOid",
            ],
            timeout=30,
            env={**os.environ, **env_with_token},
        )
        if proc.returncode != 0:
            raise MergeQueueError(
                f"gh pr view 실패 (PR #{pr_number}): {proc.stderr[-300:].strip()}"
            )
        try:
            pr_data = json.loads(proc.stdout or "{}")
        except json.JSONDecodeError as exc:
            raise MergeQueueError(f"gh pr view JSON 파싱 실패: {exc}") from exc

        pr_author = (pr_data.get("author") or {}).get("login", "")
        pr_state = pr_data.get("state", "")
        mergeable = pr_data.get("mergeable", "")
        merge_state_status = pr_data.get("mergeStateStatus", "")
        head_sha = pr_data.get("headRefOid", "")
        latest_reviews = pr_data.get("latestReviews") or []

    # reviews override 적용
    if reviews_override_raw:
        try:
            latest_reviews = json.loads(reviews_override_raw)
        except Exception:
            pass

    # 3) PR author=bot 검증
    from utils.bot_pr_author import is_bot_author, assert_human_actor  # type: ignore[import-not-found]  # noqa: PLC0415

    if not is_bot_author(pr_author):
        _append_audit(
            pr_number=pr_number, author=pr_author, reviewer="",
            action="enqueue", outcome="BLOCKED_NOT_BOT_AUTHOR",
            head_sha=head_sha, dry_run=dry_run,
        )
        raise MergeQueueError(
            f"enqueue_pr: PR author '{pr_author}'은 bot이 아닙니다. "
            f"bot author PR만 merge queue 진입 가능."
        )

    # latestReviews에서 사람 APPROVED + allowed_approvers.json 재검증
    from utils.bot_pr_author import (  # type: ignore[import-not-found]  # noqa: PLC0415
        load_allowed_human_approvers,
    )
    allowed_humans = load_allowed_human_approvers()
    human_approved_reviews = []
    for review in latest_reviews:
        reviewer_login = (review.get("author") or {}).get("login", "")
        state = review.get("state", "")
        if state.upper() == "APPROVED":
            try:
                assert_human_actor(reviewer_login)
                # ★ allowed_approvers.json에 있는지 재검증 (fail-closed)
                if allowed_humans and reviewer_login not in allowed_humans:
                    continue  # 허용되지 않은 사람은 APPROVED 인정 안 됨
                human_approved_reviews.append(reviewer_login)
            except Exception:
                pass  # bot reviewer는 무시

    if not human_approved_reviews:
        _append_audit(
            pr_number=pr_number, author=pr_author, reviewer="",
            action="enqueue", outcome="BLOCKED_NO_HUMAN_APPROVAL",
            head_sha=head_sha, dry_run=dry_run,
        )
        raise MergeQueueError(
            f"enqueue_pr: PR #{pr_number}에 사람 reviewer의 APPROVED가 없습니다. "
            f"최소 1명의 사람 승인 필요."
        )

    reviewer = human_approved_reviews[0]

    # PR state / mergeable 검증 — fail-closed
    if pr_state and pr_state.upper() not in {"OPEN"}:
        _append_audit(
            pr_number=pr_number, author=pr_author, reviewer=reviewer,
            action="enqueue", outcome=f"BLOCKED_PR_STATE_{pr_state}",
            head_sha=head_sha, dry_run=dry_run,
            mergeable=mergeable, merge_state_status=merge_state_status,
        )
        raise MergeQueueError(
            f"enqueue_pr: PR #{pr_number} state='{pr_state}' (OPEN 필요)"
        )
    if mergeable and mergeable.upper() not in {"MERGEABLE", "UNKNOWN"}:
        _append_audit(
            pr_number=pr_number, author=pr_author, reviewer=reviewer,
            action="enqueue", outcome=f"BLOCKED_MERGEABLE_{mergeable}",
            head_sha=head_sha, dry_run=dry_run,
            mergeable=mergeable, merge_state_status=merge_state_status,
        )
        raise MergeQueueError(
            f"enqueue_pr: PR #{pr_number} mergeable='{mergeable}' (MERGEABLE 필요)"
        )

    if dry_run:
        _append_audit(
            pr_number=pr_number, author=pr_author, reviewer=reviewer,
            action="enqueue", outcome="DRY_RUN",
            head_sha=head_sha, dry_run=True,
            mergeable=mergeable, merge_state_status=merge_state_status,
        )
        audit_entry = {
            "ts": _now(), "pr_number": pr_number, "author": pr_author,
            "reviewer": reviewer, "action": "enqueue", "outcome": "DRY_RUN",
            "head_sha": head_sha, "dry_run": True,
            "mergeable": mergeable, "merge_state_status": merge_state_status,
        }
        return {
            "pr_number": pr_number,
            "action": "enqueue",
            "outcome": "DRY_RUN",
            "audit_entry": audit_entry,
        }

    # 4) gh pr merge --merge --auto --delete-branch (--admin 절대 금지)
    # ★ --auto: GitHub merge_queue가 활성화된 저장소에서는 queue 진입 / required checks 통과 후 자동 머지
    #          비활성 저장소에서는 즉시 머지 (fail-fast). admin override 절대 사용 안 함.
    merge_cmd = [
        "gh", "pr", "merge", str(pr_number),
        "--merge", "--auto", "--delete-branch",
    ]
    merge_proc = runner(
        merge_cmd,
        timeout=120,
        env={**os.environ, **env_with_token},
    )

    if merge_proc.returncode != 0:
        outcome = "FAILED"
        _append_audit(
            pr_number=pr_number, author=pr_author, reviewer=reviewer,
            action="enqueue", outcome=outcome,
            head_sha=head_sha, dry_run=False,
            mergeable=mergeable, merge_state_status=merge_state_status,
        )
        raise MergeQueueError(
            f"gh pr merge 실패 (PR #{pr_number}): {merge_proc.stderr[-300:].strip()}"
        )

    outcome = "ENQUEUED"

    # 5) audit jsonl append
    _append_audit(
        pr_number=pr_number, author=pr_author, reviewer=reviewer,
        action="enqueue", outcome=outcome,
        head_sha=head_sha, dry_run=False,
        mergeable=mergeable, merge_state_status=merge_state_status,
    )

    audit_entry = {
        "ts": _now(), "pr_number": pr_number, "author": pr_author,
        "reviewer": reviewer, "action": "enqueue", "outcome": outcome,
        "head_sha": head_sha,
        "mergeable": mergeable, "merge_state_status": merge_state_status,
    }

    # 6) 반환
    return {
        "pr_number": pr_number,
        "action": "enqueue",
        "outcome": outcome,
        "audit_entry": audit_entry,
    }


def _append_audit(
    *,
    pr_number: int,
    author: str,
    reviewer: str,
    action: str,
    outcome: str,
    head_sha: str,
    dry_run: bool,
    mergeable: str = "",
    merge_state_status: str = "",
) -> None:
    """audit jsonl에 merge queue 이벤트 기록."""
    MERGE_QUEUE_LOG.parent.mkdir(parents=True, exist_ok=True)
    record = {
        "ts": _now(),
        "pr_number": pr_number,
        "author": author,
        "reviewer": reviewer,
        "action": action,
        "outcome": outcome,
        "head_sha": head_sha,
        "dry_run": dry_run,
        "mergeable": mergeable,
        "merge_state_status": merge_state_status,
    }
    with MERGE_QUEUE_LOG.open("a", encoding="utf-8") as f:
        f.write(json.dumps(record, ensure_ascii=False) + "\n")
