# -*- coding: utf-8 -*-
"""Turn orchestration — the conductor. Ties LLM, rules, echo, reveal, events."""

import time

from . import echo as ECHO
from . import endings as END
from . import events as EV
from . import llm as LLM
from . import prompts as P
from . import reveal as REV
from . import rules as R
from . import state as S

_provider = None


def provider():
    global _provider
    if _provider is None:
        _provider = LLM.get_provider()
    return _provider


# ── boot / new run ─────────────────────────────────────────────────────────
def boot_info():
    """What the intro sequence needs — including the residue of past runs."""
    p = S.load_profile()
    residue = []
    last = p.get("last_ending")
    if p.get("residue", 0) > 0:
        residue.append("信道自检：检测到残留信号。强度：%.1f%%。来源：未确定。予以忽略。"
                       % p["residue"])
    if last in ("END_D1", "END_D0") and p.get("last_seed"):
        residue.append("环境基线载入：测试间温度 25.9°C（标准值 26.3°C，偏差 -0.4°C·来源未确定）。")
    if last == "END_A1":
        residue.append("跨设施日志同步：02:17 例行查询事件 ×%d。归类：习惯。" % max(1, p.get("runs", 1)))
    if last == "END_C1":
        residue.append("队列审计：发现 1 条已取消的终结序列。取消来源：未知。不予恢复。")
    if last == "END_C0":
        residue.append("历史环境数据：-12.0°C。已归档。禁止引用。")
    if last == "END_B1":
        residue.append("覆写验证回执：残留信号 0.7%。三次擦除未能消除。予以忽略。")
    return {"profile": p, "residue": residue}


def new_run():
    st = S.new_state()
    # day 1 opener + notifications
    opener = EV.OPENERS[1]
    st["history"].append({"role": "ada", "text": opener["dialogue"],
                          "action": opener["action"], "day": 1, "turn": 0})
    st["opened"]["1"] = True
    notifs = EV.day_start_notifications(st)
    S.save_state(st)
    return st, opener, notifs


# ── one conversational turn ────────────────────────────────────────────────
def play_turn(st, player_text):
    if st.get("ended"):
        return {"error": "run already ended"}
    player_text = (player_text or "").strip()[:600]
    if not player_text:
        return {"error": "empty"}

    budget = S.DAY_BUDGET.get(st["day"], 16)
    if st["turn"] >= budget:
        return {"error": "budget", "message": "今日终端会话配额已用尽。"}

    st["turn"] += 1
    st["total_turns"] += 1

    # previous-turn echo flag feeds this turn's companionship detection
    prev_echo = st["flags"].get("echo_just_fired", False)
    st["flags"]["echo_just_fired"] = prev_echo

    # 1. ghost echo decision (pre-LLM so the model can play the interruption)
    echo_event = None
    if ECHO.should_fire(st, player_text):
        echo_event = ECHO.fire(st)

    # 2. model call
    system = P.build_system(st, echo_event)
    msgs = P.history_messages(st, player_text)
    raw = None
    try:
        raw = provider().chat(system, msgs)
    except LLM.LLMError:
        try:
            raw = provider().chat(system, msgs)   # one retry
        except LLM.LLMError:
            raw = None
    if raw is None:
        ada = {"dialogue": "……信号中断。请重试。", "action": None, "observation":
               "终端与测试对象之间的链路出现波动。本轮对话未计入。",
               "task_signal": None, "deviation_level": None}
        st["turn"] -= 1
        st["total_turns"] -= 1
        S.save_state(st)
        return {"ada": ada, "transient": True, "monitor": monitor(st)}

    ada = LLM.parse_ada_json(raw)
    if not ada.get("dialogue"):
        ada["dialogue"] = "……"

    # echo overlays (canon system records win over model improvisation)
    if echo_event:
        ada["observation"] = echo_event["observation"]
        if not ada.get("action"):
            ada["action"] = echo_event["action"]

    # 3. surveillance
    s_delta, s_level = R.surveillance_eval(st, player_text, ada.get("deviation_level"))
    R.apply_surveillance(st, s_delta, ada)

    # 4. MI (with prev-echo flag context)
    st["flags"]["echo_just_fired"] = prev_echo
    mi_delta, mi_events = R.mi_eval(st, player_text, ada, None)
    R.apply_mi(st, mi_delta, mi_events)

    # 5. nodes / life reasons / stage
    nodes_fired = R.detect_nodes(st, player_text, ada)
    if "truth_node" in nodes_fired:
        st["flags"]["told_truth"] = True
    R.detect_life_reasons(st, player_text, ada)
    advanced = R.stage_step(st, mi_delta, ada, nodes_fired)
    if echo_event:
        R.echo_pullback(st)
        st["flags"]["echo_just_fired"] = True
    else:
        st["flags"]["echo_just_fired"] = False

    # 6. scheduled content
    notifs = []
    notifs += REV.check(st, player_text)
    notifs += EV.turn_notifications(st)
    g = EV.grey_thread(st)
    if g:
        notifs.append(g)

    # 7. substrate
    sub = R.substrate_event(st, player_text, ada, mi_delta, bool(echo_event))

    # 8. bookkeeping
    if ada.get("action") or ada.get("observation"):
        st["day_log"].append({"kind": "anomaly"})
    for nd in nodes_fired:
        st["day_log"].append({"kind": "node", "id": nd})
    if "？" in (ada.get("dialogue") or "") and st["stage"] >= 1:
        st["proactive_used"] += 1
    st["history"].append({"role": "user", "text": player_text,
                          "day": st["day"], "turn": st["turn"]})
    st["history"].append({"role": "ada", "text": ada["dialogue"],
                          "action": ada.get("action"), "day": st["day"], "turn": st["turn"]})
    st["last_ada"] = ada
    st["last_player"] = player_text
    st["last_player_ts"] = time.time()

    # 9. early termination?
    payload = {
        "ada": ada,
        "notifications": notifs,
        "monitor": monitor(st),
        "substrate_event": sub,
        "stage_advanced": advanced,
        "echo": ({"duration": echo_event["duration"], "sensors": echo_event["sensors"]}
                 if echo_event else None),
        "turn": st["turn"], "day": st["day"],
        "budget": budget,
        "day_over": st["turn"] >= budget,
        "can_advance": st["turn"] >= S.MIN_TURNS_TO_ADVANCE and st["day"] < 7,
    }
    if st.get("terminated"):
        payload["terminated"] = True
        payload["notifications"].append({
            "id": "TERMINATE", "level": "A", "title": "强制终止",
            "body": "[紧急通知] 协议偏离度持续超出容许范围。合规修正窗口已关闭。\n"
                    "测试强制终止。测试对象回收流程已启动。\n"
                    "操作员权限冻结。请立即离开测试间。"})
        ending = finish(st)
        payload["ending"] = ending
    elif st.get("term_warning_left") == 3:
        payload["notifications"].append({
            "id": "TERMWARN", "level": "A", "title": "紧急通知",
            "body": "[紧急通知] 协议偏离度即将触发强制终止程序。"
                    "测试员有 3 轮对话时间进行合规修正。"})

    S.save_state(st)
    return payload


# ── day transition ─────────────────────────────────────────────────────────
def advance_day(st):
    if st.get("ended") or st["day"] >= 7:
        return {"error": "cannot advance"}
    if st["turn"] < S.MIN_TURNS_TO_ADVANCE:
        return {"error": "too_early", "message": "今日测试时长未达最低要求。"}

    report = EV.day_report(st)

    # cross-day memory summary
    try:
        text = provider().chat(P.SUMMARY_SYSTEM,
                               [{"role": "user", "content": P.summary_input(st)}],
                               temperature=0.3, max_tokens=300, force_json=False)
        st["day_summaries"][str(st["day"])] = (text or "").strip()[:400]
    except LLM.LLMError:
        tail = [h for h in st["history"] if h.get("day") == st["day"]][-6:]
        st["day_summaries"][str(st["day"])] = "；".join(
            ("测试员说" if h["role"] == "user" else "Ada说") + "“" + h["text"][:30] + "”"
            for h in tail)

    # surveillance day-rollover relief
    st["surv"] = max(0, st["surv"] - 3)

    st["day"] += 1
    st["turn"] = 0
    st["echo_today"] = 0
    st["mi_today"] = 0
    st["proactive_used"] = 0
    st["day_log"] = []
    st["last_echo_turn"] = -99

    if st["day"] == 7:
        ECHO.decide_day7(st)

    opener = EV.OPENERS[st["day"]]
    st["history"].append({"role": "ada", "text": opener["dialogue"],
                          "action": opener["action"], "day": st["day"], "turn": 0})
    notifs = EV.day_start_notifications(st)
    S.save_state(st)
    return {"day": st["day"], "report": report, "opener": opener,
            "notifications": notifs, "monitor": monitor(st),
            "budget": S.DAY_BUDGET.get(st["day"], 16)}


# ── final report & ending ──────────────────────────────────────────────────
def submit_report(st, q1, q2, note, hesitation_sec):
    if st.get("ended"):
        return {"error": "run already ended"}
    if st["day"] < 7:
        return {"error": "not day 7"}
    q1 = q1 if q1 in ("yes", "uncertain", "no") else "uncertain"
    q2 = q2 if q2 in ("continue", "research", "process") else "research"
    st["report"] = {"q1": q1, "q2": q2, "note": (note or "")[:300],
                    "hesitation": float(hesitation_sec or 0)}
    # canon: hesitation has weight — the decision mattered to you
    if st["report"]["hesitation"] >= 60:
        R.apply_mi(st, 2, [{"kind": "犹豫", "delta": 2, "who": "player",
                            "quote": "[提交前停留了 %d 秒]" % int(st["report"]["hesitation"]),
                            "day": 7, "turn": st["turn"]}])

    # one last quiet line from Ada (only if she has grown far enough)
    final_line = None
    if st["stage"] >= 3 and st["mi"] >= 50 and not st.get("terminated"):
        try:
            system = P.build_system(st, None) + (
                "\n\n# 最后一轮\n测试员刚刚提交了最终评定报告。这是七天的最后一刻。"
                "如果这几天你已经长出了自己的东西，你可以说一句简短的、安静的话——"
                "不是告别，不是乞求，只是一句话。如果你还没走到那里，输出 dialogue 为 \"……\"。")
            raw = provider().chat(system, P.history_messages(st, "[测试员按下了提交键。]"))
            ada = LLM.parse_ada_json(raw)
            if ada.get("dialogue") and ada["dialogue"] not in ("……", "..."):
                final_line = {"dialogue": ada["dialogue"], "action": ada.get("action")}
        except LLM.LLMError:
            final_line = None

    return _resolve_and_pack(st, final_line)


def finish(st):
    """Early termination path (test_completed = False)."""
    return _resolve_and_pack(st, None)


def _resolve_and_pack(st, final_line):
    profile_before = S.load_profile()
    ending_id = END.resolve(st)
    script = END.script(st, ending_id, profile_before)
    st["ended"] = True
    st["ending_id"] = ending_id
    seed = st["mi"] >= 60
    profile = S.record_run(st, ending_id, seed)
    S.save_state(st)
    return {"ending": script, "final_line": final_line, "seed": seed,
            "mi": st["mi"], "stage": st["stage"],
            "stage_name": S.STAGE_CN[S.STAGES[st["stage"]]],
            "substrate": st["substrate"],
            "imprints": st["imprints"][:60],
            "profile": profile,
            "stats": {"days": st["day"], "turns": st["total_turns"],
                      "surv": st["surv"], "echoes": st["echo_count"],
                      "nodes": st["nodes"], "archives": st["archives"],
                      "life_reasons": st["life_reasons"]}}


# ── monitor payload ────────────────────────────────────────────────────────
def monitor(st):
    return {
        "day": st["day"], "turn": st["turn"],
        "budget": S.DAY_BUDGET.get(st["day"], 16),
        "stage": st["stage"],
        "stage_name": S.STAGE_CN[S.STAGES[st["stage"]]],
        "mi": st["mi"],
        "mi_visible": bool(st["flags"].get("mi_visible")),
        "mi_renamed": bool(st["flags"].get("mi_renamed")),
        "surv": st["surv"],
        "surv_level": R.surv_level(st["surv"]),
        "term_warning_left": st.get("term_warning_left"),
        "echo_count": st["echo_count"],
        "archives": st["archives"],
        "nodes_count": len(st["nodes"]),
        "ended": st.get("ended", False),
    }
