"""
correction_budget.py — ANU v2.13 correction budget ledger (순수 함수, stdlib only)

★ 이 코드는 budget을 reset하지 않는다.
  budget reset 권한은 Chair 전용이며, 코드는 소비/잔액 추적만 수행함.
  IMMEDIATE_CHAIR_AXES 에 해당하는 축은 budget 차감 없이 즉시 Chair 에스컬레이션.
"""

import json
import os
import copy
import subprocess

# ---------------------------------------------------------------------------
# 동적 workspace root resolve (HIGH 6~9 — 하드코딩 절대경로 제거)
#   1) ANU_WORKSPACE 환경변수
#   2) git rev-parse --show-toplevel (이 파일 위치 기준)
#   3) os.getcwd()
# default arg 에 절대경로를 하드코딩하지 않고, 호출 시점에 resolve 한다.
# ---------------------------------------------------------------------------

# git rev-parse 서브프로세스 결과는 프로세스 생애 내 불변 → module-level 1회 캐시.
# ANU_WORKSPACE / cwd 는 호출마다 변할 수 있으므로 캐시하지 않고 매번 평가한다
# (테스트의 monkeypatch.setenv 격리 보존). 비싼 git subprocess 만 캐싱한다.
_GIT_TOPLEVEL_CACHE = None
_GIT_TOPLEVEL_RESOLVED = False


def _git_toplevel():
    global _GIT_TOPLEVEL_CACHE, _GIT_TOPLEVEL_RESOLVED
    if _GIT_TOPLEVEL_RESOLVED:
        return _GIT_TOPLEVEL_CACHE
    try:
        here = os.path.dirname(os.path.abspath(__file__))
        top = subprocess.run(
            ["git", "rev-parse", "--show-toplevel"],
            cwd=here, capture_output=True, text=True,
        ).stdout.strip()
        _GIT_TOPLEVEL_CACHE = top or None
    except Exception:
        _GIT_TOPLEVEL_CACHE = None
    _GIT_TOPLEVEL_RESOLVED = True
    return _GIT_TOPLEVEL_CACHE


def _resolve_root():
    env = os.environ.get("ANU_WORKSPACE")
    if env:
        return env
    top = _git_toplevel()
    if top:
        return top
    return os.getcwd()


def default_ledger_path():
    """correction budget ledger 의 기본 경로 (동적 resolve)."""
    return os.path.join(
        _resolve_root(), "memory", "state", "correction_budget_ledger.json"
    )

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

DEFAULT_BUDGET = 5

WEIGHTS = {
    "expected_files_reversible":  1,
    "test_fixture_reporting":     1,
    "local_runner_wiring":        2,
    "runtime_config_touch":       3,
    "systemd_unit_touch":         3,
}

# same semantic axis 반복 시 추가 페널티
SAME_AXIS_REPEAT_PENALTY = 1   # 보통 -1, strong repeat 시 -2

# 이 축이 터치되면 budget 차감 없이 즉시 Chair
IMMEDIATE_CHAIR_AXES = frozenset([
    "credential",
    "production",
    "merge",
    "control_db_write",
    "promotion_approval",
])

# ---------------------------------------------------------------------------
# correction_weight
# ---------------------------------------------------------------------------

def correction_weight(correction_type, same_semantic_axis_repeat=False, strong_repeat=False):
    """
    Parameters
    ----------
    correction_type : str
    same_semantic_axis_repeat : bool
    strong_repeat : bool  — True 이면 페널티 2, False 이면 페널티 1

    Returns
    -------
    int — budget 차감 단위
    """
    base = WEIGHTS.get(correction_type, 1)
    if same_semantic_axis_repeat:
        penalty = 2 if strong_repeat else SAME_AXIS_REPEAT_PENALTY
        base += penalty
    return base

# ---------------------------------------------------------------------------
# is_immediate_chair
# ---------------------------------------------------------------------------

def is_immediate_chair(touched_axis):
    """해당 축이 IMMEDIATE_CHAIR_AXES 에 속하면 True."""
    return (touched_axis or "").lower() in IMMEDIATE_CHAIR_AXES

# ---------------------------------------------------------------------------
# load_ledger / save_ledger
# ---------------------------------------------------------------------------

def load_ledger(ledger_path=None):
    """
    ledger JSON 로드. 파일 없거나 비어있으면 {"goals": {}} 반환.

    Parameters
    ----------
    ledger_path : str | None  — None 이면 default_ledger_path() 로 동적 resolve

    Returns
    -------
    dict
    """
    if ledger_path is None:
        ledger_path = default_ledger_path()
    if not os.path.exists(ledger_path):
        return {"goals": {}}
    with open(ledger_path, "r", encoding="utf-8") as f:
        content = f.read().strip()
    if not content:
        return {"goals": {}}
    # ledger 가 손상(부분 쓰기/수기 편집)되어 유효 JSON 이 아니면 json.JSONDecodeError.
    # budget 추적은 fail-safe 여야 하므로 안전 기본값으로 폴백한다 (MEDIUM).
    try:
        return json.loads(content)
    except json.JSONDecodeError:
        return {"goals": {}}


def save_ledger(ledger, ledger_path=None):
    """
    ledger dict를 JSON 파일로 atomic 하게 저장 (HIGH 10).
    같은 디렉토리에 tmp 파일을 쓰고 flush + fsync 후 os.replace 로 교체한다.
    쓰기 도중 중단되어도 기존 ledger 가 부분 손상되지 않는다.

    Parameters
    ----------
    ledger : dict
    ledger_path : str | None  — None 이면 default_ledger_path() 로 동적 resolve
    """
    if ledger_path is None:
        ledger_path = default_ledger_path()
    dirpath = os.path.dirname(ledger_path)
    if dirpath:
        os.makedirs(dirpath, exist_ok=True)
    tmp_path = ledger_path + ".tmp"
    with open(tmp_path, "w", encoding="utf-8") as f:
        json.dump(ledger, f, ensure_ascii=False, indent=2)
        f.flush()
        os.fsync(f.fileno())
    os.replace(tmp_path, ledger_path)

# ---------------------------------------------------------------------------
# update_ledger
# ---------------------------------------------------------------------------

def update_ledger(
    goal_id,
    signature,
    correction_type,
    *,
    same_semantic_axis_repeat=False,
    budget=DEFAULT_BUDGET,
    ledger_path=None,
):
    """
    goal_id 에 대한 correction budget 소비를 ledger 에 기록하고 entry 반환.

    ★ budget reset 불가 (Chair 전용 권한).
      음수 소진 허용 — clamp 없음 (소진 판정은 is_budget_exhausted 로).

    Parameters
    ----------
    goal_id : str
    signature : str     — diff/tool_calls 서명 (same_signature_retry_count 추적용)
    correction_type : str
    same_semantic_axis_repeat : bool
    budget : int         — 신규 goal 생성 시 초기 budget (기존 entry 에는 무시)
    ledger_path : str

    Returns
    -------
    dict — goal entry
    """
    if ledger_path is None:
        ledger_path = default_ledger_path()
    ledger = load_ledger(ledger_path)
    goals = ledger.setdefault("goals", {})

    # 신규 goal 초기화
    if goal_id not in goals:
        goals[goal_id] = {
            "goal_id":                   goal_id,
            "budget":                    budget,
            "budget_remaining":          budget,
            "same_signature_retry_count": {},
            "same_goal_correction_count": 0,
            "history":                   [],
        }

    entry = goals[goal_id]

    # 카운터 증가
    entry["same_goal_correction_count"] = entry.get("same_goal_correction_count", 0) + 1

    sig_map = entry.setdefault("same_signature_retry_count", {})
    sig_map[signature] = sig_map.get(signature, 0) + 1

    # weight 계산 및 budget 차감
    weight = correction_weight(
        correction_type,
        same_semantic_axis_repeat=same_semantic_axis_repeat,
    )
    # 기존 goal entry 에 budget_remaining 이 누락된 경우, 함수 기본 budget 파라미터가
    # 아니라 해당 entry 자신의 budget 을 우선 사용한다 (entry 별 초기 budget 보존, MEDIUM).
    prev_remaining = entry.get("budget_remaining", entry.get("budget", budget))
    entry["budget_remaining"] = prev_remaining - weight

    # history append
    entry.setdefault("history", []).append({
        "signature":                signature,
        "correction_type":          correction_type,
        "weight":                   weight,
        "same_semantic_axis_repeat": same_semantic_axis_repeat,
        "budget_remaining_after":   entry["budget_remaining"],
        "same_goal_correction_count": entry["same_goal_correction_count"],
    })

    save_ledger(ledger, ledger_path)
    return copy.deepcopy(entry)

# ---------------------------------------------------------------------------
# is_budget_exhausted
# ---------------------------------------------------------------------------

def is_budget_exhausted(entry):
    """entry["budget_remaining"] <= 0 이면 True."""
    return entry.get("budget_remaining", 0) <= 0
