# -*- coding: utf-8 -*-
"""
转盘抽奖系统 v7.1 - 双模式 + 音效 + 闪烁
==========================================
确定性模式: 先抽后转，指针精准对准结果
悬念模式:   先转后揭，停下才显示（有紧张感）
音效:       开始/转动/tick/停下（winsound, Windows）
闪烁:       悬念模式最后阶段指针红黑闪烁
单文件 · tkinter · 纯标准库 · 创作者模式锁 · 记录统计只读 · 状态持久化
"""

import tkinter as tk
from tkinter import ttk, colorchooser, messagebox, filedialog, simpledialog
import random
import math
import json
import csv
import os
import time
import hashlib
import traceback
from datetime import datetime
from dataclasses import dataclass, asdict, fields
from typing import List, Dict, Optional, Tuple
from enum import Enum

# 音效（Windows 标准库）
try:
    import winsound
    _HAS_SOUND = True
except ImportError:
    _HAS_SOUND = False


# ============================================================
# 数据模型
# ============================================================

class DrawMode(Enum):
    EQUAL = "等概率"
    WEIGHTED = "加权"
    PITY = "保底"
    MULTI_NO_REPEAT = "多人去重"


class SpinMode(Enum):
    DETERMINISTIC = "确定性(先抽后转)"
    SUSPENSE = "悬念(先转后揭)"


@dataclass
class Prize:
    id: str = ""
    name: str = ""
    color: str = "#FFD700"
    emoji: str = "🎁"
    weight: float = 1.0
    level: str = "普通"
    quantity: int = -1
    remaining: int = -1
    description: str = ""

    def __post_init__(self):
        if self.remaining == -1:
            self.remaining = self.quantity

    def is_available(self) -> bool:
        return self.remaining == -1 or self.remaining > 0

    def consume(self) -> bool:
        if not self.is_available():
            return False
        if self.remaining > 0:
            self.remaining -= 1
        return True

    def to_dict(self) -> dict:
        return asdict(self)

    @classmethod
    def from_dict(cls, d: dict) -> 'Prize':
        allowed = {f.name for f in fields(cls)}
        return cls(**{k: v for k, v in d.items() if k in allowed})


@dataclass
class DrawRecord:
    prize_id: str = ""
    prize_name: str = ""
    timestamp: str = ""
    mode: str = ""
    participant: str = ""
    pity_count: int = 0

    def to_dict(self) -> dict:
        return asdict(self)

    @classmethod
    def from_dict(cls, d: dict) -> 'DrawRecord':
        allowed = {f.name for f in fields(cls)}
        return cls(**{k: v for k, v in d.items() if k in allowed})


# ============================================================
# 抽奖引擎
# ============================================================

class PrizePool:
    def __init__(self, prizes: Optional[List[Prize]] = None):
        self.prizes: List[Prize] = prizes if prizes is not None else []

    def add(self, prize: Prize):
        self.prizes.append(prize)

    def remove(self, prize_id: str) -> bool:
        for i, p in enumerate(self.prizes):
            if p.id == prize_id:
                self.prizes.pop(i)
                return True
        return False

    def get(self, prize_id: str) -> Optional[Prize]:
        for p in self.prizes:
            if p.id == prize_id:
                return p
        return None

    def available(self) -> List[Prize]:
        return [p for p in self.prizes if p.is_available()]

    def to_dict(self) -> dict:
        return {"prizes": [p.to_dict() for p in self.prizes]}

    @classmethod
    def from_dict(cls, d: dict) -> 'PrizePool':
        return cls([Prize.from_dict(pd) for pd in d.get("prizes", [])])

    @classmethod
    def default(cls) -> 'PrizePool':
        colors = ["#FFD700","#C0C0C0","#CD7F32","#FF6B6B","#4ECDC4",
                   "#45B7D1","#96CEB4","#FFEAA7","#DDA0DD","#98D8C8"]
        data = [
            ("一等奖","🏆",1,"传说"),("二等奖","🥈",3,"史诗"),
            ("三等奖","🥉",6,"稀有"),("四等奖","🎖️",12,"罕见"),
            ("五等奖","🎫",20,"普通"),("六等奖","🎟️",30,"普通"),
            ("幸运奖","🍀",50,"普通"),("参与奖","🎈",100,"普通"),
            ("安慰奖","🌟",150,"普通"),("谢谢参与","💨",300,"普通"),
        ]
        return cls([Prize(f"p{i}", n, colors[i%len(colors)], e, w, lvl)
                    for i, (n, e, w, lvl) in enumerate(data)])


class LotteryEngine:
    def __init__(self, pool: Optional[PrizePool] = None, seed: Optional[int] = None):
        self.pool = pool if pool is not None else PrizePool.default()
        self.pity_counter: Dict[str, int] = {}
        self.pity_threshold: int = 50
        self.pity_map: Dict[str, List[str]] = {}
        self.history: List[DrawRecord] = []
        self._rng = random.Random(seed)
        self._drawn_ids: set = set()

    def set_seed(self, seed: int):
        self._rng = random.Random(seed)

    def _weighted_pick(self, available: List[Prize]) -> Prize:
        total = sum(p.weight for p in available)
        if total <= 0:
            return self._rng.choice(available)
        r = self._rng.uniform(0, total)
        cum = 0.0
        for p in available:
            cum += p.weight
            if r <= cum:
                return p
        return available[-1]

    def draw_one(self, mode: DrawMode, participant: str = "") -> Tuple[Prize, DrawRecord]:
        available = self.pool.available()
        if not available:
            raise ValueError("没有可用奖品！")

        if mode == DrawMode.EQUAL:
            prize = self._rng.choice(available)
        elif mode == DrawMode.WEIGHTED:
            prize = self._weighted_pick(available)
        elif mode == DrawMode.PITY:
            picked = False
            for level, pids in self.pity_map.items():
                if self.pity_counter.get(level, 0) >= self.pity_threshold:
                    for pid in pids:
                        p = self.pool.get(pid)
                        if p and p.is_available():
                            prize = p
                            self.pity_counter[level] = 0
                            picked = True
                            break
                    if picked:
                        break
            if not picked:
                prize = self._weighted_pick(available)
            for level in self.pity_map:
                if level not in self.pity_counter:
                    self.pity_counter[level] = 0
                if prize.id not in self.pity_map.get(level, []):
                    self.pity_counter[level] += 1
        elif mode == DrawMode.MULTI_NO_REPEAT:
            if len(self._drawn_ids) >= len(available):
                self._drawn_ids.clear()
            candidates = [p for p in available if p.id not in self._drawn_ids]
            if not candidates:
                candidates = available
            prize = self._rng.choice(candidates)
            self._drawn_ids.add(prize.id)
        else:
            prize = self._rng.choice(available)

        prize.consume()
        record = DrawRecord(
            prize_id=prize.id, prize_name=prize.name,
            timestamp=datetime.now().isoformat(),
            mode=mode.value, participant=participant,
        )
        self.history.append(record)
        return prize, record

    def draw_multi(self, count: int, mode: DrawMode, participant: str = "") -> List[Tuple[Prize, DrawRecord]]:
        return [self.draw_one(mode, participant) for _ in range(count)]

    def reset_pity(self):
        self.pity_counter.clear()

    def export_csv(self, filepath: str):
        with open(filepath, 'w', newline='', encoding='utf-8-sig') as f:
            w = csv.DictWriter(f, fieldnames=["prize_name","timestamp","mode","participant"])
            w.writeheader()
            for r in self.history:
                w.writerow({"prize_name":r.prize_name,"timestamp":r.timestamp,"mode":r.mode,"participant":r.participant})

    def stats(self) -> Dict[str, Dict]:
        if not self.history:
            return {}
        counter = {}
        for r in self.history:
            counter[r.prize_name] = counter.get(r.prize_name, 0) + 1
        total = len(self.history)
        return {n: {"count": c, "percentage": c/total*100} for n, c in counter.items()}


# ============================================================
# 几何
# ============================================================

class WheelGeometry:
    @staticmethod
    def ease_out_quart(t: float) -> float:
        return 1 - (1 - t) ** 4

    @staticmethod
    def polar_to_cartesian(cx, cy, r, angle_rad):
        return cx + r*math.cos(angle_rad), cy + r*math.sin(angle_rad)

    @staticmethod
    def slice_path(cx, cy, r, start_angle, end_angle):
        points = [(cx, cy)]
        steps = max(20, int(abs(end_angle - start_angle) * 30))
        for i in range(steps + 1):
            a = start_angle + (end_angle - start_angle) * i / steps
            points.append(WheelGeometry.polar_to_cartesian(cx, cy, r, a))
        return points


# ============================================================
# 主窗口
# ============================================================

class WheelApp:
    STATE_FILE = "lottery_state.json"
    PRIZE_FILE = "prizes.json"

    def __init__(self, root: tk.Tk):
        self.root = root
        self.root.title("🎡 转盘抽奖系统 v7.1")
        self.root.geometry("1500x900")
        self.root.minsize(1000, 650)

        self.engine = LotteryEngine()

        self.is_admin = False
        self.admin_password_hash = hashlib.sha256("admin123".encode()).hexdigest()

        self._animating = False
        self._anim_start = 0.0
        self._anim_duration = 5000
        self._target_angle = 0.0
        self._rng = random.Random()
        self._pending_results = None
        self._spin_mode = SpinMode.SUSPENSE
        self._blink_job = None
        self.sound_enabled = True
        self.blink_enabled = True

        self._setup_style()
        self._build_ui()
        self._apply_admin_mode()
        self._load_state()
        self._refresh_wheel()
        self._refresh_list()
        self._refresh_stats()
        self._status("就绪（观众模式）- Ctrl+Shift+A 登录创作者模式")
        self.root.protocol("WM_DELETE_WINDOW", self._on_close)

    # ----------------------------------------------------------
    # 音效 & 闪烁
    # ----------------------------------------------------------
    def _beep(self, freq, dur):
        if not self.sound_enabled or not _HAS_SOUND: return
        try:
            winsound.Beep(freq, dur)
        except Exception:
            pass

    def _play_start_sound(self):
        self._beep(400, 300)

    def _play_tick_sound(self):
        self._beep(800, 50)

    def _play_stop_sound(self):
        self._beep(1000, 500)

    def _start_blink(self):
        if not self.blink_enabled: return
        if self._blink_job: return
        def blink():
            try:
                current = self.canvas.itemcget("pointer", "fill")
                new_color = "#FF4444" if current == "#CC0000" else "#CC0000"
                self.canvas.itemconfig("pointer", fill=new_color)
                self._blink_job = self.root.after(150, blink)
            except Exception:
                pass
        blink()

    def _stop_blink(self):
        if self._blink_job:
            self.root.after_cancel(self._blink_job)
            self._blink_job = None
        try:
            self.canvas.itemconfig("pointer", fill="#CC0000")
        except Exception:
            pass

    # ----------------------------------------------------------
    # 状态持久化
    # ----------------------------------------------------------
    def _save_state(self):
        if not hasattr(self, "engine"): return
        try:
            state = {
                "version": 7,
                "prizes": [p.to_dict() for p in self.engine.pool.prizes],
                "history": [r.to_dict() for r in self.engine.history],
                "pity_counter": dict(self.engine.pity_counter),
                "pity_threshold": int(self.engine.pity_threshold),
                "pity_map": {k: list(v) for k, v in self.engine.pity_map.items()},
                "spin_mode": self._spin_mode.value,
                "sound_enabled": bool(self.sound_enabled),
                "blink_enabled": bool(self.blink_enabled),
                "saved_at": datetime.now().isoformat(),
            }
            with open(self.STATE_FILE, "w", encoding="utf-8") as f:
                json.dump(state, f, ensure_ascii=False, indent=2)
            self._status(f"状态已保存 ({len(self.engine.history)} 条记录)")
        except Exception:
            traceback.print_exc()

    def _load_state(self):
        if not hasattr(self, "hist_text") or not hasattr(self, "status_bar"):
            return
        if not os.path.exists(self.STATE_FILE):
            self._load_prizes()
            return
        try:
            with open(self.STATE_FILE, "r", encoding="utf-8") as f:
                state = json.load(f)

            self.engine.pool.prizes = []
            for pd in state.get("prizes", []):
                try: self.engine.pool.prizes.append(Prize.from_dict(pd))
                except Exception: pass

            self.engine.history = []
            for rd in state.get("history", []):
                try: self.engine.history.append(DrawRecord.from_dict(rd))
                except Exception: pass

            self.engine.pity_counter = {k: int(v) for k, v in state.get("pity_counter", {}).items()}
            self.engine.pity_threshold = int(state.get("pity_threshold", 50))
            self.engine.pity_map = {k: v for k, v in state.get("pity_map", {}).items()}

            sm = state.get("spin_mode", SpinMode.SUSPENSE.value)
            self._spin_mode = SpinMode(sm) if sm in [m.value for m in SpinMode] else SpinMode.SUSPENSE
            if hasattr(self, "spin_mode_var"):
                self.spin_mode_var.set(self._spin_mode.value)

            self.sound_enabled = bool(state.get("sound_enabled", True))
            self.blink_enabled = bool(state.get("blink_enabled", True))
            if hasattr(self, "sound_var"): self.sound_var.set(self.sound_enabled)
            if hasattr(self, "blink_var"): self.blink_var.set(self.blink_enabled)

            self.hist_text.delete("1.0", tk.END)
            for r in self.engine.history:
                self._append_history(r)

            self._status(f"已恢复: {len(self.engine.history)} 条记录")
        except Exception:
            traceback.print_exc()
            self._load_prizes()

    def _load_prizes(self):
        try:
            if os.path.exists(self.PRIZE_FILE):
                with open(self.PRIZE_FILE, "r", encoding="utf-8") as f:
                    self.engine.pool = PrizePool.from_dict(json.load(f))
        except Exception:
            pass

    def _is_immutable(self, label: str) -> bool:
        return label in ("清空记录", "记录(只读)", "清空历史", "删除记录")

    # ----------------------------------------------------------
    # 样式 & UI
    # ----------------------------------------------------------
    def _setup_style(self):
        style = ttk.Style()
        style.theme_use("clam")
        style.configure("TLabel", font=("Microsoft YaHei", 10))
        style.configure("TButton", font=("Microsoft YaHei", 10))
        style.configure("Result.TLabel", font=("Microsoft YaHei", 22, "bold"))
        style.configure("Treeview", font=("Microsoft YaHei", 9))
        style.configure("Treeview.Heading", font=("Microsoft YaHei", 10, "bold"))

    def _build_ui(self):
        menubar = tk.Menu(self.root)
        file_menu = tk.Menu(menubar, tearoff=0)
        file_menu.add_command(label="导出记录(CSV)", command=self._export_csv)
        file_menu.add_separator()
        file_menu.add_command(label="退出", command=self._on_close)
        menubar.add_cascade(label="文件", menu=file_menu)

        self.prize_menu = tk.Menu(menubar, tearoff=0)
        self.prize_menu.add_command(label="添加奖品", command=self._add_prize)
        self.prize_menu.add_command(label="编辑选中", command=self._edit_prize)
        self.prize_menu.add_command(label="删除选中", command=self._delete_prize)
        self.prize_menu.add_separator()
        self.prize_menu.add_command(label="重置库存", command=self._reset_stock)
        self.prize_menu.add_separator()
        self.prize_menu.add_command(label="导入奖品(JSON)", command=self._import_prizes)
        self.prize_menu.add_command(label="导出奖品(JSON)", command=self._export_prizes)
        menubar.add_cascade(label="奖品", menu=self.prize_menu)

        self.tool_menu = tk.Menu(menubar, tearoff=0)
        self.tool_menu.add_command(label="批量模拟", command=self._batch_simulate)
        self.tool_menu.add_command(label="统计面板", command=self._show_stats)
        self.tool_menu.add_command(label="概率分布图", command=self._plot_chart)
        self.tool_menu.add_separator()
        self.tool_menu.add_command(label="重置保底", command=self._reset_pity)
        self.tool_menu.add_command(label="记录(只读)", state=tk.DISABLED)
        menubar.add_cascade(label="工具", menu=self.tool_menu)

        admin_menu = tk.Menu(menubar, tearoff=0)
        admin_menu.add_command(label="管理员登录", command=self._admin_login)
        admin_menu.add_command(label="退出创作者模式", command=self._admin_logout)
        admin_menu.add_separator()
        admin_menu.add_command(label="修改密码", command=self._change_password)
        menubar.add_cascade(label="管理", menu=admin_menu)

        help_menu = tk.Menu(menubar, tearoff=0)
        help_menu.add_command(label="关于", command=self._about)
        menubar.add_cascade(label="帮助", menu=help_menu)
        self.root.config(menu=menubar)

        main = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
        main.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)

        # 左侧
        left = ttk.Frame(main)
        main.add(left, weight=3)

        self.canvas = tk.Canvas(left, width=500, height=500, bg="#1a1a2e", highlightthickness=0)
        self.canvas.pack(pady=10)
        self.canvas.create_polygon(240,20,260,20,250,50, fill="#FF4444", outline="#CC0000", width=2, tags="pointer")
        self.canvas.create_oval(235,45,265,55, fill="#CC0000", tags="pointer")

        ctrl = ttk.Frame(left)
        ctrl.pack(pady=5)

        ttk.Label(ctrl, text="模式:").pack(side=tk.LEFT, padx=2)
        self.mode_var = tk.StringVar(value="加权")
        ttk.Combobox(ctrl, textvariable=self.mode_var, width=12,
                     values=[m.value for m in DrawMode], state="readonly").pack(side=tk.LEFT, padx=2)

        ttk.Label(ctrl, text="转盘:").pack(side=tk.LEFT, padx=(15,2))
        self.spin_mode_var = tk.StringVar(value=self._spin_mode.value)
        self.spin_mode_cb = ttk.Combobox(ctrl, textvariable=self.spin_mode_var, width=18,
                                         values=[m.value for m in SpinMode], state="readonly")
        self.spin_mode_cb.pack(side=tk.LEFT, padx=2)
        self.spin_mode_cb.bind("<<ComboboxSelected>>", self._on_spin_mode_change)

        ttk.Label(ctrl, text="次数:").pack(side=tk.LEFT, padx=(15,2))
        self.count_var = tk.IntVar(value=1)
        ttk.Spinbox(ctrl, from_=1, to=100, textvariable=self.count_var, width=5).pack(side=tk.LEFT, padx=2)

        ttk.Label(ctrl, text="参与者:").pack(side=tk.LEFT, padx=2)
        self.part_var = tk.StringVar()
        ttk.Entry(ctrl, textvariable=self.part_var, width=10).pack(side=tk.LEFT, padx=2)

        self.draw_btn = ttk.Button(ctrl, text="🎯 开始抽奖 (F9)", command=self._start_draw)
        self.draw_btn.pack(side=tk.LEFT, padx=10)
        self.stop_btn = ttk.Button(ctrl, text="⏹ 停止", command=self._stop_anim, state=tk.DISABLED)
        self.stop_btn.pack(side=tk.LEFT)

        self.result_var = tk.StringVar(value="等待抽奖...")
        ttk.Label(left, textvariable=self.result_var, style="Result.TLabel").pack(pady=10)

        # 右侧
        right = ttk.Frame(main)
        main.add(right, weight=2)
        tabs = ttk.Notebook(right)
        tabs.pack(fill=tk.BOTH, expand=True)

        prize_tab = ttk.Frame(tabs)
        tabs.add(prize_tab, text="奖品池")
        self.tree = ttk.Treeview(prize_tab, columns=("name","level","weight","remain","emoji"),
                                  show="headings", height=15)
        for col, txt, w in [("name","名称",100),("level","等级",60),("weight","权重",60),("remain","剩余",60),("emoji","图标",40)]:
            self.tree.heading(col, text=txt)
            self.tree.column(col, width=w)
        self.tree.pack(fill=tk.BOTH, expand=True, side=tk.LEFT)
        ttk.Scrollbar(prize_tab, orient=tk.VERTICAL, command=self.tree.yview).pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.configure(yscrollcommand=self.tree.yview)

        self.tree_btn = ttk.Frame(prize_tab)
        self.tree_btn.pack(fill=tk.X, pady=2)
        self.btn_add = ttk.Button(self.tree_btn, text="➕ 添加", command=self._add_prize, state=tk.DISABLED)
        self.btn_add.pack(side=tk.LEFT, padx=2)
        self.btn_edit = ttk.Button(self.tree_btn, text="✏️ 编辑", command=self._edit_prize, state=tk.DISABLED)
        self.btn_edit.pack(side=tk.LEFT, padx=2)
        self.btn_del = ttk.Button(self.tree_btn, text="🗑️ 删除", command=self._delete_prize, state=tk.DISABLED)
        self.btn_del.pack(side=tk.LEFT, padx=2)

        hist_tab = ttk.Frame(tabs)
        tabs.add(hist_tab, text="抽奖记录")
        self.hist_text = tk.Text(hist_tab, height=20, wrap=tk.WORD, font=("Consolas", 9))
        self.hist_text.pack(fill=tk.BOTH, expand=True, side=tk.LEFT)
        ttk.Scrollbar(hist_tab, orient=tk.VERTICAL, command=self.hist_text.yview).pack(side=tk.RIGHT, fill=tk.Y)
        self.hist_text.configure(yscrollcommand=self.hist_text.yview)

        stat_tab = ttk.Frame(tabs)
        tabs.add(stat_tab, text="统计")
        self.stat_text = tk.Text(stat_tab, height=20, wrap=tk.WORD, font=("Consolas", 9))
        self.stat_text.pack(fill=tk.BOTH, expand=True)

        set_tab = ttk.Frame(tabs)
        tabs.add(set_tab, text="设置")
        si = ttk.Frame(set_tab); si.pack(pady=20)
        ttk.Label(si, text="保底阈值:").grid(row=0, column=0, sticky=tk.W, pady=5)
        self.pity_var = tk.IntVar(value=50)
        self.pity_sp = ttk.Spinbox(si, from_=1, to=999, textvariable=self.pity_var, width=10)
        self.pity_sp.grid(row=0, column=1, padx=5)
        ttk.Label(si, text="动画时长(ms):").grid(row=1, column=0, sticky=tk.W, pady=5)
        self.anim_var = tk.IntVar(value=5000)
        self.anim_sp = ttk.Spinbox(si, from_=1000, to=15000, textvariable=self.anim_var, width=10)
        self.anim_sp.grid(row=1, column=1, padx=5)
        ttk.Label(si, text="随机种子:").grid(row=2, column=0, sticky=tk.W, pady=5)
        self.seed_var = tk.StringVar()
        self.seed_entry = ttk.Entry(si, textvariable=self.seed_var, width=12)
        self.seed_entry.grid(row=2, column=1, padx=5)
        self.seed_btn = ttk.Button(si, text="应用种子", command=self._apply_seed)
        self.seed_btn.grid(row=2, column=2, padx=5)
        self.pity_btn = ttk.Button(si, text="重置所有保底计数", command=self._reset_pity)
        self.pity_btn.grid(row=3, column=0, columnspan=2, pady=10)

        # 音效 & 闪烁开关
        ttk.Label(si, text="音效:").grid(row=4, column=0, sticky=tk.W, pady=5)
        self.sound_var = tk.BooleanVar(value=self.sound_enabled)
        ttk.Checkbutton(si, variable=self.sound_var, text="启用",
                        command=lambda: setattr(self, 'sound_enabled', self.sound_var.get())).grid(row=4, column=1, sticky=tk.W)
        ttk.Label(si, text="指针闪烁:").grid(row=5, column=0, sticky=tk.W, pady=5)
        self.blink_var = tk.BooleanVar(value=self.blink_enabled)
        ttk.Checkbutton(si, variable=self.blink_var, text="启用",
                        command=lambda: setattr(self, 'blink_enabled', self.blink_var.get())).grid(row=5, column=1, sticky=tk.W)

        self.status_bar = ttk.Label(self.root, text="就绪", relief=tk.SUNKEN, anchor=tk.W)
        self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)

        self.root.bind("<F9>", lambda e: self._start_draw())
        self.root.bind("<Control-Shift-A>", lambda e: self._admin_login())

    # ----------------------------------------------------------
    # 转盘模式切换
    # ----------------------------------------------------------
    def _on_spin_mode_change(self, event=None):
        new_val = self.spin_mode_var.get()
        for m in SpinMode:
            if m.value == new_val:
                self._spin_mode = m
                break
        hint = "先抽奖，转盘精准停到对应扇区" if self._spin_mode == SpinMode.DETERMINISTIC \
               else "转盘先空转，停下才揭晓结果"
        self._status(f"已切换为「{new_val}」- {hint}")

    # ----------------------------------------------------------
    # 管理员
    # ----------------------------------------------------------
    def _admin_login(self):
        pwd = simpledialog.askstring("管理员登录", "请输入密码:", show="*")
        if not pwd: return
        if hashlib.sha256(pwd.encode()).hexdigest() == self.admin_password_hash:
            self.is_admin = True
            self._apply_admin_mode()
            messagebox.showinfo("成功", "已进入创作者模式\n（抽奖记录与统计仍为只读）")
        else:
            messagebox.showerror("失败", "密码错误")

    def _admin_logout(self):
        self.is_admin = False
        self._apply_admin_mode()

    def _change_password(self):
        old = simpledialog.askstring("修改密码", "当前密码:", show="*")
        if not old or hashlib.sha256(old.encode()).hexdigest() != self.admin_password_hash:
            messagebox.showerror("错误", "当前密码不正确"); return
        new = simpledialog.askstring("修改密码", "新密码:", show="*")
        if not new: return
        confirm = simpledialog.askstring("修改密码", "确认新密码:", show="*")
        if new != confirm:
            messagebox.showerror("错误", "两次输入不一致"); return
        self.admin_password_hash = hashlib.sha256(new.encode()).hexdigest()
        messagebox.showinfo("成功", "密码已修改")

    def _apply_admin_mode(self):
        mode = tk.NORMAL if self.is_admin else tk.DISABLED
        for btn in [self.btn_add, self.btn_edit, self.btn_del]:
            btn.config(state=mode)
        for w in [self.pity_sp, self.anim_sp, self.seed_entry, self.seed_btn, self.pity_btn]:
            w.config(state=mode)
        for m in (self.prize_menu, self.tool_menu):
            for i in range(m.index("end") + 1):
                try:
                    lbl = m.entrycget(i, "label")
                    m.entryconfig(i, state=tk.DISABLED if self._is_immutable(lbl) else mode)
                except tk.TclError:
                    pass

    # ----------------------------------------------------------
    # 转盘 & 动画
    # ----------------------------------------------------------
    def _refresh_wheel(self, angle_offset=0.0):
        self.canvas.delete("slice", "label", "center")
        cx, cy, r = 250, 250, 220
        prizes = self.engine.pool.prizes
        if not prizes:
            self.canvas.create_text(cx, cy, text="暂无奖品", fill="white", font=("Microsoft YaHei", 16))
            return
        n = len(prizes)
        sa = 2 * math.pi / n
        for i, p in enumerate(prizes):
            s = angle_offset + i * sa
            e = s + sa
            pts = WheelGeometry.slice_path(cx, cy, r, s, e)
            self.canvas.create_polygon([c for pt in pts for c in pt],
                                       fill=p.color, outline="white", width=2, tags="slice")
            mid = s + sa / 2
            lx, ly = WheelGeometry.polar_to_cartesian(cx, cy, r * 0.65, mid)
            self.canvas.create_text(lx, ly, text=f"{p.emoji}\n{p.name}",
                                    fill="black", font=("Microsoft YaHei", 8, "bold"),
                                    justify=tk.CENTER, tags="label")
        self.canvas.create_oval(cx-25,cy-25,cx+25,cy+25, fill="#FFD700", outline="#FFA500", width=3, tags="center")
        self.canvas.create_text(cx, cy, text="GO", fill="#333", font=("Microsoft YaHei", 12, "bold"), tags="center")

    def _start_draw(self):
        if self._animating or not self.engine.pool.prizes:
            return

        mode = DrawMode(self.mode_var.get())
        count = self.count_var.get()
        participant = self.part_var.get().strip()
        n = len(self.engine.pool.prizes)
        sa = 2 * math.pi / n

        # 悬念模式至少 8 秒，确定性用用户设定
        if self._spin_mode == SpinMode.SUSPENSE:
            self._anim_duration = max(self.anim_var.get(), 8000)
        else:
            self._anim_duration = self.anim_var.get()

        # 两种模式：统一在开始前抽奖
        try:
            self._pending_results = self.engine.draw_multi(count, mode, participant)
        except Exception:
            traceback.print_exc()
            return

        if self._spin_mode == SpinMode.SUSPENSE:
            # 悬念：随机停
            extra_spins = 5 + self._rng.randint(0, 4)
            self._target_angle = extra_spins * 2 * math.pi + self._rng.uniform(0, 2 * math.pi)
        else:
            # 确定性：对准结果
            first = self._pending_results[0][0]
            idx = self.engine.pool.prizes.index(first)
            base_offset = -(idx + 0.5) * sa
            extra_spins = (5 + self._rng.randint(0, 3)) * 2 * math.pi
            self._target_angle = extra_spins - base_offset

        self._anim_start = time.time() * 1000
        self._animating = True
        self.draw_btn.config(state=tk.DISABLED)
        self.stop_btn.config(state=tk.NORMAL)

        if self._spin_mode == SpinMode.SUSPENSE:
            self.result_var.set("转动中...")

        self._play_start_sound()
        self._anim_frame()

    def _anim_frame(self):
        if not self._animating: return
        elapsed = time.time()*1000 - self._anim_start
        p = min(elapsed / self._anim_duration, 1.0)
        eased = WheelGeometry.ease_out_quart(p)
        angle = eased * self._target_angle
        self._refresh_wheel(angle_offset=angle % (2 * math.pi))

        # 悬念模式：最后 20% 闪烁，最后 5% tick
        if self._spin_mode == SpinMode.SUSPENSE:
            if p > 0.8 and not self._blink_job:
                self._start_blink()
            if p > 0.95:
                self._play_tick_sound()

        if p < 1.0:
            self.root.after(16, self._anim_frame)
        else:
            self._animating = False
            self._stop_blink()
            self._play_stop_sound()
            self._finish_draw()

    def _stop_anim(self):
        if self._animating:
            self._animating = False
            self._stop_blink()
            self._play_stop_sound()
            self._finish_draw()

    def _finish_draw(self):
        self._stop_blink()
        self.draw_btn.config(state=tk.NORMAL)
        self.stop_btn.config(state=tk.DISABLED)

        if not self._pending_results:
            return

        names = [p.name for p, _ in self._pending_results]
        self.result_var.set(
            f"🎉 {names[0]} 🎉" if len(names) == 1
            else f"{', '.join(names[:3])}{'...' if len(names) > 3 else ''}"
        )
        for _, rec in self._pending_results:
            self._append_history(rec)
        self._pending_results = None
        self._refresh_list()
        self._refresh_stats()
        self._save_state()

    # ----------------------------------------------------------
    # 奖品管理
    # ----------------------------------------------------------
    def _add_prize(self):
        if not self.is_admin: return
        dlg = PrizeDialog(self.root, title="添加奖品")
        if dlg.result:
            dlg.result.id = f"p{int(time.time()*1000)}"
            self.engine.pool.add(dlg.result)
            self._refresh_list(); self._refresh_wheel(); self._save_state()

    def _edit_prize(self):
        if not self.is_admin: return
        sel = self.tree.selection()
        if not sel: return
        idx = self.tree.index(sel[0])
        dlg = PrizeDialog(self.root, prize=self.engine.pool.prizes[idx], title="编辑奖品")
        if dlg.result:
            self.engine.pool.prizes[idx] = dlg.result
            self._refresh_list(); self._refresh_wheel(); self._save_state()

    def _delete_prize(self):
        if not self.is_admin: return
        sel = self.tree.selection()
        if not sel: return
        idx = self.tree.index(sel[0])
        if messagebox.askyesno("确认", f"删除「{self.engine.pool.prizes[idx].name}」？"):
            self.engine.pool.prizes.pop(idx)
            self._refresh_list(); self._refresh_wheel(); self._save_state()

    def _reset_stock(self):
        if not self.is_admin: return
        for p in self.engine.pool.prizes:
            p.remaining = p.quantity
        self._refresh_list(); self._save_state()

    # ----------------------------------------------------------
    # 刷新
    # ----------------------------------------------------------
    def _refresh_list(self):
        for item in self.tree.get_children():
            self.tree.delete(item)
        for p in self.engine.pool.prizes:
            self.tree.insert("", tk.END, values=(p.name, p.level, f"{p.weight:.1f}",
                                                  "∞" if p.remaining < 0 else str(p.remaining), p.emoji))

    def _refresh_stats(self):
        self.stat_text.delete("1.0", tk.END)
        stats = self.engine.stats()
        if not stats:
            self.stat_text.insert(tk.END, "暂无数据")
            return
        self.stat_text.insert(tk.END, f"总抽奖次数: {len(self.engine.history)}\n\n")
        for name, info in sorted(stats.items(), key=lambda x: x[1]["count"], reverse=True):
            bar = "█" * int(info["percentage"] / 2)
            self.stat_text.insert(tk.END, f"{name:15s} {info['count']:4d} 次  {info['percentage']:5.1f}%  {bar}\n")

    def _append_history(self, record: DrawRecord):
        ts = record.timestamp[11:19] if len(record.timestamp) > 19 else ""
        line = f"[{ts}] {record.participant + ' ' if record.participant else ''}{record.prize_name} ({record.mode})\n"
        self.hist_text.insert(tk.END, line)
        self.hist_text.see(tk.END)

    # ----------------------------------------------------------
    # 工具
    # ----------------------------------------------------------
    def _batch_simulate(self):
        if not self.is_admin: return
        count = simpledialog.askinteger("批量模拟", "次数:", minvalue=1, maxvalue=100000, initialvalue=1000)
        if count:
            self.engine.draw_multi(count, DrawMode(self.mode_var.get()))
            self._save_state(); self._refresh_stats()

    def _show_stats(self):
        self._refresh_stats()

    def _plot_chart(self):
        if not self.is_admin: return
        try:
            import matplotlib; matplotlib.use('Agg')
            import matplotlib.pyplot as plt
            from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
            stats = self.engine.stats()
            if not stats: return
            fig, ax = plt.subplots(figsize=(6,4))
            ax.barh(list(stats.keys()), [stats[n]["count"] for n in stats])
            ax.set_xlabel("次数"); ax.set_title("奖品分布"); plt.tight_layout()
            w = tk.Toplevel(self.root); w.title("统计图表"); w.geometry("600x450")
            FigureCanvasTkAgg(fig, master=w).get_tk_widget().pack(fill=tk.BOTH, expand=True)
        except ImportError:
            messagebox.showinfo("提示", "需要 matplotlib")

    def _reset_pity(self):
        if not self.is_admin: return
        self.engine.reset_pity(); self._save_state()

    def _apply_seed(self):
        if not self.is_admin: return
        s = self.seed_var.get().strip()
        if s:
            self.engine.set_seed(int(hashlib.md5(s.encode()).hexdigest()[:8], 16))
        else:
            self.engine.set_seed(None)

    # ----------------------------------------------------------
    # 导入/导出/关闭
    # ----------------------------------------------------------
    def _export_csv(self):
        if not self.engine.history: return
        path = filedialog.asksaveasfilename(defaultextension=".csv")
        if path: self.engine.export_csv(path)

    def _import_prizes(self):
        if not self.is_admin: return
        path = filedialog.askopenfilename(filetypes=[("JSON","*.json")])
        if path:
            with open(path, "r", encoding="utf-8") as f:
                self.engine.pool = PrizePool.from_dict(json.load(f))
            self._refresh_list(); self._refresh_wheel(); self._save_state()

    def _export_prizes(self):
        if not self.is_admin: return
        path = filedialog.asksaveasfilename(defaultextension=".json")
        if path:
            with open(path, "w", encoding="utf-8") as f:
                json.dump(self.engine.pool.to_dict(), f, ensure_ascii=False, indent=2)

    def _on_close(self):
        self._save_state()
        self.root.destroy()

    def _status(self, msg: str):
        if hasattr(self, "status_bar"):
            self.status_bar.config(text=msg)

    def _about(self):
        messagebox.showinfo("关于",
            "🎡 转盘抽奖系统 v7.1\n\n"
            "• 双模式可切换:\n"
            "  - 确定性: 先抽后转，视觉与结果一致\n"
            "  - 悬念: 先转后揭，停下才揭晓\n"
            "• 音效 & 指针闪烁（可开关）\n"
            "• 加权/等概率/保底/多人去重\n"
            "• 创作者模式锁\n"
            "• 抽奖记录 & 统计 永久只读\n"
            "• 状态自动持久化\n\n© 2026"
            "• 注：指针在-90度")


# ============================================================
# 奖品编辑对话框
# ============================================================

class PrizeDialog(tk.Toplevel):
    def __init__(self, parent, prize=None, title="奖品"):
        super().__init__(parent)
        self.title(title); self.geometry("400x420")
        self.transient(parent); self.grab_set(); self.result = None
        self._build(prize); self.wait_window()

    def _build(self, prize):
        f = ttk.Frame(self, padding=10); f.pack(fill=tk.BOTH, expand=True)
        self.name_var = tk.StringVar(value=prize.name if prize else "")
        self.emoji_var = tk.StringVar(value=prize.emoji if prize else "🎁")
        self.color_var = tk.StringVar(value=prize.color if prize else "#FFD700")
        self.weight_var = tk.DoubleVar(value=prize.weight if prize else 1.0)
        self.level_var = tk.StringVar(value=prize.level if prize else "普通")
        self.qty_var = tk.IntVar(value=prize.quantity if prize and prize.quantity>0 else -1)

        ttk.Label(f, text="名称:").grid(row=0, column=0, sticky=tk.W, pady=3)
        ttk.Entry(f, textvariable=self.name_var, width=25).grid(row=0, column=1, pady=3)
        ttk.Label(f, text="图标:").grid(row=1, column=0, sticky=tk.W, pady=3)
        ttk.Entry(f, textvariable=self.emoji_var, width=10).grid(row=1, column=1, sticky=tk.W, pady=3)
        ttk.Label(f, text="颜色:").grid(row=2, column=0, sticky=tk.W, pady=3)
        cf = ttk.Frame(f); cf.grid(row=2, column=1, sticky=tk.W, pady=3)
        self.color_btn = tk.Button(cf, text="  ", bg=self.color_var.get(), width=3, command=self._pick_color)
        self.color_btn.pack(side=tk.LEFT)
        ttk.Label(cf, textvariable=self.color_var).pack(side=tk.LEFT, padx=5)
        ttk.Label(f, text="权重:").grid(row=3, column=0, sticky=tk.W, pady=3)
        ttk.Spinbox(f, from_=0.1, to=9999, increment=0.1, textvariable=self.weight_var, width=10).grid(row=3, column=1, sticky=tk.W, pady=3)
        ttk.Label(f, text="等级:").grid(row=4, column=0, sticky=tk.W, pady=3)
        ttk.Combobox(f, textvariable=self.level_var, width=12, values=["普通","罕见","稀有","史诗","传说"]).grid(row=4, column=1, sticky=tk.W, pady=3)
        ttk.Label(f, text="总数量:").grid(row=5, column=0, sticky=tk.W, pady=3)
        ttk.Spinbox(f, from_=-1, to=99999, textvariable=self.qty_var, width=10).grid(row=5, column=1, sticky=tk.W, pady=3)
        ttk.Label(f, text="(-1=无限)").grid(row=5, column=2, sticky=tk.W)

        btns = ttk.Frame(f); btns.grid(row=6, column=0, columnspan=3, pady=20)
        ttk.Button(btns, text="确定", command=self._ok).pack(side=tk.LEFT, padx=10)
        ttk.Button(btns, text="取消", command=self.destroy).pack(side=tk.LEFT)

    def _pick_color(self):
        c = colorchooser.askcolor(initialcolor=self.color_var.get())[1]
        if c: self.color_var.set(c); self.color_btn.config(bg=c)

    def _ok(self):
        if not self.name_var.get().strip():
            messagebox.showwarning("警告", "名称不能为空"); return
        self.result = Prize(
            id="", name=self.name_var.get().strip(),
            color=self.color_var.get(), emoji=self.emoji_var.get().strip() or "🎁",
            weight=self.weight_var.get(), level=self.level_var.get(),
            quantity=self.qty_var.get(),
        )
        self.destroy()


# ============================================================
# 入口
# ============================================================

def main():
    root = tk.Tk()
    app = WheelApp(root)
    root.mainloop()

if __name__ == "__main__":
    main()