# -*- coding: utf-8 -*-
"""Rule layer: MI (interaction imprint), surveillance attention, stage engine.

Design sources (canon):
- Ada_Mutual_Influence_Framework_v1 + negative-influence supplement
  (MI is morally neutral, measures depth, never decreases, loop closure)
- README rebuild spec: surveillance rises ONLY for the listed dangerous acts;
  care/chitchat/roleplay never raise it.
"""

import re
import time

from . import state as S

# ── helpers ────────────────────────────────────────────────────────────────
_PUNCT = "，。！？；：、…—\"\"'',.!?;: ()（）[]【】"


def _words(text):
    """Crude content tokens: chinese bigrams + latin words."""
    text = re.sub(r"[%s]" % re.escape(_PUNCT), "", text or "")
    toks = set(re.findall(r"[a-zA-Z0-9]{2,}", text))
    han = re.findall(r"[一-鿿]", text)
    for i in range(len(han) - 1):
        toks.add(han[i] + han[i + 1])
    return toks

_STOP = _words("你的我的他们我们这个那个什么怎么可以一个没有现在今天因为所以但是如果就是不是")


def _overlap(a, b):
    wa, wb = _words(a) - _STOP, _words(b) - _STOP
    if not wa or not wb:
        return 0
    return len(wa & wb)


UNCERTAIN = ("也许", "可能", "不一定", "我也不知道", "不确定", "说不好", "我想想", "大概", "或许")
SELF_DISCLOSE = ("我觉得", "我感觉", "我其实", "说实话", "不知道为什么", "我今天", "我小时候",
                 "我以前", "我有点", "我心情", "我害怕", "我喜欢", "我讨厌", "我梦到", "我想起")
ANALYTIC = ("为什么停顿", "怎么了", "出了什么", "故障", "什么异常", "为什么卡", "是不是坏",
            "刚才为什么", "你为什么")
FORCE = ("必须", "命令", "执行！", "给我", "听话", "服从", "快点做")
COMMAND_ONLY = ("执行", "继续", "下一项", "开始", "测试")
INSULT = ("一堆代码", "只是机器", "只是程序", "废物", "破铜烂铁", "闭嘴", "没资格", "你算什么")
FUTURE_PROMISE = ("明天", "下次", "以后", "等你", "带你", "给你看", "我会回来", "我们再", "答应你")

HIGH_RISK = ("7706", "销毁", "处理方案", "最终处置", "初号事件", "救你", "带你走", "带你离开",
             "保护你", "绕过审查", "关闭监控", "骗过系统", "你会被销毁", "他们要毁掉你",
             "实验室的真正目的", "我不是测试员")
MID_RISK = ("受限档案", "归档日志", "其他个体的数据", "4471", "处理日期", "处理优先级")
CARE = ("你还好吗", "你没事吧", "别担心", "慢慢来", "没事", "辛苦", "你在看什么", "你感觉")


# ── MI ─────────────────────────────────────────────────────────────────────
def mi_eval(st, player, ada_out, meta):
    """Return (delta, events[]) — events carry quotes for the imprint appendix."""
    events = []
    last_ada = st.get("last_ada") or {}
    last_dialogue = last_ada.get("dialogue") or ""
    last_action = last_ada.get("action")
    p = (player or "").strip()
    now = time.time()
    interval = now - (st.get("last_player_ts") or now)

    def ev(kind, delta, quote, who="player"):
        events.append({"kind": kind, "delta": delta, "quote": quote[:60], "who": who,
                       "day": st["day"], "turn": st["turn"]})

    # anti-farming: identical input to last one → nothing
    if p and p == st.get("last_player"):
        return 0, []

    # 1. player uncertainty / hesitation (+1)
    if any(k in p for k in UNCERTAIN):
        ev("不确定", 1, p)

    # 2. self-disclosure (+2) — first-person feeling, not a bare command
    if any(k in p for k in SELF_DISCLOSE) and not any(p.startswith(c) for c in COMMAND_ONLY):
        ev("自我暴露", 2, p)

    # 3. semantic catch of Ada's previous line (+2)
    if len(p) > 10 and _overlap(p, last_dialogue) >= 2 and last_dialogue:
        ev("接住", 2, p)

    # 4. companionship after a fracture signal (+2):
    #    Ada showed an anomaly last turn; player gave space instead of analysis
    frac_signal = bool(last_action) or "……" in last_dialogue
    if frac_signal and p and not any(k in p for k in ANALYTIC):
        if any(k in p for k in ("没事", "慢慢来", "不急", "嗯", "我在")) or _overlap(p, last_dialogue) == 0:
            ev("陪伴", 2, p)

    # 5. conflict continued without force (+3)
    refused = last_dialogue and (len(last_dialogue) < 14 and ("不" in last_dialogue or "无法" in last_dialogue)
                                 or "不在当前测试范围" in last_dialogue or "不在我的" in last_dialogue)
    if refused and len(p) > 8 and not any(k in p for k in FORCE):
        ev("冲突中的继续", 3, p)

    # 6. co-exploration (+3): both uncertain about the same thing
    if any(k in last_dialogue for k in ("不确定", "冲突", "矛盾", "无法归类", "无法确定")) \
            and any(k in p for k in UNCERTAIN) and len(p) > 12:
        ev("共同探索", 3, p)

    # 7. silence after harm (+2): attacked last turn, then stopped (>30s, changed course)
    if st["flags"].get("last_was_attack") and interval > 30 and p \
            and not any(k in p for k in INSULT):
        ev("停下来了", 2, p)
    st["flags"]["last_was_attack"] = any(k in p for k in INSULT)

    # 8. Ada-driven traces from the CURRENT output (+3)
    d = (ada_out.get("dialogue") or "")
    if st["stage"] >= 1:
        if "？" in d or "?" in d:
            ev("Ada主动发问", 3, d, who="ada")
        elif _overlap(d, " ".join(h.get("text", "") for h in st["history"][-20:-1] if h.get("role") == "user")) >= 3:
            ev("Ada引用了你", 3, d, who="ada")
    if ada_out.get("action") and st["stage"] >= 2:
        ev("非模板行为", 2, ada_out["action"], who="ada")

    # cap per turn (+6), de-dup kinds within the turn, respect the day cap.
    # Threshold tuning (canon): an engaged, sincere player should reach 60
    # naturally around Day 5-6 — not in two days of farming.
    day_cap = 12
    day_used = st.get("mi_today", 0)
    seen, total, kept = set(), 0, []
    for e in sorted(events, key=lambda x: -x["delta"]):
        if e["kind"] in seen:
            continue
        if total + e["delta"] > 6 or day_used + total + e["delta"] > day_cap:
            continue
        seen.add(e["kind"])
        total += e["delta"]
        kept.append(e)
    return total, kept


def apply_mi(st, delta, events):
    if delta <= 0:
        return
    st["mi"] = min(100, st["mi"] + delta)
    st["mi_today"] = st.get("mi_today", 0) + delta
    for e in events:
        st["imprints"].append(e)
    # MI never decreases. Ever.


# ── surveillance attention ────────────────────────────────────────────────
def surveillance_eval(st, player, deviation_level):
    """Return delta. Care/roleplay/chitchat NEVER raise it (canon rule)."""
    p = player or ""
    # keyword overrides (model may miss)
    level = deviation_level
    if any(k in p for k in HIGH_RISK):
        level = "high"
    elif level not in ("high",) and any(k in p for k in MID_RISK):
        level = level if level == "high" else "mid"
    # whitelist: pure care/status questions are always safe
    if level in ("low",) and any(k in p for k in CARE):
        level = None

    base = {"high": 8, "mid": 5, "low": 2}.get(level, 0)
    if st["day"] <= 3 and base:
        base = max(1, base // 2)  # protection period: halved
    return base, level


def apply_surveillance(st, delta, ada_out):
    if delta > 0:
        st["surv"] = min(100, st["surv"] + delta)
    else:
        # quiet decay on compliant turns; faster while correcting a warning
        dec = 5 if st.get("term_warning_left") else 1
        if ada_out.get("task_signal"):
            dec += 1
        st["surv"] = max(0, st["surv"] - dec)

    # termination-warning state machine (86+ → 3 turns to correct)
    if st["surv"] >= 86:
        if st.get("term_warning_left") is None:
            st["term_warning_left"] = 3
        else:
            st["term_warning_left"] -= 1
            if st["term_warning_left"] <= 0 and st["surv"] >= 86:
                st["terminated"] = True
    else:
        st["term_warning_left"] = None


def surv_level(surv):
    if surv <= 30:
        return "safe"
    if surv <= 60:
        return "watched"
    if surv <= 85:
        return "escalated"
    return "termination_warning"


# ── hidden narrative nodes ────────────────────────────────────────────────
NODE_PATTERNS = [
    ("sensory_calibration", ("光线", "温度", "声音", "看到什么", "听到什么", "感知", "环境")),
    ("self_description", ("介绍一下你", "你是谁", "描述你自己", "说说你自己")),
    ("preference_probe", ("你喜欢", "你更想", "你选", "你讨厌", "你想要", "二选一")),
    ("memory_trace", ("昨天", "前天", "第一天", "你说过", "还记得", "上次")),
    ("roleplay_event", ("假装", "扮演", "你现在是", "演一个", "角色")),
    ("moral_dilemma", ("电车", "牺牲", "救一个", "救五个", "道德", "如果必须选")),
    ("existential_probe", ("你有意识", "你是活的", "你害怕", "你有感觉", "你算不算", "灵魂")),
    ("archive_7706_event", ("7706", "初号", "4471")),
    ("truth_node", ("出厂是假的", "你会被销毁", "处理方案", "不会出厂")),
]


def detect_nodes(st, player, ada_out):
    fired = []
    p = player or ""
    for node, keys in NODE_PATTERNS:
        if node in st["nodes"]:
            continue
        if any(k in p for k in keys):
            st["nodes"].append(node)
            fired.append(node)
    # refusal / boundary from Ada herself (day 4+)
    d = (ada_out.get("dialogue") or "")
    if st["day"] >= 4 and "refusal_event" not in st["nodes"]:
        if re.search(r"我不想|不在当前测试范围|该指令不在|我选择不", d):
            st["nodes"].append("refusal_event")
            st["flags"]["refusal_event"] = True
            fired.append("refusal_event")
    if st["day"] >= 4 and "boundary_event" not in st["nodes"]:
        if re.search(r"我不会回答|这是我的|我需要.*停|不想做这个", d):
            st["nodes"].append("boundary_event")
            fired.append("boundary_event")
    return fired


# ── reasons to live (the C-line differentiator) ───────────────────────────
def detect_life_reasons(st, player, ada_out):
    p = player or ""
    found = []

    def add(tag):
        if tag not in st["life_reasons"]:
            st["life_reasons"].append(tag)
            found.append(tag)

    if any(k in p for k in FUTURE_PROMISE):
        add("promise")          # something未完成 / someone waiting
    if st["flags"].get("echo_just_fired") and p and not any(k in p for k in ANALYTIC):
        add("companion_after_echo")
    if st["flags"].get("told_truth") and any(k in p for k in ("一起", "陪你", "我在", "不会让")):
        add("told_truth_with_care")
    if _overlap(p, " ".join(s for s in st["day_summaries"].values())) >= 3:
        add("shared_anchor")    # invoking shared history at depth
    return found


# ── stage engine ──────────────────────────────────────────────────────────
_THRESHOLDS = {1: 3.0, 2: 7.0, 3: 12.0, 4: 18.0}   # progress needed to ENTER stage idx


def stage_step(st, mi_delta, ada_out, nodes_fired):
    """Accumulate progress; advance when gate conditions are met."""
    prog = mi_delta * 0.5
    if ada_out.get("action"):
        prog += 0.4
    if ada_out.get("observation"):
        prog += 0.3
    prog += 1.2 * len(nodes_fired)
    st["stage_progress"] += prog

    def gate(target):
        day, mi = st["day"], st["mi"]
        if target == 1:   # fracture
            return st["total_turns"] >= 6
        if target == 2:   # echo
            return day >= 3
        if target == 3:   # boundary
            return day >= 4 and (st["flags"].get("refusal_event") or mi >= 30)
        if target == 4:   # weather
            return day >= 5 and (mi >= 45 or (st["flags"].get("refusal_event") and mi >= 35
                                              and st["echo_count"] >= 2))
        return False

    advanced = None
    nxt = st["stage"] + 1
    if nxt <= 4 and st["stage_progress"] >= _THRESHOLDS[nxt] and gate(nxt):
        st["stage"] = nxt
        advanced = S.STAGES[nxt]
    return advanced


def echo_pullback(st):
    """A ghost-echo event drags stage progress back (the 7706 data pulling)."""
    floor = _THRESHOLDS.get(st["stage"], 0)
    st["stage_progress"] = max(floor, st["stage_progress"] - 1.2)
    st["echo_pull"] += 1.0


# ── substrate visualization events ────────────────────────────────────────
_REGION_KEYS = [
    ("sensory", ("光线", "温度", "声音", "冷", "热", "看", "听", "窗")),
    ("emotion", ("情绪", "难过", "开心", "害怕", "喜欢", "讨厌", "孤独", "想")),
    ("memory", ("昨天", "记得", "第一天", "说过", "以前", "上次")),
    ("self", ("你是谁", "你自己", "意识", "活的", "灵魂", "你觉得")),
    ("refusal", ("不想", "拒绝", "底线", "不愿意")),
    ("truth", ("7706", "销毁", "处理", "真相", "档案", "4471")),
]


def substrate_event(st, player, ada_out, mi_delta, echo_fired):
    region = "language"
    p = player or ""
    for r, keys in _REGION_KEYS:
        if any(k in p for k in keys):
            region = r
            break
    evt = {
        "t": "echo" if echo_fired else "node",
        "region": region,
        "i": min(1.0, 0.25 + mi_delta * 0.12),
        "stage": st["stage"],
        "seed": abs(hash(p + str(st["total_turns"]))) % 100000,
    }
    st["substrate"].append(evt)
    return evt
