"""utils/bot_merge_identity.py — task-2522 §본질.

회장 §명시 본질:
  task-2522의 목표는 "머지가 되게 하는 것"이 **아니다.** 이미 머지는 된다.
  목표는 **"누가 머지했는가"**를 자동화 기준에 맞게 고치는 것.
  owner_pat로 머지되면 기능적으로는 성공이어도 **autonomy success로 인정하지 않는다.**

본 모듈은 4가지 책임을 박제한다:
  1. **token_source 4 enum 분류** — env var 존재 여부 + token prefix 휴리스틱.
     절대로 raw token 값을 출력/리턴하지 않는다 (회장 §보안 7번).
  2. **identity audit JSONL** — pre/post merge token_source + mergedBy 비교.
  3. **autonomy_capability_gap 박제** — owner_pat fallback이 1건이라도 감지되면
     기능적 머지 성공 여부와 무관하게 autonomy success로 인정하지 않는다.
  4. **autonomy_score delta 산출** — owner_pat→stuck 또는 하락, bot/app→상승.

회장 §금지 행위:
  - ❌ token raw value 출력 (값/hex/마스킹된 값 X) — env var 존재 여부와 prefix만 사용.
  - ❌ owner_pat을 bot identity로 위장.
  - ❌ admin override 호출.
  - ❌ branch protection 우회.
  - ❌ GitHub App private key 파일 공유/노출.
  - ❌ AUTOMATION_CAPABILITY_GAP을 CriticalEscalationType에 추가 (Critical 7종 외 보고).
  - ❌ 5 모듈 본체 신규 abstraction 생성.

CLI:
  python3 utils/bot_merge_identity.py --classify-token-source
  python3 utils/bot_merge_identity.py --audit --pr 72 --task-id task-2521
  python3 utils/bot_merge_identity.py --score --identity-from-stdin
"""
from __future__ import annotations

import argparse
import hashlib
import json
import os
import sys
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from enum import Enum
from pathlib import Path
from typing import Iterable, Mapping, Optional

__all__ = [
    "TokenSource",
    "TokenSourceProbe",
    "MergeIdentityAuditRecord",
    "AutonomyScoreDelta",
    "classify_token_source",
    "probe_token_source_from_env",
    "fingerprint_token_for_audit",
    "build_audit_record",
    "append_audit_jsonl",
    "decide_autonomy_capability_gap",
    "compute_autonomy_score_delta",
    "expected_bot_identity_for_actor",
    # task-2523 narrow additions — 회귀 검증 #6/#8용
    "verify_branch_cleanup_token_inheritance",
    "REQUIRED_AUDIT_FIELDS_2523",
    # 4 enum source label 표준 문자열 (분류 결과 직렬화에 사용)
    "TOKEN_SOURCE_OWNER_PAT",
    "TOKEN_SOURCE_GITHUB_APP",
    "TOKEN_SOURCE_GITHUB_ACTIONS",
    "TOKEN_SOURCE_UNKNOWN",
    # default audit JSONL 경로
    "DEFAULT_AUDIT_JSONL_PATH",
]


# ---------------------------------------------------------------------------
# 1. TokenSource enum (회장 §1 — 4종 정확)
# ---------------------------------------------------------------------------

TOKEN_SOURCE_OWNER_PAT = "OWNER_PAT"
TOKEN_SOURCE_GITHUB_APP = "GITHUB_APP_INSTALLATION_TOKEN"
TOKEN_SOURCE_GITHUB_ACTIONS = "GITHUB_ACTIONS_TOKEN"
TOKEN_SOURCE_UNKNOWN = "UNKNOWN"


class TokenSource(str, Enum):
    """Token 출처 4 enum. 회장 §1 — 정확히 4종, 추가 금지."""

    OWNER_PAT = TOKEN_SOURCE_OWNER_PAT
    GITHUB_APP_INSTALLATION_TOKEN = TOKEN_SOURCE_GITHUB_APP
    GITHUB_ACTIONS_TOKEN = TOKEN_SOURCE_GITHUB_ACTIONS
    UNKNOWN = TOKEN_SOURCE_UNKNOWN


# ---------------------------------------------------------------------------
# 2. Token prefix → TokenSource 결정 표 (raw 값 노출 X — 첫 5자만 검사)
# ---------------------------------------------------------------------------
# 회장 §보안: token raw value 절대 출력 X. prefix 5자만 비교에 사용.
# GitHub 공식 토큰 prefix:
#   - ghs_  : GitHub App installation access token
#   - ghp_  : Personal Access Token (classic)
#   - github_pat_ : Fine-grained PAT (owner-scope)
#   - gho_  : OAuth token
#   - ghu_  : User-to-server (App에서 발급되는 user token)
#   - ghr_  : Refresh token
_INSTALLATION_TOKEN_PREFIXES: tuple[str, ...] = ("ghs_",)
_OWNER_PAT_PREFIXES: tuple[str, ...] = ("ghp_", "github_pat_", "gho_")
# Actions runner 토큰은 prefix가 일정하지 않다 (ghs_ 형태로 발급되기도 함).
# 따라서 GITHUB_ACTIONS=true 환경 변수 + ACTIONS_ID_TOKEN_REQUEST_TOKEN 같은
# runner-only env로 식별한다.

# Actions 환경 식별 환경 변수 (env 존재 여부만 확인 — 값 X)
_ACTIONS_RUNNER_ENV_NAMES: tuple[str, ...] = (
    "GITHUB_ACTIONS",                    # "true" 면 actions runner
    "RUNNER_NAME",                       # actions runner instance name
    "GITHUB_WORKFLOW",                   # workflow name 존재
    "ACTIONS_ID_TOKEN_REQUEST_TOKEN",    # OIDC token request endpoint
)

# token 값을 보유한 env var 후보 (존재 여부만 확인)
_TOKEN_ENV_NAMES_OWNER_PAT: tuple[str, ...] = (
    "GITHUB_PAT",
    "GH_PAT",
    "OWNER_GITHUB_TOKEN",
)
_TOKEN_ENV_NAMES_INSTALLATION: tuple[str, ...] = (
    "INSTALLATION_TOKEN",
    "GITHUB_APP_INSTALLATION_TOKEN",
    "APP_INSTALLATION_TOKEN",
)
_TOKEN_ENV_NAMES_GENERIC: tuple[str, ...] = (
    "GITHUB_TOKEN",
    "GH_TOKEN",
)


@dataclass(frozen=True)
class TokenSourceProbe:
    """token_source 분류 결과. raw value는 절대 보유하지 않는다.

    Fields:
      token_source: 분류 결과 (TokenSource enum value).
      env_var_name_observed: 어느 env var에서 token이 발견됐는지 (값 X — 이름만).
      token_prefix_observed: 5자 prefix만 (e.g. "ghs_", "ghp_", "githu"). 값 자체 X.
      actions_runner_signal: GITHUB_ACTIONS=true 또는 RUNNER_NAME 존재 시 True.
      installation_signal: ghs_ prefix 또는 INSTALLATION_TOKEN 계열 env 존재 시 True.
      owner_pat_signal: ghp_/github_pat_/gho_ prefix 또는 GH_PAT 계열 env 존재 시 True.
      token_fingerprint_sha256_8: token raw가 있을 경우 sha256(token)의 첫 8 hex 문자만.
                                  raw value는 절대 보존되지 않으며, 동일 token
                                  identity 추적용으로만 사용.

    회장 §보안: raw token value는 어떤 형태로도 보유 X.
    """

    token_source: str
    env_var_name_observed: Optional[str] = None
    token_prefix_observed: Optional[str] = None
    actions_runner_signal: bool = False
    installation_signal: bool = False
    owner_pat_signal: bool = False
    token_fingerprint_sha256_8: Optional[str] = None

    def to_dict(self) -> dict:
        return asdict(self)


def fingerprint_token_for_audit(token_value: Optional[str]) -> Optional[str]:
    """token raw value의 sha256 첫 8 hex 문자만 반환 (회장 §보안).

    동일 token identity 추적이 필요할 때만 사용. raw 값은 어디에도 저장하지 않는다.
    None / 빈 문자열 → None 반환 (audit에 fingerprint=null 로 기록).
    """
    if not token_value:
        return None
    digest = hashlib.sha256(token_value.encode("utf-8")).hexdigest()
    return digest[:8]


def _detect_prefix(token_value: str) -> str:
    """token raw → 첫 5자 prefix만 추출. 5자 미만이면 그대로 (X 마스크).

    회장 §보안: 5자 이상 노출 금지. 첫 5자도 prefix 식별용으로만 사용.
    """
    if not token_value:
        return ""
    return token_value[:5]


def classify_token_source(
    *,
    token_value: Optional[str],
    env: Optional[Mapping[str, str]] = None,
) -> TokenSourceProbe:
    """token 값 (선택) + env mapping → TokenSource 분류.

    분류 우선순위 (회장 §1 deterministic):
      1. token prefix 검사 (값이 있는 경우 — 5자만 비교, raw 노출 X):
         - "ghs_"            → GITHUB_APP_INSTALLATION_TOKEN
         - "ghp_" / "githu" (github_pat_) / "gho_" → OWNER_PAT
      2. env-only fallback (값이 없거나 prefix 매칭 실패):
         - GITHUB_APP_* env 존재 → GITHUB_APP_INSTALLATION_TOKEN
         - GH_PAT / GITHUB_PAT / OWNER_GITHUB_TOKEN env 존재 → OWNER_PAT
         - GITHUB_ACTIONS=true 또는 RUNNER_NAME 존재 → GITHUB_ACTIONS_TOKEN
         - 그 외 → UNKNOWN
      3. **GitHub Actions runner 신호가 있는데 token prefix가 ghs_ 인 경우는
         GITHUB_ACTIONS_TOKEN으로 분류** (Actions runner의 GITHUB_TOKEN은
         실제로는 installation token이지만, 식별 가능한 출처는 Actions runner이므로
         "Actions runner의 자동 token" 으로 박제).

    회장 §보안 준수:
      - raw token value 절대 출력/저장 X.
      - prefix는 5자만 보존 (e.g. "ghs_", "ghp_").
      - env_var_name_observed는 이름만 (값 X).
    """
    env_map: Mapping[str, str] = env if env is not None else os.environ

    # ─── env signals ─────────────────────────────────────────────────────
    actions_signal = False
    if env_map.get("GITHUB_ACTIONS", "").lower() == "true":
        actions_signal = True
    else:
        for name in _ACTIONS_RUNNER_ENV_NAMES:
            if name == "GITHUB_ACTIONS":
                continue  # 위에서 이미 처리
            if env_map.get(name):
                actions_signal = True
                break

    installation_env_signal = any(env_map.get(n) for n in _TOKEN_ENV_NAMES_INSTALLATION)
    owner_pat_env_signal = any(env_map.get(n) for n in _TOKEN_ENV_NAMES_OWNER_PAT)

    # ─── prefix from token_value (raw 노출 X) ─────────────────────────────
    prefix = ""
    installation_prefix_signal = False
    owner_pat_prefix_signal = False
    if token_value:
        prefix = _detect_prefix(token_value)
        for p in _INSTALLATION_TOKEN_PREFIXES:
            if token_value.startswith(p):
                installation_prefix_signal = True
                break
        for p in _OWNER_PAT_PREFIXES:
            if token_value.startswith(p):
                owner_pat_prefix_signal = True
                break

    # ─── env_var name 추적 (raw value 저장 X) ─────────────────────────────
    env_var_name_observed: Optional[str] = None
    for name in (
        list(_TOKEN_ENV_NAMES_INSTALLATION)
        + list(_TOKEN_ENV_NAMES_OWNER_PAT)
        + list(_TOKEN_ENV_NAMES_GENERIC)
    ):
        if env_map.get(name):
            env_var_name_observed = name
            break

    # ─── 최종 분류 ────────────────────────────────────────────────────────
    final_source: str

    # priority 1: prefix가 명확하면 prefix 우선
    if installation_prefix_signal and not actions_signal:
        # installation token이지만 actions runner 환경이 아니면 GitHub App
        final_source = TOKEN_SOURCE_GITHUB_APP
    elif owner_pat_prefix_signal:
        # PAT prefix → OWNER_PAT (actions runner라도 PAT 주입은 owner_pat)
        final_source = TOKEN_SOURCE_OWNER_PAT
    elif installation_prefix_signal and actions_signal:
        # GITHUB_TOKEN (ghs_) on actions runner → Actions runner token
        final_source = TOKEN_SOURCE_GITHUB_ACTIONS
    elif installation_env_signal:
        final_source = TOKEN_SOURCE_GITHUB_APP
    elif owner_pat_env_signal:
        final_source = TOKEN_SOURCE_OWNER_PAT
    elif actions_signal:
        final_source = TOKEN_SOURCE_GITHUB_ACTIONS
    else:
        final_source = TOKEN_SOURCE_UNKNOWN

    return TokenSourceProbe(
        token_source=final_source,
        env_var_name_observed=env_var_name_observed,
        token_prefix_observed=prefix or None,
        actions_runner_signal=actions_signal,
        installation_signal=installation_prefix_signal or installation_env_signal,
        owner_pat_signal=owner_pat_prefix_signal or owner_pat_env_signal,
        token_fingerprint_sha256_8=fingerprint_token_for_audit(token_value),
    )


def probe_token_source_from_env(env: Optional[Mapping[str, str]] = None) -> TokenSourceProbe:
    """env-only 분류. env 우선순위:
       INSTALLATION_TOKEN 계열 > GITHUB_PAT 계열 > GITHUB_TOKEN/GH_TOKEN > 없음.

    raw token 값은 함수 내부에서만 검사하고 외부로 노출하지 않는다.
    """
    env_map: Mapping[str, str] = env if env is not None else os.environ

    candidate_value: Optional[str] = None
    for name in (
        list(_TOKEN_ENV_NAMES_INSTALLATION)
        + list(_TOKEN_ENV_NAMES_OWNER_PAT)
        + list(_TOKEN_ENV_NAMES_GENERIC)
    ):
        v = env_map.get(name)
        if v:
            candidate_value = v
            break

    return classify_token_source(token_value=candidate_value, env=env_map)


# ---------------------------------------------------------------------------
# 3. Identity audit (회장 §3)
# ---------------------------------------------------------------------------

# default audit JSONL 경로 (workspace 안). dispatch에서 override 가능.
DEFAULT_AUDIT_JSONL_PATH = Path(
    os.environ.get(
        "BOT_MERGE_IDENTITY_AUDIT_JSONL",
        str(Path(__file__).resolve().parent.parent / "memory" / "orchestration-audit" / "bot-merge-identity.jsonl"),
    )
)


@dataclass(frozen=True)
class MergeIdentityAuditRecord:
    """Single PR merge audit record.

    회장 §3 audit 구조 7 field 정확 매칭:
      pr_number, token_source_used, mergedBy_login, mergedBy_is_bot,
      expected_bot_identity, autonomy_capability_gap, timestamp.

    추가 metadata는 분리된 sub-dict (raw value 절대 X).
    """

    pr_number: int
    token_source_used: str               # TokenSource value
    mergedBy_login: str                  # 빈 문자열 = 데이터 없음
    mergedBy_is_bot: bool
    expected_bot_identity: bool          # bot/app으로 머지될 것을 기대했는지
    autonomy_capability_gap: bool        # owner_pat fallback 또는 token mismatch
    timestamp: str                       # ISO8601 UTC
    # 보조 metadata (회장 §보안 — raw value 노출 X)
    token_prefix_observed: Optional[str] = None     # 5자 prefix만
    env_var_name_observed: Optional[str] = None
    token_fingerprint_sha256_8: Optional[str] = None
    merge_commit_sha: Optional[str] = None
    task_id: Optional[str] = None
    notes: Optional[str] = None
    # task-2523 §검증 8 — audit JSONL 4 필드 contract용 명시 boolean
    # (token_source_used == OWNER_PAT 인지 별도 박제 — 회장 §"owner_pat_used")
    owner_pat_used: bool = False

    def to_dict(self) -> dict:
        return asdict(self)


def expected_bot_identity_for_actor(login: str, actor_type: str) -> bool:
    """주어진 mergedBy actor가 bot/app identity로 인정 가능한지.

    - login이 "[bot]" 으로 끝나면 → True
    - actor_type이 "Bot" 이면 → True (대소문자 무시)
    - 그 외 → False
    """
    if not login:
        return False
    if login.lower().endswith("[bot]"):
        return True
    if (actor_type or "").lower() == "bot":
        return True
    return False


def decide_autonomy_capability_gap(
    *,
    token_source: str,
    mergedBy_login: str,
    mergedBy_is_bot: bool,
) -> bool:
    """token_source + mergedBy → autonomy_capability_gap 박제.

    회장 §본질:
      - mergedBy가 bot/app이고 token_source도 GITHUB_APP_*/GITHUB_ACTIONS_TOKEN
        → False (정상 자동화 success).
      - mergedBy가 owner (사람) 이면 → True (owner_pat fallback이 명시적).
      - token_source == OWNER_PAT 면 → True (mergedBy가 bot이라도 owner_pat 사용은
        autonomy success로 인정 X — task-2522 §본질).
      - token_source == UNKNOWN 면 → True (fail-closed, 분류 불가는 보수적으로 GAP).
      - mergedBy 데이터가 비어있고 token_source가 GITHUB_APP_*/GITHUB_ACTIONS_TOKEN
        → False (audit 데이터 부재 → GAP 분류 안 함; pre-merge 단계 record).
    """
    # owner가 직접 머지 → 무조건 GAP
    if mergedBy_login and not mergedBy_is_bot:
        return True
    # token_source가 OWNER_PAT 면 무조건 GAP (mergedBy bot이라도 적용)
    if token_source == TOKEN_SOURCE_OWNER_PAT:
        return True
    # token_source UNKNOWN 도 보수적으로 GAP
    if token_source == TOKEN_SOURCE_UNKNOWN:
        return True
    # mergedBy 데이터 부재 + token_source 정상 → pre-merge record (GAP X)
    return False


def build_audit_record(
    *,
    pr_number: int,
    token_probe: TokenSourceProbe,
    mergedBy_login: str = "",
    mergedBy_type: str = "",
    merge_commit_sha: Optional[str] = None,
    task_id: Optional[str] = None,
    notes: Optional[str] = None,
    timestamp: Optional[str] = None,
) -> MergeIdentityAuditRecord:
    """TokenSourceProbe + mergedBy → MergeIdentityAuditRecord 생성.

    timestamp 미지정 시 datetime.now(timezone.utc) 사용.
    """
    is_bot = expected_bot_identity_for_actor(mergedBy_login, mergedBy_type)
    expected = token_probe.token_source in {
        TOKEN_SOURCE_GITHUB_APP,
        TOKEN_SOURCE_GITHUB_ACTIONS,
    }
    gap = decide_autonomy_capability_gap(
        token_source=token_probe.token_source,
        mergedBy_login=mergedBy_login,
        mergedBy_is_bot=is_bot,
    )
    ts = timestamp or datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
    owner_pat_used = token_probe.token_source == TOKEN_SOURCE_OWNER_PAT
    return MergeIdentityAuditRecord(
        pr_number=int(pr_number),
        token_source_used=token_probe.token_source,
        mergedBy_login=mergedBy_login or "",
        mergedBy_is_bot=is_bot,
        expected_bot_identity=expected,
        autonomy_capability_gap=gap,
        timestamp=ts,
        token_prefix_observed=token_probe.token_prefix_observed,
        env_var_name_observed=token_probe.env_var_name_observed,
        token_fingerprint_sha256_8=token_probe.token_fingerprint_sha256_8,
        merge_commit_sha=merge_commit_sha,
        task_id=task_id,
        notes=notes,
        owner_pat_used=owner_pat_used,
    )


def append_audit_jsonl(
    record: MergeIdentityAuditRecord,
    *,
    audit_path: Optional[Path] = None,
) -> Path:
    """audit record를 JSONL 형식으로 append.

    회장 §보안: record는 raw token 값을 보유하지 않으므로 그대로 직렬화 가능.
    경로 부재 시 default 경로 사용. 부모 디렉터리는 자동 생성.
    """
    path = audit_path or DEFAULT_AUDIT_JSONL_PATH
    path.parent.mkdir(parents=True, exist_ok=True)
    line = json.dumps(record.to_dict(), ensure_ascii=False)
    with open(path, "a", encoding="utf-8") as fh:
        fh.write(line + "\n")
    return path


# ---------------------------------------------------------------------------
# 3.1 task-2523 추가 — branch cleanup token inheritance + audit 4 필드 contract
# ---------------------------------------------------------------------------

# task-2523 §검증 8: audit JSONL 라인이 반드시 가져야 하는 4 필드.
# token_source_used / mergedBy_login / owner_pat_used / expected_bot_identity 4종.
REQUIRED_AUDIT_FIELDS_2523: tuple[str, ...] = (
    "token_source_used",
    "mergedBy_login",
    "owner_pat_used",
    "expected_bot_identity",
)


def verify_branch_cleanup_token_inheritance(merge_args: list[str]) -> dict:
    """`gh pr merge --delete-branch ...` 호출이 process-local GH_TOKEN 주입을
    그대로 상속하는지 정적 검증 (task-2523 §검증 6).

    회장 §본질:
      branch cleanup도 같은 머지 호출 안에서 일어나야 한다 (별도 토큰 X).
      `gh pr merge --squash --delete-branch` 한 번 호출로 머지+삭제가
      동일 process 내 동일 GH_TOKEN으로 수행된다.

    검증 항목 (정적 — 실제 API 호출 X):
      - args에 `gh pr merge` 가 포함
      - `--delete-branch` 플래그 존재 (branch cleanup 동일 호출)
      - admin override / force / rebase 플래그 부재 (회장 §금지)

    반환:
      {
        "branch_cleanup_in_same_call": bool,
        "delete_branch_flag_present": bool,
        "merge_command_present": bool,
        "forbidden_flags_detected": list[str],
        "token_inherits_process_local_gh_token": bool,
      }
    """
    args = list(merge_args or [])
    merge_present = (
        len(args) >= 3 and args[0] == "gh" and args[1] == "pr" and args[2] == "merge"
    )
    delete_present = "--delete-branch" in args
    forbidden = [
        a for a in args
        if a in {"--admin", "--force", "--rebase"} or a.startswith("--admin=")
    ]
    # process-local GH_TOKEN 주입은 호출 측 책임. 본 함수는 cleanup이 별도
    # 호출이 아닌 동일 호출 안에 묶여 있는지를 검증한다 (즉, 동일 GH_TOKEN 상속 보장).
    token_inherits = merge_present and delete_present and not forbidden
    return {
        "branch_cleanup_in_same_call": merge_present and delete_present,
        "delete_branch_flag_present": delete_present,
        "merge_command_present": merge_present,
        "forbidden_flags_detected": forbidden,
        "token_inherits_process_local_gh_token": token_inherits,
    }


# ---------------------------------------------------------------------------
# 4. Autonomy score delta (회장 §완료조건 — autonomy 7→N)
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class AutonomyScoreDelta:
    """이전 score → 다음 score 변화 박제.

    회장 §정책:
      - mergedBy=bot/app & token_source=GITHUB_APP_* → +1 (cap 10)
      - mergedBy=bot/app & token_source=GITHUB_ACTIONS_TOKEN → +1 (cap 10)
      - mergedBy=owner & token_source=OWNER_PAT → 유지 또는 -1 (회장 §본질 - stuck signal)
      - token_source=OWNER_PAT (mergedBy bot 포함) → -1 (위장 차단)
      - token_source=UNKNOWN → 유지 (분류 실패는 점수 변경 안 함, GAP만 박제)
    """

    previous_score: int
    new_score: int
    delta: int
    reason: str


def compute_autonomy_score_delta(
    *,
    previous_score: int,
    record: MergeIdentityAuditRecord,
) -> AutonomyScoreDelta:
    """audit record를 기반으로 autonomy_score 변화 산출.

    score 범위: 0 ~ 10.
    """
    prev = max(0, min(10, int(previous_score)))
    delta = 0
    reason = ""

    if record.autonomy_capability_gap:
        # owner_pat fallback이 1건이라도 → 무조건 stuck or down
        if record.token_source_used == TOKEN_SOURCE_OWNER_PAT:
            delta = -1
            reason = "owner_pat token used (autonomy fallback) — score -1"
        elif record.mergedBy_login and not record.mergedBy_is_bot:
            delta = -1
            reason = f"owner direct merge (mergedBy={record.mergedBy_login}) — score -1"
        else:
            delta = 0
            reason = "capability_gap detected without explicit owner_pat — score held"
    else:
        # gap 없음 → bot/app 머지 정상
        if record.token_source_used in (
            TOKEN_SOURCE_GITHUB_APP,
            TOKEN_SOURCE_GITHUB_ACTIONS,
        ) and record.mergedBy_is_bot:
            delta = 1
            reason = (
                f"bot/app merge ({record.token_source_used}) by "
                f"{record.mergedBy_login or '(pre-merge)'} — score +1"
            )
        else:
            delta = 0
            reason = "no gap but identity ambiguous — score held"

    new_score = max(0, min(10, prev + delta))
    return AutonomyScoreDelta(
        previous_score=prev,
        new_score=new_score,
        delta=new_score - prev,
        reason=reason,
    )


# ---------------------------------------------------------------------------
# 5. CLI entrypoint
# ---------------------------------------------------------------------------

def _build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(
        description=(
            "bot_merge_identity — token source 4 enum 분류 + identity audit 박제. "
            "(회장 §보안: raw token value 절대 출력 X)"
        )
    )
    p.add_argument(
        "--classify-token-source",
        action="store_true",
        help="env에서 token source 추출 후 JSON 출력 (값 X — source label만)",
    )
    p.add_argument(
        "--audit",
        action="store_true",
        help="--pr 와 함께 사용. mergedBy stdin JSON 입력 → audit record 출력",
    )
    p.add_argument("--pr", type=int, default=0, help="PR 번호")
    p.add_argument("--task-id", type=str, default=None, help="task id (audit metadata)")
    p.add_argument("--merge-commit", type=str, default=None, help="merge commit sha")
    p.add_argument(
        "--audit-path",
        type=str,
        default=None,
        help="audit JSONL 경로 (default: DEFAULT_AUDIT_JSONL_PATH)",
    )
    p.add_argument(
        "--no-write",
        action="store_true",
        help="--audit 시 JSONL append 하지 않고 stdout만",
    )
    p.add_argument(
        "--score",
        action="store_true",
        help="stdin record JSON + --previous-score 로 delta 계산",
    )
    p.add_argument(
        "--previous-score",
        type=int,
        default=7,
        help="--score 시 이전 점수 (default 7)",
    )
    p.add_argument(
        "--mergedBy-login",
        type=str,
        default="",
        help="--audit: mergedBy login (실호출 대신 직접 주입)",
    )
    p.add_argument(
        "--mergedBy-type",
        type=str,
        default="",
        help="--audit: mergedBy type (User/Bot)",
    )
    return p


def main(argv: Optional[Iterable[str]] = None) -> int:
    parser = _build_parser()
    args = parser.parse_args(list(argv) if argv is not None else None)

    if args.classify_token_source:
        probe = probe_token_source_from_env()
        # raw value 절대 출력 X — to_dict() 로 직렬화
        print(json.dumps(probe.to_dict(), ensure_ascii=False, indent=2))
        return 0

    if args.audit:
        if args.pr <= 0:
            print(json.dumps({"error": "--pr is required"}), file=sys.stderr)
            return 2
        probe = probe_token_source_from_env()
        record = build_audit_record(
            pr_number=args.pr,
            token_probe=probe,
            mergedBy_login=args.mergedBy_login,
            mergedBy_type=args.mergedBy_type,
            merge_commit_sha=args.merge_commit,
            task_id=args.task_id,
        )
        if not args.no_write:
            audit_path = Path(args.audit_path) if args.audit_path else None
            append_audit_jsonl(record, audit_path=audit_path)
        print(json.dumps(record.to_dict(), ensure_ascii=False, indent=2))
        return 0

    if args.score:
        try:
            payload = json.loads(sys.stdin.read() or "{}")
        except json.JSONDecodeError as exc:
            print(json.dumps({"error": f"stdin not JSON: {exc}"}), file=sys.stderr)
            return 2
        # payload는 MergeIdentityAuditRecord.to_dict() 형식 기대
        try:
            record = MergeIdentityAuditRecord(**{k: v for k, v in payload.items() if k in MergeIdentityAuditRecord.__dataclass_fields__})
        except TypeError as exc:
            print(json.dumps({"error": f"record fields mismatch: {exc}"}), file=sys.stderr)
            return 2
        delta = compute_autonomy_score_delta(previous_score=args.previous_score, record=record)
        print(json.dumps(asdict(delta), ensure_ascii=False, indent=2))
        return 0

    parser.print_help()
    return 0


if __name__ == "__main__":  # pragma: no cover
    sys.exit(main())
