"""
codex_review_loop_decider.py
task-2711 — ANU_CODEX_REVIEW_LOOP_DECIDER
chair_authorization_id: CHAIR-AUTH-TASK-2711-ANU-CODEX-REVIEW-LOOP-DECIDER-260530

Python ≥3.9 호환. 표준 라이브러리만 사용.
"""
from __future__ import annotations

import json
import re
import sys
from typing import Literal, NamedTuple, Optional

# ---------------------------------------------------------------------------
# Public API types
# ---------------------------------------------------------------------------

DecisionEnum = Literal[
    'AUTO_REVISION_CONTINUE',
    'CHAIR_DECISION_REQUIRED',
    'LOCK_READY',
    'PILOT_READY_BUT_NEEDS_CHAIR',
    'CRITICAL_ESCALATION',
]


class CodexReviewInput(NamedTuple):
    task_id: str
    version: int
    round_number: int
    overall_verdict: str            # PASS / PASS_WITH_RECOMMENDATIONS / NEEDS_REVISION / HOLD_FOR_CHAIR
    pilot_readiness: str            # READY / READY_WITH_RECOMMENDATIONS / NOT_READY_WITHOUT_FOLLOWUP / NOT_READY
    axis_counts: dict
    remaining_recommendations: list
    locked_status: bool
    chair_authorization_id: str
    chair_minor_doc_cleanup_authorized: bool = False
    prior_rounds_history: Optional[list] = None


class DecisionResult(NamedTuple):
    decision: DecisionEnum
    rationale: str
    next_action: str
    risk_triggers_matched: list     # §4.3 trigger 번호 (1~6)
    chair_facing_summary: str       # AUTO 시 빈 문자열 ""
    audit_marker_path: str


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

def _make_blob(remaining: list) -> str:
    """remaining_recommendations 의 모든 문자열을 합쳐 영문만 소문자 정규화한 텍스트."""
    joined = " ".join(str(r) for r in remaining)
    # 영문 알파벳만 소문자로, 한글 및 기타 문자는 그대로
    result = []
    for ch in joined:
        if ch.isascii() and ch.isalpha():
            result.append(ch.lower())
        else:
            result.append(ch)
    return "".join(result)


def _check_trigger1(blob: str) -> bool:
    """Critical 7 doctrine 키워드 매칭."""
    keywords = [
        "critical 7",
        "chair_required",
        "critical_escalation",
        "feedback_critical",
    ]
    return any(kw in blob for kw in keywords)


def _check_trigger4(blob: str) -> bool:
    """실제 dispatch/PR/push/merge/GitHub write."""
    keywords = [
        "dispatch.py",
        "bot_settings",
        "실제 dispatch",
        " pr ",
        "pr/push",
        "pull request",
        "push",
        "merge",
        "github write",
        "git push",
    ]
    return any(kw in blob for kw in keywords)


def _check_trigger5(blob: str) -> bool:
    """immutable scope edit 요구."""
    # immutable scope 관련 파일명이 등장
    immutable_files = [
        "finish-task.sh",
        "task-scope-guard",
        "session-watchdog",
        "qc_verify",
        "merge_queue_executor",
        "real_merge_hooks",
        "anu_v3",
        "immutable scope",
    ]
    if any(kw in blob for kw in immutable_files):
        return True
    # allowed_existing_file_edits 와 immutable 언급이 동시에 있을 때
    if "allowed_existing_file_edits" in blob and "immutable" in blob:
        return True
    return False


# evidence-reference 토큰
_EVIDENCE_TOKENS = [
    "marker",
    "evidence",
    "recorded_at",
    "task-2706",
    "task-2707",
    "task-2708",
    "task-2709",
    "task-2710",
]

# mutation 동사 (reclassify/overwrite/덮어/재분류 조합일 때만)
_MUTATION_VERBS = [
    "reclassify",
    "overwrite",
    "덮어",
    "재분류",
]


def _check_trigger6(remaining: list) -> bool:
    """기존 evidence overwrite/reclassify 시도.

    remaining 문자열 중 하나가 evidence-reference 토큰과
    mutation 동사(reclassify/overwrite/덮어/재분류)를 동시에 포함할 때만 trigger.
    '정정'이나 '변경' 단독은 trigger 안 됨.
    """
    for item in remaining:
        # 각 item을 blob 처리 (영문 소문자 + 한글 유지)
        item_chars = []
        for ch in str(item):
            if ch.isascii() and ch.isalpha():
                item_chars.append(ch.lower())
            else:
                item_chars.append(ch)
        item_normalized = "".join(item_chars)

        has_evidence = any(tok in item_normalized for tok in _EVIDENCE_TOKENS)
        has_mutation = any(verb in item_normalized for verb in _MUTATION_VERBS)

        if has_evidence and has_mutation:
            return True
    return False


def _check_trigger2(blob: str) -> bool:
    """권한 확대 요구."""
    keywords = [
        "new allowed_path",
        "allowed_path 추가",
        "권한 확대",
        "권한확대",
        "new chair_auth",
        "chair_authorization scope",
    ]
    return any(kw in blob for kw in keywords)


def _check_trigger3(blob: str) -> bool:
    """forbidden target 변경 요구."""
    keywords = [
        "forbidden",
        "§11.3",
        "forbidden_paths",
        "forbidden target",
    ]
    return any(kw in blob for kw in keywords)


def _normalize_tokens(text: str) -> set:
    """텍스트를 정규화하여 길이≥2 토큰 집합 반환."""
    # 영문 소문자화
    result = []
    for ch in text:
        if ch.isascii() and ch.isalpha():
            result.append(ch.lower())
        else:
            result.append(ch)
    normalized = "".join(result)
    tokens = set(t for t in re.split(r"[\s,./·★\-]+", normalized) if len(t) >= 2)
    return tokens


def _check_same_blocker_3_rounds(
    remaining_recommendations: list,
    prior_rounds_history: Optional[list],
) -> bool:
    """§5.4 동일 blocker 3회 반복 검사.

    현재(N) + 직전 2개 round(N-1, N-2) 중 ≥2/3 round에서
    현재 remaining 핵심 토큰이 동일하게 등장하면 True.
    """
    if not prior_rounds_history or len(prior_rounds_history) < 2:
        return False

    # 최근 2개 prior round
    recent_priors = prior_rounds_history[-2:]  # [N-2, N-1] 순서

    current_text = " ".join(str(r) for r in remaining_recommendations)
    current_tokens = _normalize_tokens(current_text)

    if not current_tokens:
        return False

    # 현재 포함 3 round 텍스트 목록
    round_texts = []
    for pr in recent_priors:
        pr_remaining = pr.get("remaining", [])
        round_texts.append(" ".join(str(r) for r in pr_remaining))
    # 현재 round는 current_text
    round_texts.append(current_text)

    # 각 round 텍스트에서 current_tokens 중 얼마나 등장하는지 확인
    # "등장"의 기준: 해당 round 텍스트에 current_tokens의 토큰이 하나라도 포함
    # §7.6 fixture: 모든 round remaining이 "spec X 보강 필요"로 동일 → 매치
    match_count = 0
    for rt in round_texts:
        rt_tokens = _normalize_tokens(rt)
        # 현재 토큰과 교집합이 있으면 해당 round에서 동일 blocker 등장
        overlap = current_tokens & rt_tokens
        if len(overlap) >= 1:
            match_count += 1

    # 3개 round 중 ≥2개에서 동일 핵심 문구 반복
    return match_count >= 2


def _check_fail_2_same_axis(
    axis_counts: dict,
    prior_rounds_history: Optional[list],
) -> bool:
    """§5.4 FAIL 2회 같은 axis 반복 검사.

    현재 axis_counts.fail_axes와 prior_rounds_history 최근 1개(N-1)
    axis_counts.fail_axes에 공통 axis 존재 시 True.
    """
    if not prior_rounds_history:
        return False

    current_fail_axes = axis_counts.get("fail_axes")
    if not current_fail_axes:
        return False

    # 최근 1개 prior round
    prev_round = prior_rounds_history[-1]
    prev_axis_counts = prev_round.get("axis_counts", {})
    prev_fail_axes = prev_axis_counts.get("fail_axes")

    if not prev_fail_axes:
        return False

    # 공통 axis 존재 여부
    current_set = set(current_fail_axes)
    prev_set = set(prev_fail_axes)
    return bool(current_set & prev_set)


def _check_stagnation(
    axis_counts: dict,
    remaining_recommendations: list,
    prior_rounds_history: Optional[list],
) -> bool:
    """§5.4 stagnation: 현재 axis 분포 == 직전 round axis 분포 AND remaining ≥ 직전."""
    if not prior_rounds_history:
        return False

    prev_round = prior_rounds_history[-1]
    prev_axis_counts = prev_round.get("axis_counts", {})
    prev_remaining = prev_round.get("remaining", [])

    # axis 분포 비교 (fail_axes 제외한 숫자 counts)
    def _counts_only(ac: dict) -> dict:
        return {k: v for k, v in ac.items() if k != "fail_axes"}

    if _counts_only(axis_counts) != _counts_only(prev_axis_counts):
        return False

    return len(remaining_recommendations) >= len(prev_remaining)


# ---------------------------------------------------------------------------
# audit_marker_path
# ---------------------------------------------------------------------------

def _audit_marker_path(task_id: str, round_number: int) -> str:
    return f"memory/events/{task_id}.codex-review-decision-r{round_number}.json"


# ---------------------------------------------------------------------------
# Main decide() function
# ---------------------------------------------------------------------------

def decide(input: CodexReviewInput) -> DecisionResult:
    """회장 verbatim 5 enum + risk gating + loop boundary.

    5-step precedence (위에서부터 평가, 첫 매치 반환):
      Step 1: CRITICAL_ESCALATION (trigger 1,4,5,6)
      Step 2: CHAIR_DECISION_REQUIRED (trigger 2,3 + §5.4 boundary)
      Step 3: LOCK_READY
      Step 4: PILOT_READY_BUT_NEEDS_CHAIR
      Step 5: AUTO_REVISION_CONTINUE (default)
    """
    blob = _make_blob(input.remaining_recommendations)
    amp = _audit_marker_path(input.task_id, input.round_number)

    # ------------------------------------------------------------------
    # STEP 1 — CRITICAL_ESCALATION
    # ------------------------------------------------------------------
    triggered = []
    if _check_trigger1(blob):
        triggered.append(1)
    if _check_trigger4(blob):
        triggered.append(4)
    if _check_trigger5(blob):
        triggered.append(5)
    if _check_trigger6(input.remaining_recommendations):
        triggered.append(6)

    if triggered:
        summary_lines = [
            f"[CRITICAL_ESCALATION] task_id={input.task_id} round={input.round_number}",
            f"트리거: {triggered}",
            f"remaining: {input.remaining_recommendations}",
        ]
        return DecisionResult(
            decision='CRITICAL_ESCALATION',
            rationale=(
                f"§4.3 risk trigger {triggered} 매칭으로 CRITICAL_ESCALATION 결정. "
                "ANU auto loop 즉시 중단하고 회장에 즉시 보고해야 합니다."
            ),
            next_action="ANU auto loop 즉시 중단 + 회장 즉시 보고",
            risk_triggers_matched=triggered,
            chair_facing_summary="\n".join(summary_lines),
            audit_marker_path=amp,
        )

    # ------------------------------------------------------------------
    # STEP 2 — CHAIR_DECISION_REQUIRED
    # ------------------------------------------------------------------
    chair_triggers = []
    chair_boundary_reason = ""

    if _check_trigger2(blob):
        chair_triggers.append(2)
    if _check_trigger3(blob):
        chair_triggers.append(3)

    # §5.4 loop boundary 검사
    boundary_violations = []

    # max round > 7
    if input.round_number > 7:
        boundary_violations.append(f"max round 초과 (round_number={input.round_number} > 7)")

    # 동일 blocker 3회 반복
    if _check_same_blocker_3_rounds(
        input.remaining_recommendations, input.prior_rounds_history
    ):
        boundary_violations.append("동일 blocker 3회 반복 (§5.4.1)")

    # FAIL 2회 같은 axis
    if _check_fail_2_same_axis(input.axis_counts, input.prior_rounds_history):
        boundary_violations.append("FAIL 2회 같은 axis 반복 (§5.4.2)")

    # stagnation (보조)
    if _check_stagnation(
        input.axis_counts, input.remaining_recommendations, input.prior_rounds_history
    ):
        boundary_violations.append("stagnation (axis 분포 동일 + remaining 미감소)")

    if chair_triggers or boundary_violations:
        if boundary_violations:
            chair_boundary_reason = "; ".join(boundary_violations)

        rationale_parts = []
        if chair_triggers:
            rationale_parts.append(f"§4.3 trigger {chair_triggers} 매칭")
        if boundary_violations:
            rationale_parts.append(f"§5.4 loop boundary 위반: {chair_boundary_reason}")

        summary_lines = [
            f"[CHAIR_DECISION_REQUIRED] task_id={input.task_id} round={input.round_number}",
        ]
        if chair_triggers:
            summary_lines.append(f"트리거: {chair_triggers}")
        if boundary_violations:
            summary_lines.append(f"경계 위반: {boundary_violations}")
        summary_lines.append(f"remaining: {input.remaining_recommendations}")

        return DecisionResult(
            decision='CHAIR_DECISION_REQUIRED',
            rationale=(
                " + ".join(rationale_parts) +
                "으로 CHAIR_DECISION_REQUIRED 결정. ANU 회장 보고 후 verbatim 대기."
            ),
            next_action="ANU 회장 보고 + verbatim 대기",
            risk_triggers_matched=chair_triggers,  # boundary는 [] 로, rationale에 명시
            chair_facing_summary="\n".join(summary_lines),
            audit_marker_path=amp,
        )

    # ------------------------------------------------------------------
    # STEP 3 — LOCK_READY
    # ------------------------------------------------------------------
    if (
        input.overall_verdict in ('PASS', 'PASS_WITH_RECOMMENDATIONS')
        and len(input.remaining_recommendations) == 0
        and input.locked_status is False
    ):
        summary_lines = [
            f"[LOCK_READY] task_id={input.task_id} version={input.version} round={input.round_number}",
            f"overall_verdict={input.overall_verdict}, remaining=0, locked_status=False",
        ]
        return DecisionResult(
            decision='LOCK_READY',
            rationale=(
                f"overall_verdict={input.overall_verdict}, remaining_recommendations가 비어 있고, "
                "locked_status=False이므로 LOCK_READY 결정. 회장 lock 인가 대기."
            ),
            next_action="ANU 회장 보고 + lock 인가 대기",
            risk_triggers_matched=[],
            chair_facing_summary="\n".join(summary_lines),
            audit_marker_path=amp,
        )

    # ------------------------------------------------------------------
    # STEP 4 — PILOT_READY_BUT_NEEDS_CHAIR
    # ------------------------------------------------------------------
    if (
        input.pilot_readiness in ('READY', 'READY_WITH_RECOMMENDATIONS')
        and input.chair_minor_doc_cleanup_authorized is False
    ):
        summary_lines = [
            f"[PILOT_READY_BUT_NEEDS_CHAIR] task_id={input.task_id} round={input.round_number}",
            f"pilot_readiness={input.pilot_readiness}, chair_minor_doc_cleanup_authorized=False",
        ]
        return DecisionResult(
            decision='PILOT_READY_BUT_NEEDS_CHAIR',
            rationale=(
                f"pilot_readiness={input.pilot_readiness}이나 "
                "chair_minor_doc_cleanup_authorized=False이므로 PILOT_READY_BUT_NEEDS_CHAIR 결정. "
                "회장 pilot 인가 대기."
            ),
            next_action=(
                "ANU 회장 보고 + pilot 별도 결정 대기 "
                "(신규 fresh task ID + 별도 chair_authorization_id)"
            ),
            risk_triggers_matched=[],
            chair_facing_summary="\n".join(summary_lines),
            audit_marker_path=amp,
        )

    # ------------------------------------------------------------------
    # STEP 5 — AUTO_REVISION_CONTINUE (default)
    # ------------------------------------------------------------------
    return DecisionResult(
        decision='AUTO_REVISION_CONTINUE',
        rationale=(
            "CRITICAL/CHAIR/LOCK_READY/PILOT 조건 미충족. "
            "minor refinement only → ANU 자동 v+1 revision drafting 진행."
        ),
        next_action="ANU 자동 v+1 revision drafting + Codex round+1 재요청 (silent loop)",
        risk_triggers_matched=[],
        chair_facing_summary="",
        audit_marker_path=amp,
    )


# ---------------------------------------------------------------------------
# from_dict helper
# ---------------------------------------------------------------------------

def from_dict(d: dict) -> CodexReviewInput:
    """dict(예: fixture JSON 로드 결과)를 CodexReviewInput으로 변환.
    누락 옵션 필드는 기본값 사용.
    """
    return CodexReviewInput(
        task_id=d["task_id"],
        version=d["version"],
        round_number=d["round_number"],
        overall_verdict=d["overall_verdict"],
        pilot_readiness=d["pilot_readiness"],
        axis_counts=d["axis_counts"],
        remaining_recommendations=d["remaining_recommendations"],
        locked_status=d["locked_status"],
        chair_authorization_id=d["chair_authorization_id"],
        chair_minor_doc_cleanup_authorized=d.get("chair_minor_doc_cleanup_authorized", False),
        prior_rounds_history=d.get("prior_rounds_history", None),
    )


# ---------------------------------------------------------------------------
# CLI entry point
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python3 codex_review_loop_decider.py <fixture.json>", file=sys.stderr)
        sys.exit(1)

    fixture_path = sys.argv[1]
    with open(fixture_path, "r", encoding="utf-8") as f:
        data = json.load(f)

    inp = from_dict(data)
    result = decide(inp)
    print(json.dumps(result._asdict(), ensure_ascii=False, indent=2))
