"""
模拟投票 / 调查系统
功能：
  1. 创建投票主题 + 多个选项
  2. 参与投票（每人每主题限投一票，按用户名区分）
  3. 实时查看投票结果（数字 + 柱状图 + 饼图）
  4. 查看历史投票主题
数据持久化：SQLite (votes.db)
"""

import os
import sqlite3
import tkinter as tk
from tkinter import ttk, messagebox, simpledialog
from datetime import datetime
from collections import Counter

import matplotlib
matplotlib.use("TkAgg")
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg


DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "votes.db")


# ----------------------------- 数据库层 -----------------------------
class _ConnWrapper:
    """包装共享内存连接，close() 设为 no-op，避免连接被关闭后无法复用。"""
    def __init__(self, conn):
        self._conn = conn
    def cursor(self): return self._conn.cursor()
    def commit(self): return self._conn.commit()
    def close(self): return None
    def __getattr__(self, name):
        return getattr(self._conn, name)


_MEM_CONN = None


def get_conn():
    global _MEM_CONN
    if DB_PATH == ":memory:":
        if _MEM_CONN is None:
            _MEM_CONN = sqlite3.connect("file::memory:?cache=shared", uri=True)
        return _ConnWrapper(_MEM_CONN)
    return sqlite3.connect(DB_PATH)


def init_db():
    conn = get_conn()
    cur = conn.cursor()
    cur.executescript("""
        CREATE TABLE IF NOT EXISTS polls (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL,
            question TEXT,
            created_by TEXT,
            created_at TEXT NOT NULL
        );
        CREATE TABLE IF NOT EXISTS options (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            poll_id INTEGER NOT NULL,
            text TEXT NOT NULL,
            FOREIGN KEY (poll_id) REFERENCES polls(id) ON DELETE CASCADE
        );
        CREATE TABLE IF NOT EXISTS votes (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            poll_id INTEGER NOT NULL,
            option_id INTEGER NOT NULL,
            voter TEXT NOT NULL,
            voted_at TEXT NOT NULL,
            UNIQUE(poll_id, voter),
            FOREIGN KEY (poll_id) REFERENCES polls(id) ON DELETE CASCADE,
            FOREIGN KEY (option_id) REFERENCES options(id) ON DELETE CASCADE
        );
        CREATE INDEX IF NOT EXISTS idx_votes_poll ON votes(poll_id);
    """)
    conn.commit()
    if DB_PATH != ":memory:":
        conn.close()


def create_poll(title, question, creator, options):
    """创建投票主题，返回 poll_id"""
    conn = get_conn()
    cur = conn.cursor()
    cur.execute(
        "INSERT INTO polls(title, question, created_by, created_at) VALUES (?,?,?,?)",
        (title, question, creator, datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
    )
    poll_id = cur.lastrowid
    for opt in options:
        cur.execute("INSERT INTO options(poll_id, text) VALUES (?,?)", (poll_id, opt))
    conn.commit()
    conn.close()
    return poll_id


def list_polls():
    conn = get_conn()
    cur = conn.cursor()
    cur.execute("SELECT id, title, created_at FROM polls ORDER BY id DESC")
    rows = cur.fetchall()
    conn.close()
    return rows


def get_poll(poll_id):
    conn = get_conn()
    cur = conn.cursor()
    cur.execute("SELECT id, title, question FROM polls WHERE id=?", (poll_id,))
    poll = cur.fetchone()
    cur.execute("SELECT id, text FROM options WHERE poll_id=? ORDER BY id", (poll_id,))
    options = cur.fetchall()
    conn.close()
    return poll, options


def has_voted(poll_id, voter):
    conn = get_conn()
    cur = conn.cursor()
    cur.execute("SELECT 1 FROM votes WHERE poll_id=? AND voter=?", (poll_id, voter))
    row = cur.fetchone()
    conn.close()
    return row is not None


def cast_vote(poll_id, option_id, voter):
    conn = get_conn()
    cur = conn.cursor()
    try:
        cur.execute(
            "INSERT INTO votes(poll_id, option_id, voter, voted_at) VALUES (?,?,?,?)",
            (poll_id, option_id, voter, datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
        )
        conn.commit()
        return True, "投票成功，感谢参与！"
    except sqlite3.IntegrityError:
        return False, "您已参与过该主题的投票，无法重复投票。"
    finally:
        conn.close()


def tally(poll_id):
    """返回 (total, [(option_text, count), ...])"""
    conn = get_conn()
    cur = conn.cursor()
    cur.execute("SELECT COUNT(*) FROM votes WHERE poll_id=?", (poll_id,))
    total = cur.fetchone()[0]
    cur.execute("""
        SELECT o.text, COUNT(v.id)
        FROM options o
        LEFT JOIN votes v ON v.option_id = o.id
        WHERE o.poll_id=?
        GROUP BY o.id
        ORDER BY o.id
    """, (poll_id,))
    rows = cur.fetchall()
    conn.close()
    return total, [(r[0], r[1]) for r in rows]


# ----------------------------- 图表 -----------------------------
def draw_charts(parent, total, data):
    """在 parent 中绘制柱状图 + 饼图"""
    for w in parent.winfo_children():
        w.destroy()
    if not data:
        return
    labels = [d[0] for d in data]
    counts = [d[1] for d in data]

    fig = Figure(figsize=(7.5, 5.2), dpi=100)
    # 柱状图
    ax1 = fig.add_subplot(211)
    bars = ax1.bar(labels, counts, color=["#42a5f5", "#66bb6a", "#ffa726", "#ab47bc", "#ef5350", "#26c6da"][:len(labels)])
    ax1.set_title(f"投票结果统计（总票数：{total}）", fontsize=12, fontweight="bold")
    ax1.set_ylabel("票数")
    ax1.set_ylim(0, max(counts + [1]) + 1)
    for b, c in zip(bars, counts):
        ax1.text(b.get_x() + b.get_width() / 2, c + 0.1, str(c), ha="center", va="bottom", fontsize=9)
    fig.autofmt_xdate(rotation=20)
    # 饼图
    ax2 = fig.add_subplot(212)
    nonzero = [(l, c) for l, c in zip(labels, counts) if c > 0]
    if nonzero:
        ax2.pie([c for _, c in nonzero], labels=[l for l, _ in nonzero],
                autopct=lambda p: f"{p:.1f}%" if p > 0 else "", startangle=90,
                colors=["#42a5f5", "#66bb6a", "#ffa726", "#ab47bc", "#ef5350", "#26c6da"][:len(nonzero)])
        ax2.set_title("占比分布", fontsize=11)
    else:
        ax2.text(0.5, 0.5, "暂无投票数据", ha="center", va="center", fontsize=11, color="#999")
        ax2.axis("off")
    fig.tight_layout()
    canvas = FigureCanvasTkAgg(fig, master=parent)
    canvas.draw()
    canvas.get_tk_widget().pack(fill="both", expand=True)


# ----------------------------- 主应用 -----------------------------
class VotingApp:
    def __init__(self, root):
        self.root = root
        self.root.title("模拟投票 / 调查系统")
        self.root.geometry("900x720")
        self.root.minsize(820, 640)
        self.root.configure(bg="#eef2f7")
        self.current_user = "匿名用户"
        self.current_poll_id = None

        init_db()
        self._build_ui()
        self.refresh_poll_list()

    def _build_ui(self):
        # 顶部栏
        top = tk.Frame(self.root, bg="#1976d2", height=60)
        top.pack(fill="x")
        top.pack_propagate(False)
        tk.Label(top, text="🗳️ 模拟投票 / 调查系统", font=("Microsoft YaHei", 20, "bold"),
                 fg="white", bg="#1976d2").pack(side="left", padx=20)
        self.user_btn = tk.Button(top, text=f"当前用户：{self.current_user}", font=("Microsoft YaHei", 10),
                                  bg="#1565c0", fg="white", relief="flat", padx=10, cursor="hand2",
                                  command=self._switch_user)
        self.user_btn.pack(side="right", padx=15)

        # 主体：左侧主题列表 + 右侧详情
        body = tk.Frame(self.root, bg="#eef2f7")
        body.pack(fill="both", expand=True, padx=10, pady=10)
        body.columnconfigure(0, weight=1, minsize=260)
        body.columnconfigure(1, weight=3)
        body.rowconfigure(0, weight=1)

        # 左侧
        left = tk.Frame(body, bg="white", relief="solid", bd=1)
        left.grid(row=0, column=0, sticky="nsew", padx=(0, 8))
        tk.Label(left, text="投票主题", font=("Microsoft YaHei", 13, "bold"),
                 bg="white", fg="#37474f").pack(anchor="w", padx=12, pady=(12, 6))
        self.poll_listbox = tk.Listbox(left, font=("Microsoft YaHei", 11), height=18,
                                       selectbackground="#bbdefb", activestyle="none")
        self.poll_listbox.pack(fill="both", expand=True, padx=10, pady=(0, 8))
        self.poll_listbox.bind("<<ListboxSelect>>", self._on_poll_select)

        btn_row = tk.Frame(left, bg="white")
        btn_row.pack(fill="x", padx=10, pady=(0, 10))
        tk.Button(btn_row, text="+ 新建投票", font=("Microsoft YaHei", 10, "bold"),
                  bg="#1976d2", fg="white", relief="flat", padx=8, pady=4, cursor="hand2",
                  command=self._create_poll_dialog).pack(side="left", fill="x", expand=True)
        tk.Button(btn_row, text="刷新", font=("Microsoft YaHei", 10),
                  bg="#90a4ae", fg="white", relief="flat", padx=8, pady=4, cursor="hand2",
                  command=self.refresh_poll_list).pack(side="right")

        # 右侧
        right = tk.Frame(body, bg="white", relief="solid", bd=1)
        right.grid(row=0, column=1, sticky="nsew")
        right.rowconfigure(2, weight=1)
        right.columnconfigure(0, weight=1)

        self.detail_title = tk.Label(right, text="请选择或新建一个投票主题", font=("Microsoft YaHei", 14, "bold"),
                                     bg="white", fg="#37474f", wraplength=520, justify="left")
        self.detail_title.grid(row=0, column=0, sticky="w", padx=15, pady=(14, 4))

        self.options_frame = tk.Frame(right, bg="white")
        self.options_frame.grid(row=1, column=0, sticky="nsew", padx=15)

        self.chart_frame = tk.Frame(right, bg="white")
        self.chart_frame.grid(row=2, column=0, sticky="nsew", padx=8, pady=(0, 10))

    def _switch_user(self):
        name = simpledialog.askstring("切换用户", "请输入用户名：", initialvalue=self.current_user, parent=self.root)
        if name and name.strip():
            self.current_user = name.strip()
            self.user_btn.config(text=f"当前用户：{self.current_user}")

    def refresh_poll_list(self):
        self.poll_listbox.delete(0, "end")
        self._polls = list_polls()
        for pid, title, created_at in self._polls:
            self.poll_listbox.insert("end", f"#{pid}  {title}")

    def _on_poll_select(self, event):
        sel = self.poll_listbox.curselection()
        if not sel:
            return
        idx = sel[0]
        if idx >= len(self._polls):
            return
        poll_id = self._polls[idx][0]
        self.current_poll_id = poll_id
        self._render_poll(poll_id)

    def _render_poll(self, poll_id):
        for w in self.options_frame.winfo_children():
            w.destroy()
        poll, options = get_poll(poll_id)
        if not poll:
            return
        pid, title, question = poll
        self.detail_title.config(text=f"{title}" + (f"\n{question}" if question else ""))

        total, data = tally(poll_id)
        voted = has_voted(poll_id, self.current_user)

        info = tk.Label(self.options_frame,
                        text=f"总票数：{total}    {'✅ 您已投票' if voted else '尚未投票'}",
                        font=("Microsoft YaHei", 10), bg="white", fg="#546e7a")
        info.pack(anchor="w", pady=(0, 6))

        if not voted:
            tk.Label(self.options_frame, text="请选择一项投票：", font=("Microsoft YaHei", 10, "bold"),
                     bg="white", fg="#37474f").pack(anchor="w")
            for opt_id, opt_text in options:
                btn = tk.Button(self.options_frame, text=opt_text, font=("Microsoft YaHei", 10),
                                bg="#e3f2fd", fg="#1565c0", anchor="w", padx=10, pady=4, relief="flat",
                                cursor="hand2", command=lambda oid=opt_id, ot=opt_text: self._do_vote(oid, ot))
                btn.pack(fill="x", pady=2)
        else:
            # 已投票：显示各选项票数条
            maxc = max((c for _, c in data), default=1) or 1
            for opt_text, count in data:
                row = tk.Frame(self.options_frame, bg="white")
                row.pack(fill="x", pady=2)
                tk.Label(row, text=f"{opt_text}", font=("Microsoft YaHei", 10), bg="white", width=18, anchor="w").pack(side="left")
                bar = tk.Frame(row, bg="#42a5f5", width=max(2, int(220 * count / maxc)), height=16)
                bar.pack(side="left", padx=6)
                tk.Label(row, text=str(count), font=("Microsoft YaHei", 9), bg="white", fg="#546e7a").pack(side="left")

        draw_charts(self.chart_frame, total, data)

    def _do_vote(self, option_id, option_text):
        if not self.current_poll_id:
            return
        ok, msg = cast_vote(self.current_poll_id, option_id, self.current_user)
        messagebox.showinfo("提示", msg)
        if ok:
            self._render_poll(self.current_poll_id)

    def _create_poll_dialog(self):
        dlg = tk.Toplevel(self.root)
        dlg.title("新建投票主题")
        dlg.geometry("460x420")
        dlg.grab_set()
        dlg.configure(bg="white")

        tk.Label(dlg, text="主题标题：", font=("Microsoft YaHei", 11, "bold"), bg="white").pack(anchor="w", padx=18, pady=(16, 2))
        title_entry = tk.Entry(dlg, font=("Microsoft YaHei", 11))
        title_entry.pack(fill="x", padx=18)

        tk.Label(dlg, text="问题描述（可选）：", font=("Microsoft YaHei", 10), bg="white").pack(anchor="w", padx=18, pady=(10, 2))
        q_entry = tk.Entry(dlg, font=("Microsoft YaHei", 10))
        q_entry.pack(fill="x", padx=18)

        tk.Label(dlg, text="选项（每行一个，2-6 个）：", font=("Microsoft YaHei", 10), bg="white").pack(anchor="w", padx=18, pady=(10, 2))
        opt_text = tk.Text(dlg, font=("Microsoft YaHei", 10), height=7)
        opt_text.pack(fill="both", expand=True, padx=18)
        opt_text.insert("end", "选项一\n选项二\n选项三")

        def submit():
            title = title_entry.get().strip()
            question = q_entry.get().strip()
            opts = [o.strip() for o in opt_text.get("1.0", "end").splitlines() if o.strip()]
            if not title:
                messagebox.showwarning("提示", "请填写主题标题", parent=dlg)
                return
            if len(opts) < 2:
                messagebox.showwarning("提示", "请至少填写 2 个选项", parent=dlg)
                return
            if len(opts) > 6:
                messagebox.showwarning("提示", "选项最多 6 个", parent=dlg)
                return
            pid = create_poll(title, question, self.current_user, opts)
            dlg.destroy()
            self.refresh_poll_list()
            # 自动选中新建的主题
            for i, (tid, *_ ) in enumerate(self._polls):
                if tid == pid:
                    self.poll_listbox.selection_set(i)
                    self.poll_listbox.see(i)
                    self.current_poll_id = pid
                    self._render_poll(pid)
                    break
            messagebox.showinfo("成功", f"投票主题「{title}」已创建！")

        tk.Button(dlg, text="创建", font=("Microsoft YaHei", 11, "bold"), bg="#1976d2", fg="white",
                  relief="flat", padx=20, pady=4, cursor="hand2", command=submit).pack(pady=12)


def main():
    root = tk.Tk()
    VotingApp(root)
    root.mainloop()


if __name__ == "__main__":
    main()
