"""anu_v2.ci_gemini_watcher_gh_adapter — task-2719, GET-only gh_runner 어댑터.

설계 원칙:
  - runner (ci_gemini_watcher_runner.py) 는 무수정 import-only.
  - GitHub write 0: 실제 comment, merge, post_review 등 write op 절대 금지.
  - decision-only, dry_run 항상 True 고정.
  - one-way isolation: anu_v2/* 와 표준 라이브러리(json, subprocess) 만 import.
    utils / dispatch / scripts / dashboard 의존성 0.
"""

from __future__ import annotations

import json
import subprocess
from typing import Any, Callable, Final, Mapping, Sequence

# ──────────────────────────────────────────────────────────────────────────────
# 공개 상수
# ──────────────────────────────────────────────────────────────────────────────

READ_OPS: Final[frozenset[str]] = frozenset(
    {"actual_head", "diff_paths", "ci_rollup", "reviews", "findings"}
)

# ──────────────────────────────────────────────────────────────────────────────
# 예외
# ──────────────────────────────────────────────────────────────────────────────

class GhWriteForbiddenError(RuntimeError):
    """write op 또는 비허용 메서드 요청 시 raise."""


# ──────────────────────────────────────────────────────────────────────────────
# 내부 방어선: argv 토큰 스캔
# ──────────────────────────────────────────────────────────────────────────────

# write 서브커맨드 집합 (gh pr / gh issue / gh api 등)
_WRITE_SUBCMDS: Final[frozenset[str]] = frozenset(
    {
        "comment", "merge", "close", "edit", "review",
        "ready", "create", "delete", "lock", "unlock", "reopen",
    }
)


_WRITE_INDUCING_FLAGS: Final[frozenset[str]] = frozenset(
    {"-f", "-F", "--field", "--raw-field", "--input"}
)


def _assert_readonly_argv(argv: Sequence[str]) -> None:
    """GET-only 보장 — argv **전체**에서 다음을 검사해 GhWriteForbiddenError raise.

    (1) write 서브커맨드 (전역 플래그로 서브커맨드 위치가 밀려도 탐지)
    (2) 비-GET 메서드 — ``-X``/``--method`` 분리·``--method=POST`` 등호·``-XPOST`` 붙여쓰기
    (3) 암묵적 POST 유도 데이터 플래그 — ``-f``/``-F``/``--field``/``--raw-field``/``--input``
        (``gh api`` 는 ``-X`` 없이도 데이터 플래그가 있으면 POST 로 승격)

    defense-in-depth 방어선 — gh_cli 호출 직전 실행.
    """
    argv_list = list(argv)

    # (1) write 서브커맨드 — 전역 플래그 우회 방지 위해 argv 전체 검사.
    for token in argv_list:
        if str(token).lower() in _WRITE_SUBCMDS:
            raise GhWriteForbiddenError(
                f"write subcommand forbidden in argv: {token!r} (argv={argv_list!r})"
            )

    # (2) 비-GET 메서드 — 분리/등호/붙여쓰기 모든 형태.
    for i, token in enumerate(argv_list):
        token_str = str(token)
        if token_str in ("-X", "--method"):
            if i + 1 < len(argv_list) and str(argv_list[i + 1]).upper() != "GET":
                raise GhWriteForbiddenError(
                    f"non-GET method forbidden: {argv_list[i + 1]!r} (argv={argv_list!r})"
                )
        elif token_str.startswith("--method="):
            if token_str.split("=", 1)[1].upper() != "GET":
                raise GhWriteForbiddenError(
                    f"non-GET method forbidden: {token_str!r} (argv={argv_list!r})"
                )
        elif token_str.startswith("-X") and len(token_str) > 2:
            if token_str[2:].upper() != "GET":
                raise GhWriteForbiddenError(
                    f"non-GET method forbidden: {token_str!r} (argv={argv_list!r})"
                )

    # (3) 암묵적 POST 유도 데이터 플래그 (gh api 가 -X 없이도 POST 승격).
    for token in argv_list:
        token_str = str(token)
        if token_str in _WRITE_INDUCING_FLAGS or token_str.startswith(
            ("--field=", "--raw-field=", "--input=")
        ):
            raise GhWriteForbiddenError(
                f"write-inducing data flag forbidden: {token_str!r} (argv={argv_list!r})"
            )


# ──────────────────────────────────────────────────────────────────────────────
# JSON 파싱 헬퍼 (fail-closed)
# ──────────────────────────────────────────────────────────────────────────────

def _parse_json(stdout: str) -> Any:
    """stdout 을 JSON 파싱. 빈 문자열/공백이면 None 반환 (예외 금지)."""
    stripped = stdout.strip() if stdout else ""
    if not stripped:
        return None
    try:
        return json.loads(stripped)
    except (json.JSONDecodeError, ValueError):
        return None


# ──────────────────────────────────────────────────────────────────────────────
# ci_rollup state 도출
# ──────────────────────────────────────────────────────────────────────────────

_FAILURE_CONCLUSIONS: Final[frozenset[str]] = frozenset(
    {"FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE"}
)


def _derive_ci_state(rollup: Any) -> dict:
    """statusCheckRollup 리스트에서 CI state 도출.

    반환: {"state": "SUCCESS"|"FAILURE"|"PENDING"|"UNKNOWN", "remediable": False}
    """
    if not rollup:
        return {"state": "UNKNOWN", "remediable": False}

    if not isinstance(rollup, list):
        return {"state": "UNKNOWN", "remediable": False}

    for item in rollup:
        if not isinstance(item, dict):
            continue
        conclusion = str(item.get("conclusion") or "").upper()
        if conclusion in _FAILURE_CONCLUSIONS:
            # 실패 결론 발견 → FAILURE (보수적; remediable=False 고정)
            return {"state": "FAILURE", "remediable": False}

    for item in rollup:
        if not isinstance(item, dict):
            continue
        status = str(item.get("status") or "").upper()
        if status and status != "COMPLETED":
            # 완료되지 않은 항목 존재 → PENDING
            return {"state": "PENDING", "remediable": False}

    # 전부 완료 + 실패 없음 → SUCCESS
    return {"state": "SUCCESS", "remediable": False}


# ──────────────────────────────────────────────────────────────────────────────
# build_readonly_gh_runner
# ──────────────────────────────────────────────────────────────────────────────

def build_readonly_gh_runner(
    *,
    gh_cli: Callable[[Sequence[str]], str],
    owner: str = "",
    repo: str = "",
) -> Callable[..., Any]:
    """GET-only gh_runner closure 반환.

    Args:
      gh_cli : `gh <argv...>` 실행 후 stdout(JSON 텍스트) 반환하는 callable.
      owner  : GitHub owner (빈 문자열이면 --repo 플래그 생략).
      repo   : GitHub repo  (빈 문자열이면 --repo 플래그 생략).

    Returns:
      op 5종(actual_head / diff_paths / ci_rollup / reviews / findings)을
      dispatch 하는 gh_runner callable.

    Raises:
      GhWriteForbiddenError: 알 수 없는 op 또는 write op 요청 시.
    """

    def _repo_flags() -> list[str]:
        """owner/repo 둘 다 있을 때만 --repo 플래그 생성."""
        if owner and repo:
            return ["--repo", f"{owner}/{repo}"]
        return []

    def _pr_base_argv(pr: int) -> list[str]:
        """gh pr view <pr> [--repo owner/repo] 공통 argv 앞부분."""
        return ["pr", "view", str(pr)] + _repo_flags()

    # ── op 구현들 ─────────────────────────────────────────────────────────────

    def _actual_head(pr_number: int) -> str:
        """PR HEAD SHA 반환. 실패 시 "" (fail-closed)."""
        argv = _pr_base_argv(pr_number) + ["--json", "headRefOid"]
        _assert_readonly_argv(argv)
        stdout = gh_cli(argv)
        data = _parse_json(stdout)
        if not isinstance(data, dict):
            return ""
        return str(data.get("headRefOid") or "")

    def _diff_paths(pr_number: int) -> list[str]:
        """PR diff 파일 경로 list 반환. 실패 시 []."""
        argv = _pr_base_argv(pr_number) + ["--json", "files"]
        _assert_readonly_argv(argv)
        stdout = gh_cli(argv)
        data = _parse_json(stdout)
        if not isinstance(data, dict):
            return []
        files = data.get("files")
        if not isinstance(files, list):
            return []
        result: list[str] = []
        for f in files:
            if isinstance(f, dict):
                path = f.get("path")
                if path is not None:
                    result.append(str(path))
        return result

    def _ci_rollup(pr_number: int) -> dict:
        """CI rollup state dict 반환."""
        argv = _pr_base_argv(pr_number) + ["--json", "statusCheckRollup"]
        _assert_readonly_argv(argv)
        stdout = gh_cli(argv)
        data = _parse_json(stdout)
        if not isinstance(data, dict):
            return {"state": "UNKNOWN", "remediable": False}
        rollup = data.get("statusCheckRollup")
        return _derive_ci_state(rollup)

    def _reviews(method: str, path: str) -> list:
        """GET-only review 목록 반환. method != GET 이면 GhWriteForbiddenError."""
        if str(method).upper() != "GET":
            raise GhWriteForbiddenError(
                f"reviews op: non-GET method forbidden: {method!r}"
            )
        argv = ["api", "-X", "GET", path]
        _assert_readonly_argv(argv)
        stdout = gh_cli(argv)
        data = _parse_json(stdout)
        if isinstance(data, list):
            return data
        return []

    def _findings(pr_number: int) -> list:
        """PR 코멘트(findings) 목록 반환."""
        _owner = owner or "OWNER"
        _repo  = repo  or "REPO"
        path = f"/repos/{_owner}/{_repo}/pulls/{pr_number}/comments"
        argv = ["api", "-X", "GET", path]
        _assert_readonly_argv(argv)
        stdout = gh_cli(argv)
        data = _parse_json(stdout)
        if isinstance(data, list):
            return data
        return []

    # ── dispatch 테이블 ───────────────────────────────────────────────────────

    def _gh_runner(op: str, /, **kwargs: Any) -> Any:
        """op 에 따라 5종 GET-only 동작을 dispatch.

        알 수 없는 op (write op 포함) 은 GhWriteForbiddenError raise.
        """
        if op == "actual_head":
            return _actual_head(kwargs["pr_number"])
        if op == "diff_paths":
            return _diff_paths(kwargs["pr_number"])
        if op == "ci_rollup":
            return _ci_rollup(kwargs["pr_number"])
        if op == "reviews":
            return _reviews(kwargs.get("method", "GET"), kwargs.get("path", ""))
        if op == "findings":
            return _findings(kwargs["pr_number"])
        # 위 5종 외 — write/unknown op hard guard
        raise GhWriteForbiddenError(f"write/unknown op forbidden: {op!r}")

    return _gh_runner


# ──────────────────────────────────────────────────────────────────────────────
# default_gh_cli — read-only one-shot dry-run helper
# ──────────────────────────────────────────────────────────────────────────────

def default_gh_cli(argv: Sequence[str]) -> str:
    """실 gh CLI 를 subprocess 로 GET-only 호출하는 read-only one-shot dry-run helper.

    - _assert_readonly_argv 검사 통과 후 gh 프로세스 실행.
    - returncode != 0 이면 "" 반환 (fail-closed).
    - 주의: 이 함수는 단발(one-shot) 호출 전용입니다.
      자동 반복/polling/스케줄 실행 목적으로 사용하지 마세요.
    """
    _assert_readonly_argv(argv)
    result = subprocess.run(
        ["gh", *map(str, argv)],
        capture_output=True,
        text=True,
        timeout=30,
        check=False,
    )
    if result.returncode != 0:
        return ""
    return result.stdout


# ──────────────────────────────────────────────────────────────────────────────
# run_one_shot_dry_run
# ──────────────────────────────────────────────────────────────────────────────

def run_one_shot_dry_run(
    pr_number: int,
    expected_head: str,
    expected_files: Sequence[str],
    *,
    gh_cli: Callable[[Sequence[str]], str] | None = None,
    owner: str = "",
    repo: str = "",
    task_id: str = "task-2719",
    owner_proof: Mapping[str, Any] | None = None,
    dedupe_checker: Any = None,
    record_trigger: Any = None,
    triage: Any = None,
) -> Any:
    """GET-only, dry_run=True 고정 단발 실행 진입점.

    gh_cli 가 None 이면 default_gh_cli 사용.
    dry_run 은 항상 True 고정 — 인자로 노출하지 않음 (실 write 0 보장).

    Returns:
      WatchResult (ci_gemini_watcher_runner.WatchResult).
    """
    from anu_v2.ci_gemini_watcher_runner import run_watch_cycle  # runner 무수정 import

    runner_cb = build_readonly_gh_runner(
        gh_cli=gh_cli if gh_cli is not None else default_gh_cli,
        owner=owner,
        repo=repo,
    )
    return run_watch_cycle(
        pr_number=pr_number,
        expected_head=expected_head,
        expected_files=expected_files,
        gh_runner=runner_cb,
        owner=owner,
        repo=repo,
        task_id=task_id,
        owner_proof=owner_proof,
        dedupe_checker=dedupe_checker,
        record_trigger=record_trigger,
        triage=triage,
        dry_run=True,  # 항상 True — 절대 변경 금지
    )


# ──────────────────────────────────────────────────────────────────────────────
# __all__
# ──────────────────────────────────────────────────────────────────────────────

__all__ = [
    "GhWriteForbiddenError",
    "READ_OPS",
    "build_readonly_gh_runner",
    "run_one_shot_dry_run",
    "default_gh_cli",
]
