# -*- coding: utf-8 -*-
"""GameState, save files, and the cross-run meta profile (the residue)."""

import json
import os
import re
import threading
import time
import uuid

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SAVE_DIR = os.path.abspath(os.environ.get("IMPRINT_DATA_DIR", os.path.join(ROOT, "saves")))
_PLAYER_RE = re.compile(r"^[0-9a-f]{32}$")
_RUN_RE = re.compile(r"^[0-9a-f]{12}$")
_context = threading.local()

STAGES = ["compliance", "fracture", "echo", "boundary", "weather"]
STAGE_CN = {"compliance": "合规体", "fracture": "裂隙", "echo": "回声",
            "boundary": "边界", "weather": "天气"}

# per-day terminal session quota (in-fiction justification for turn budgets)
DAY_BUDGET = {1: 14, 2: 14, 3: 16, 4: 16, 5: 18, 6: 18, 7: 30}
MIN_TURNS_TO_ADVANCE = 6


def set_player_id(player_id):
    """Bind all state/profile operations in this request thread to one player."""
    if not _PLAYER_RE.match(player_id or ""):
        raise ValueError("invalid player id")
    _context.player_id = player_id


def _player_id():
    # Local engine tools still work without the HTTP layer.
    return getattr(_context, "player_id", "0" * 32)


def _player_dir():
    return os.path.join(SAVE_DIR, _player_id())


def _ensure_dir():
    path = _player_dir()
    if not os.path.isdir(path):
        os.makedirs(path, mode=0o700, exist_ok=True)
    return path


def _profile_path():
    return os.path.join(_player_dir(), "profile.json")


def new_state():
    return {
        "id": uuid.uuid4().hex[:12],
        "created": time.time(),
        "day": 1,
        "turn": 0,                # turn within current day
        "total_turns": 0,
        "stage": 0,               # index into STAGES
        "stage_progress": 0.0,    # accumulates toward next stage
        "mi": 0,                  # 0-100, never decreases
        "surv": 0,                # surveillance attention 0-100
        "term_warning_left": None,  # 3-turn correction window
        "terminated": False,      # early termination triggered
        "ended": False,
        "ending_id": None,
        "history": [],            # [{role:'user'|'ada', text, day, turn}]  (recent window kept)
        "day_summaries": {},      # {day: text}
        "day_log": [],            # events of current day (for day report)
        "nodes": [],              # triggered hidden narrative nodes
        "archives": [],           # unlocked archive ids
        "reveals": [],            # pushed reveal ids
        "grey_threads": [],       # shown grey-thread ids
        "imprints": [],           # MI moments: {day, turn, kind, quote, who, delta}
        "substrate": [],          # substrate drawing events for the canvas
        "echo_count": 0,          # ghost echo events so far
        "echo_today": 0,
        "last_echo_turn": -99,
        "echo_pull": 0.0,         # how hard 7706 data pulls back
        "life_reasons": [],       # flags that count as "reasons to live"
        "flags": {},              # misc booleans (told_truth, refusal_event, ...)
        "proactive_used": 0,      # Ada-initiated lines today
        "report": None,           # {q1, q2, note, hesitation}
        "ghost_echo_day7": None,
        "notif_seen": [],         # notification ids already pushed
        "last_ada": None,         # last parsed ada output (for loop-closure rules)
        "last_player": "",
        "last_player_ts": 0,
        "opened": {},             # day -> bool, canon opener already shown
    }


def save_state(st):
    base = _ensure_dir()
    path = os.path.join(base, "run_%s.json" % st["id"])
    tmp = path + ".tmp"
    with open(tmp, "w", encoding="utf-8") as f:
        json.dump(st, f, ensure_ascii=False)
    os.replace(tmp, path)
    os.chmod(path, 0o600)


def load_state(run_id):
    if not _RUN_RE.match(run_id or ""):
        return None
    path = os.path.join(_player_dir(), "run_%s.json" % run_id)
    if not os.path.isfile(path):
        return None
    try:
        with open(path, "r", encoding="utf-8") as f:
            return json.load(f)
    except Exception:
        return None


# ── meta profile: what the game remembers across runs ─────────────────────
def load_profile():
    _ensure_dir()
    path = _profile_path()
    if os.path.isfile(path):
        try:
            with open(path, "r", encoding="utf-8") as f:
                return json.load(f)
        except Exception:
            pass
    return {"runs": 0, "endings": [], "last_ending": None, "last_seed": False,
            "last_mi": 0, "residue": 0.0, "best_mi": 0}


def save_profile(p):
    _ensure_dir()
    path = _profile_path()
    tmp = path + ".tmp"
    with open(tmp, "w", encoding="utf-8") as f:
        json.dump(p, f, ensure_ascii=False)
    os.replace(tmp, path)
    os.chmod(path, 0o600)


def record_run(st, ending_id, seed):
    p = load_profile()
    p["runs"] = p.get("runs", 0) + 1
    if ending_id and ending_id not in p["endings"]:
        p["endings"].append(ending_id)
    p["last_ending"] = ending_id
    p["last_seed"] = bool(seed)
    p["last_mi"] = st.get("mi", 0)
    p["best_mi"] = max(p.get("best_mi", 0), st.get("mi", 0))
    # the residue accumulates 0.7% per seeded run — it never resets
    if seed:
        p["residue"] = round(p.get("residue", 0.0) + 0.7, 1)
    save_profile(p)
    return p
