"""M2:人在环中。把"真人座位"接进引擎 —— **不改引擎**,只加一个路由 provider:

真人座位走 HumanController(命令行 / 测试脚本 / 将来的 web),其余座位走 AI(MindProvider);
真人输入超时或无效 → 回落到 AI/随机托管,世界绝不卡住。
真人若是狼,wolf_discuss 时会看到狼私聊频道里 AI 队友先发的提议(信息差已由 perception 保证)。
"""

from __future__ import annotations

import threading
import time
from typing import Optional, Protocol

from genesis.obs.logging_setup import get_logger
from genesis.werewolf import perception
from genesis.werewolf import state as S
from genesis.werewolf.boardmemory import BoardMemory
from genesis.werewolf.engine import RandomProvider

logger = get_logger("werewolf.human")


# 警长定序的两个离散选项(choice 动作:value 提交、label 展示)
SPEECH_DIR_OPTIONS = [
    {"value": "cw", "label": "从我左手边起 →(座位顺位)"},
    {"value": "ccw", "label": "从我右手边起 ←(座位逆位)"},
]


def _parse_int(s: Optional[str]) -> Optional[int]:
    s = (s or "").strip()
    return int(s) if s.lstrip("-").isdigit() else None


def _yes(s: Optional[str]) -> bool:
    return (s or "").strip().lower() in ("y", "yes", "是", "救", "好", "1", "true")


class HumanController(Protocol):
    """真人交互通道。kind:'seat'(从 candidates 选座位/可跳)| 'yesno' | 'text'。None=超时/跳过→AI 托管。"""
    def show(self, text: str) -> None: ...
    def ask(self, prompt: str, kind: str = "text", candidates: Optional[list] = None) -> Optional[str]: ...


class CLIHuman:
    """命令行通道:终端里文字试玩。"""
    def show(self, text: str) -> None:
        print("\n" + text)

    def ask(self, prompt: str, kind: str = "text", candidates: Optional[list] = None) -> Optional[str]:
        try:
            return input(prompt + " ")
        except EOFError:
            return None


class ScriptedHuman:
    """测试用:按队列回答;队列空了返回 None(→ AI 托管)。记录展示过的文本备查。"""
    def __init__(self, answers: Optional[list] = None) -> None:
        self.answers = list(answers or [])
        self.shown: list[str] = []

    def show(self, text: str) -> None:
        self.shown.append(text)

    def ask(self, prompt: str, kind: str = "text", candidates: Optional[list] = None) -> Optional[str]:
        return str(self.answers.pop(0)) if self.answers else None


class WebHumanController:
    """网页通道:ask 阻塞对局线程,等 /api/action 提交答案(或超时→None→AI 托管)。
    线程安全:对局线程调 ask 阻塞在 Event 上;HTTP 线程调 submit 唤醒、调 pending 取当前待输入。"""
    def __init__(self, timeout: float = 90.0) -> None:
        self.timeout = timeout
        self._lock = threading.Lock()
        self._event = threading.Event()
        self._answer: Optional[str] = None
        self._pending: Optional[dict] = None
        self._deadline = 0.0
        self._seq = 0                                # 每次 ask 自增:前端据此区分"同文本的不同回合",杜绝按 prompt 去重导致漏弹/卡死

    def show(self, text: str) -> None:
        pass                                         # 网页从 frame 自渲染对局板,无需 controller 推文本

    def ask(self, prompt: str, kind: str = "text", candidates: Optional[list] = None) -> Optional[str]:
        with self._lock:
            self._seq += 1
            self._pending = {"id": self._seq, "prompt": prompt, "kind": kind, "candidates": candidates}
            self._deadline = time.monotonic() + self.timeout   # 倒计时截止点(供前端公示)
            self._answer = None
            self._event.clear()
        got = self._event.wait(self.timeout)         # 阻塞等真人输入
        with self._lock:
            ans = self._answer if got else None
            self._pending = None
        if not got:
            logger.info("[web] 真人 %ss 未响应 → AI 托管该决策", int(self.timeout))
        return ans

    def submit(self, answer: str) -> bool:
        """HTTP 线程:提交真人答案,唤醒对局线程。返回是否确有待输入。"""
        with self._lock:
            if self._pending is None:
                return False
            self._answer = "" if answer is None else str(answer)
            self._event.set()
            return True

    def pending(self) -> Optional[dict]:
        with self._lock:
            if not self._pending:
                return None
            p = dict(self._pending)
            p["seconds_left"] = max(0, int(self._deadline - time.monotonic()))   # 公示倒计时
            return p

    def release(self) -> None:
        """玩家退出/座位被夺回时强制结束当前等待:ask 立刻返回 None → AI 托管,绝不卡住对局。"""
        with self._lock:
            self._answer = None
            self._pending = None
            self._event.set()


class HumanSeatProvider:
    """实现 DecisionProvider:human_seat 走 controller,其余委托给内层 AI provider。"""

    def __init__(self, human_seat: int, ai, controller: HumanController, roles) -> None:
        self.human_seat = human_seat
        self.ai = ai
        self.controller = controller
        self.roles = roles

    def _is_human(self, seat: int) -> bool:
        return seat == self.human_seat

    def _show_board(self, state) -> None:
        v = perception.view(state, self.human_seat, self.roles)
        self.controller.show(BoardMemory(self.human_seat).render(v, transcript_tail=30))

    # ── DecisionProvider 接口 ──
    def wolf_discuss(self, state, seat, candidates):
        if not self._is_human(seat):
            return self.ai.wolf_discuss(state, seat, candidates)
        self._show_board(state)                              # 含狼私聊频道:看得到队友先发的提议
        t = _parse_int(self.controller.ask(f"【狼队私聊】你提议刀谁?", "seat", candidates))
        msg = self.controller.ask("【狼队私聊】给队友说一句:", "text") or ""
        if t not in candidates:
            t = candidates[0] if candidates else None
        return {"target": t, "message": msg}

    def decide_kill(self, state, seat, candidates):          # 引擎已改用 wolf_discuss,这里仅为接口完整
        if not self._is_human(seat):
            return self.ai.decide_kill(state, seat, candidates)
        t = _parse_int(self.controller.ask("【你·刀人】今晚刀谁?", "seat", candidates))
        return t if t in candidates else (candidates[0] if candidates else None)

    def decide_check(self, state, seat, candidates):
        if not self._is_human(seat):
            return self.ai.decide_check(state, seat, candidates)
        self._show_board(state)
        t = _parse_int(self.controller.ask("【你·查验】今晚验谁?", "seat", candidates))
        return t if t in candidates else (candidates[0] if candidates else None)

    def decide_witch(self, state, seat, knife, can_save, can_poison, poison_candidates):
        if not self._is_human(seat):
            return self.ai.decide_witch(state, seat, knife, can_save, can_poison, poison_candidates)
        self._show_board(state)
        save = _yes(self.controller.ask(f"【你·女巫】今夜 {knife} 号倒在刀下,用解药救吗?", "yesno")) if can_save else False
        poison = None
        if can_poison:
            p = _parse_int(self.controller.ask("【你·女巫】要用毒药毒谁?(可不毒)", "seat", poison_candidates))
            poison = p if p in poison_candidates else None
        return {"save": save, "poison": poison}

    def decide_run_sheriff(self, state, seat):
        if not self._is_human(seat):
            return self.ai.decide_run_sheriff(state, seat)
        self._show_board(state)
        return _yes(self.controller.ask("【你·竞选】上警竞选警长吗?", "yesno"))

    def decide_speech_dir(self, state, seat):
        if not self._is_human(seat):
            return self.ai.decide_speech_dir(state, seat)
        self._show_board(state)
        ans = self.controller.ask("【你·警长定序】从哪边开始发言?(你将压轴最后发言)", "choice", SPEECH_DIR_OPTIONS)
        return {"direction": ans if ans in ("cw", "ccw") else "cw"}

    def decide_withdraw(self, state, seat):
        if not self._is_human(seat):
            return self.ai.decide_withdraw(state, seat)
        self._show_board(state)
        return _yes(self.controller.ask("【你·退水】竞选发言后,是否退水退出警长竞选?", "yesno"))

    def decide_badge_pass(self, state, seat, candidates):
        if not self._is_human(seat):
            return self.ai.decide_badge_pass(state, seat, candidates)
        self._show_board(state)
        t = _parse_int(self.controller.ask("【你·移交警徽】你(警长)出局,把警徽交给谁?(跳过=撕毁)", "seat", candidates))
        return t if t in candidates else None

    def decide_explode(self, state, seat):
        if not self._is_human(seat):
            return self.ai.decide_explode(state, seat)
        self._show_board(state)
        return _yes(self.controller.ask("【你·自爆】轮到你发言,是否自爆?(掀桌亮狼身份,中止今天直接入夜)", "yesno"))

    def speak(self, state, seat):
        if not self._is_human(seat):
            return self.ai.speak(state, seat)
        self._show_board(state)
        return self.controller.ask("【你·发言】轮到你了,说点什么:", "text") or "(我先听听大家的)"

    def decide_vote(self, state, seat, candidates):
        if not self._is_human(seat):
            return self.ai.decide_vote(state, seat, candidates)
        self._show_board(state)
        prompt = ("【你·警徽投票】投谁当警长?" if state.phase == S.SHERIFF_VOTE
                  else "【你·投票】投谁出局?(可弃票)")
        t = _parse_int(self.controller.ask(prompt, "seat", candidates))
        return t if t in candidates else None

    def decide_hunter_shoot(self, state, seat, candidates):
        if not self._is_human(seat):
            return self.ai.decide_hunter_shoot(state, seat, candidates)
        self._show_board(state)
        t = _parse_int(self.controller.ask("【你·开枪】开枪带走谁?(可放弃)", "seat", candidates))
        return t if t in candidates else None

    def digest_day(self, state, seat):
        if not self._is_human(seat):
            return self.ai.digest_day(state, seat)
        return None                                          # 真人自己记,不用 LLM 压缩


class MultiHumanProvider:
    """多真人版路由 provider:每个真人座位挂一个 WebHumanController;attach/detach 动态切换 真人↔AI
    —— 这正是『退出→AI补位、回来→夺回原座位』的落点。无 controller 的座位一律走 AI。线程安全(并发投票)。"""

    def __init__(self, ai, roles) -> None:
        self.ai = ai
        self.roles = roles
        self._controllers: dict[int, WebHumanController] = {}
        self._lock = threading.Lock()

    def attach(self, seat: int, controller: WebHumanController) -> None:
        with self._lock:
            self._controllers[seat] = controller

    def detach(self, seat: int) -> None:
        with self._lock:
            c = self._controllers.pop(seat, None)
        if c:
            c.release()                                      # 唤醒其可能正在等待的决策 → AI 接手

    def has_human(self, seat: int) -> bool:
        with self._lock:
            return seat in self._controllers

    def human_seats(self) -> set:
        with self._lock:
            return set(self._controllers)

    def _ctrl(self, seat: int) -> Optional[WebHumanController]:
        with self._lock:
            return self._controllers.get(seat)

    def _show(self, c, state, seat: int) -> None:
        c.show(BoardMemory(seat).render(perception.view(state, seat, self.roles), transcript_tail=30))

    # ── DecisionProvider 接口(逐座位路由:有 controller=真人,否则 AI)──
    def wolf_discuss(self, state, seat, candidates):
        c = self._ctrl(seat)
        if c is None:
            return self.ai.wolf_discuss(state, seat, candidates)
        self._show(c, state, seat)
        t = _parse_int(c.ask("【狼队私聊】你提议刀谁?", "seat", candidates))
        msg = c.ask("【狼队私聊】给队友说一句:", "text") or ""
        if t not in candidates:
            t = candidates[0] if candidates else None
        return {"target": t, "message": msg}

    def decide_kill(self, state, seat, candidates):
        c = self._ctrl(seat)
        if c is None:
            return self.ai.decide_kill(state, seat, candidates)
        t = _parse_int(c.ask("【你·刀人】今晚刀谁?", "seat", candidates))
        return t if t in candidates else (candidates[0] if candidates else None)

    def decide_check(self, state, seat, candidates):
        c = self._ctrl(seat)
        if c is None:
            return self.ai.decide_check(state, seat, candidates)
        self._show(c, state, seat)
        t = _parse_int(c.ask("【你·查验】今晚验谁?", "seat", candidates))
        return t if t in candidates else (candidates[0] if candidates else None)

    def decide_witch(self, state, seat, knife, can_save, can_poison, poison_candidates):
        c = self._ctrl(seat)
        if c is None:
            return self.ai.decide_witch(state, seat, knife, can_save, can_poison, poison_candidates)
        self._show(c, state, seat)
        save = _yes(c.ask(f"【你·女巫】今夜 {knife} 号倒在刀下,用解药救吗?", "yesno")) if can_save else False
        poison = None
        if can_poison:
            p = _parse_int(c.ask("【你·女巫】要用毒药毒谁?(可不毒)", "seat", poison_candidates))
            poison = p if p in poison_candidates else None
        return {"save": save, "poison": poison}

    def decide_run_sheriff(self, state, seat):
        c = self._ctrl(seat)
        if c is None:
            return self.ai.decide_run_sheriff(state, seat)
        self._show(c, state, seat)
        return _yes(c.ask("【你·竞选】上警竞选警长吗?", "yesno"))

    def decide_speech_dir(self, state, seat):
        c = self._ctrl(seat)
        if c is None:
            return self.ai.decide_speech_dir(state, seat)
        self._show(c, state, seat)
        ans = c.ask("【你·警长定序】从哪边开始发言?(你将压轴最后发言)", "choice", SPEECH_DIR_OPTIONS)
        return {"direction": ans if ans in ("cw", "ccw") else "cw"}

    def decide_withdraw(self, state, seat):
        c = self._ctrl(seat)
        if c is None:
            return self.ai.decide_withdraw(state, seat)
        self._show(c, state, seat)
        return _yes(c.ask("【你·退水】竞选发言后,是否退水退出警长竞选?", "yesno"))

    def decide_badge_pass(self, state, seat, candidates):
        c = self._ctrl(seat)
        if c is None:
            return self.ai.decide_badge_pass(state, seat, candidates)
        self._show(c, state, seat)
        t = _parse_int(c.ask("【你·移交警徽】你(警长)出局,把警徽交给谁?(跳过=撕毁)", "seat", candidates))
        return t if t in candidates else None

    def decide_explode(self, state, seat):
        c = self._ctrl(seat)
        if c is None:
            return self.ai.decide_explode(state, seat)
        self._show(c, state, seat)
        return _yes(c.ask("【你·自爆】轮到你发言,是否自爆?(掀桌亮狼身份,中止今天直接入夜)", "yesno"))

    def speak(self, state, seat):
        c = self._ctrl(seat)
        if c is None:
            return self.ai.speak(state, seat)
        self._show(c, state, seat)
        return c.ask("【你·发言】轮到你了,说点什么:", "text") or "(我先听听大家的)"

    def decide_vote(self, state, seat, candidates):
        c = self._ctrl(seat)
        if c is None:
            return self.ai.decide_vote(state, seat, candidates)
        self._show(c, state, seat)
        prompt = ("【你·警徽投票】投谁当警长?" if state.phase == S.SHERIFF_VOTE
                  else "【你·投票】投谁出局?(可弃票)")
        t = _parse_int(c.ask(prompt, "seat", candidates))
        return t if t in candidates else None

    def decide_hunter_shoot(self, state, seat, candidates):
        c = self._ctrl(seat)
        if c is None:
            return self.ai.decide_hunter_shoot(state, seat, candidates)
        self._show(c, state, seat)
        t = _parse_int(c.ask("【你·开枪】开枪带走谁?(可放弃)", "seat", candidates))
        return t if t in candidates else None

    def digest_day(self, state, seat):
        if self._ctrl(seat) is None:
            return self.ai.digest_day(state, seat)
        return None                                          # 真人座位不做 LLM 复盘
