"""Soma v2:躯体模型——饭点驱动的饥饿、昼夜节律的精力、恐惧标量(纯函数,可单测锁形)。

设计要点(docs/agent-runtime-v2.md §1.6):
- 饥饿不线性累积:由"距上一餐时长"决定基线,饭点窗口(08/13/19±1h)抬升;吃一顿管 5–6 小时;
- 精力 = 昼夜节律 + 睡眠债,深夜困、白天缓回,不是匀速流失;
- 恐惧:事件冲击瞬间抬升,~40 模拟分钟半衰;恐惧 >0.5 抑制饥饿与困意(肾上腺素);
- 需求只有过阈值才进 prompt(`prompt_lines`),且以"候选动作"方式参与决策(`candidates`),不再每拍喊饿。
"""

from __future__ import annotations

import math
from dataclasses import dataclass, field

MEAL_WINDOWS = ((7.0, 9.0), (12.0, 14.0), (18.0, 20.0))
FEAR_HALF_LIFE_H = 0.66  # 恐惧半衰期 ~40 模拟分钟


def hunger_curve(hours_since_meal: float, hour_of_day: float) -> float:
    """饥饿基线:餐后 3h 内≈0,3–7h 缓升,9h+ 饱和;饭点窗口 +0.18。"""
    base = 1.0 / (1.0 + math.exp(-(hours_since_meal - 5.5) * 0.9))  # 5.5h 处过半
    in_window = any(lo <= hour_of_day <= hi for lo, hi in MEAL_WINDOWS)
    return min(1.0, base + (0.18 if in_window and hours_since_meal > 2.5 else 0.0))


def fatigue_curve(hour_of_day: float, hours_awake: float) -> float:
    """困意 = 昼夜节律(23–06 高)+ 清醒时长债(16h 起每小时 +0.05)。"""
    h = hour_of_day
    circadian = 0.55 * (1.0 if (h >= 23.0 or h < 5.0) else 0.5 if (22.0 <= h < 23.0 or 5.0 <= h < 6.5) else 0.0)
    debt = max(0.0, hours_awake - 16.0) * 0.05
    return min(1.0, circadian + debt)


@dataclass
class Soma:
    """某个 agent 的躯体状态。所有时间为模拟小时。"""

    last_meal_h: float = 19.5     # 上一餐时刻(默认开局前刚吃过晚饭)
    woke_at_h: float = 8.0        # 最近醒来时刻
    fear: float = 0.0
    _fear_at_h: float = field(default=0.0, repr=False)

    # —— 事件钩子 ——
    def ate(self, now_h: float) -> None:
        self.last_meal_h = now_h

    def slept(self, woke_h: float) -> None:
        self.woke_at_h = woke_h

    def shock(self, now_h: float, amount: float) -> None:
        """恐惧冲击(目击尸体 1.0 / 惨叫 0.7 / 停电 0.4…),先衰减旧值再叠加。"""
        self.fear = min(1.0, self._decayed_fear(now_h) + amount)
        self._fear_at_h = now_h

    # —— 读数(均带恐惧抑制)——
    def _decayed_fear(self, now_h: float) -> float:
        dt = max(0.0, now_h - self._fear_at_h)
        return self.fear * (0.5 ** (dt / FEAR_HALF_LIFE_H))

    def snapshot(self, now_h: float) -> dict:
        fear = self._decayed_fear(now_h)
        suppress = 1.0 - 0.7 * fear if fear > 0.5 else 1.0  # 肾上腺素:大恐惧压食欲困意
        hunger = hunger_curve(now_h - self.last_meal_h, now_h % 24.0) * suppress
        fatigue = fatigue_curve(now_h % 24.0, now_h - self.woke_at_h) * suppress
        return {"hunger": round(hunger, 3), "fatigue": round(fatigue, 3), "fear": round(fear, 3)}

    def prompt_lines(self, now_h: float) -> list[str]:
        """只把越过阈值的躯体感受写进 prompt(克制,绝不刷屏)。"""
        s = self.snapshot(now_h)
        lines = []
        if s["fear"] >= 0.55:
            lines.append("你心跳得厉害,手心全是汗——恐惧攫住了你。")
        elif s["fear"] >= 0.3:
            lines.append("你心里发毛,警觉地留意着四周的动静。")
        if s["hunger"] >= 0.65:
            lines.append("你已经很久没吃东西,胃里空得发慌。")
        if s["fatigue"] >= 0.7:
            lines.append("你困得眼皮发沉,撑不了多久了。")
        return lines

    def candidates(self, now_h: float) -> list[str]:
        """L0 候选注入:需求过阈值时,把对应动作放进候选(而非命令)。"""
        s = self.snapshot(now_h)
        out = []
        if s["hunger"] >= 0.65:
            out.append("去厨房弄点吃的")
        if s["fatigue"] >= 0.7 and s["fear"] < 0.5:
            out.append("回自己房间睡觉")
        return out
