"""세션 jsonl 증분 스캔 캐시 — byte cursor + usage 이벤트 추출본 (P6).

캐시에는 *윈도우 적용 전* 이벤트 추출본을 저장하고, since/until 윈도우는 매
호출 시 이벤트 위에서 재평가한다. run 재시도로 윈도우가 좁아져도(until 이
과거로 이동) 합계가 틀어지지 않는 이유다.

캐시는 파생 데이터다: head-bytes 식별자 불일치(파일 교체)·truncate·손상 시
조용히 폐기하고 전체 재스캔으로 폴백한다(fail-open). 쓰기는 tmp+os.replace
원자적. 동시 collect 가 같은 캐시를 쓰면 last-writer-wins — 최악의 경우 다음
호출이 일부 byte 를 다시 읽을 뿐 결과는 불변.
"""
from __future__ import annotations

import hashlib
import json
import os
from pathlib import Path

from okstra_project.dirs import okstra_home

# v2: usage 이벤트에 `p`(lead PROGRESS 마커) 키 추가 — v1 캐시는 이미 스캔한
# 구간의 마커가 없으므로 폐기하고 전체 재스캔한다(fail-open).
CACHE_SCHEMA_VERSION = 2
IDENTITY_PREFIX_BYTES = 256
MAX_NEEDLES = 16


def cache_path_for(jsonl_path: Path) -> Path:
    """`$OKSTRA_HOME/cache/token-usage/<transcript-dir-name>/<session>.json`."""
    return (okstra_home() / "cache" / "token-usage"
            / jsonl_path.parent.name / f"{jsonl_path.stem}.json")


def fresh_cache(identity: dict | None = None) -> dict:
    return {
        "schemaVersion": CACHE_SCHEMA_VERSION,
        "identity": identity,
        "usage": {"offset": 0, "agentName": None, "model": None, "events": []},
        "needles": {},
    }


def _file_identity(jsonl_path: Path) -> dict | None:
    try:
        with jsonl_path.open("rb") as fh:
            prefix = fh.read(IDENTITY_PREFIX_BYTES)
    except OSError:
        return None
    return {"prefixLen": len(prefix), "sha256": hashlib.sha256(prefix).hexdigest()}


def _identity_matches(jsonl_path: Path, identity: object) -> bool:
    if not isinstance(identity, dict):
        return False
    want_len = identity.get("prefixLen") or 0
    try:
        with jsonl_path.open("rb") as fh:
            prefix = fh.read(want_len)
    except OSError:
        return False
    if len(prefix) != want_len:
        return False  # 캐시 시점보다 짧아짐 → truncate/교체
    return hashlib.sha256(prefix).hexdigest() == identity.get("sha256")


def load_cache(jsonl_path: Path) -> dict:
    """파일에 대응하는 캐시. 미스·손상·버전/식별자 불일치면 빈 캐시.

    identity 는 이번 스캔 시점 기준으로 갱신해 둔다 — 첫 256B 미만이던 파일이
    자란 경우 prefix 를 늘려 잡기 위함(append-only 라 기존 prefix 는 불변).
    """
    identity = _file_identity(jsonl_path)
    p = cache_path_for(jsonl_path)
    try:
        cache = json.loads(p.read_text())
    except (OSError, json.JSONDecodeError):
        return fresh_cache(identity)
    if not isinstance(cache, dict) or cache.get("schemaVersion") != CACHE_SCHEMA_VERSION:
        return fresh_cache(identity)
    if not _identity_matches(jsonl_path, cache.get("identity")):
        return fresh_cache(identity)
    cache["identity"] = identity
    return cache


def save_cache(jsonl_path: Path, cache: dict) -> None:
    """원자적 저장. 실패는 무시 — 캐시는 파생 데이터, 결과에 영향 없음."""
    p = cache_path_for(jsonl_path)
    try:
        p.parent.mkdir(parents=True, exist_ok=True)
        tmp = p.with_suffix(".json.tmp")
        tmp.write_text(json.dumps(cache, ensure_ascii=False, separators=(",", ":")))
        os.replace(tmp, p)
    except OSError:
        pass
