#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
burger_shop_cn.py
单文件完整版（简体中文界面）

功能：
- 用户注册/登录（用户名、显示名、密码 + 确认，密码以 PBKDF2 散列保存）
- 投诉箱（所有人可见，显示提交者名字），支持编辑状态与回复
- 投诉与用户可以导出为 CSV
- 多工位并行烹饪（默认 3 个工位），每个工位有进度条
- 成就系统（若干内置成就），达成弹窗提示并保存
- 存档保存/加载
- 头像/汉堡/金币用 Pillow 生成（可选）

使用：
1) 安装 Pillow（可选）：pip install pillow
2) 运行：python burger_shop_cn.py

"""

from __future__ import annotations
import os
import sys
import time
import json
import random
import hashlib
import hmac
import secrets
import csv
from dataclasses import dataclass, field
from typing import Optional, Dict, Any, List
import tkinter as tk
from tkinter import ttk, messagebox, filedialog

# Pillow（用于生成头像/汉堡/金币）
try:
    from PIL import Image, ImageDraw, ImageFont, ImageTk, ImageFilter
except Exception:
    Image = None
    ImageDraw = None
    ImageFont = None
    ImageTk = None
    ImageFilter = None

# ----------------------------
# 配置与路径
# ----------------------------
APP_NAME = "BurgerShopCN"
APP_DIR = os.path.join(os.path.expanduser("~"), f".{APP_NAME.lower()}")
ASSETS_DIR = os.path.join(APP_DIR, "assets")
SAVES_DIR = os.path.join(APP_DIR, "saves")
LOGS_DIR = os.path.join(APP_DIR, "logs")

for d in (APP_DIR, ASSETS_DIR, SAVES_DIR, LOGS_DIR):
    try:
        os.makedirs(d, exist_ok=True)
    except Exception:
        pass

USERS_DB = os.path.join(APP_DIR, "users.json")
COMPLAINTS_DB = os.path.join(APP_DIR, "complaints.json")
SAVE_FILE = os.path.join(SAVES_DIR, "save.json")
LOG_FILE = os.path.join(LOGS_DIR, "log.txt")

# 游戏常量
START_MONEY = 50.0
INIT_STOCK = {"bun": 10, "patty": 8, "lettuce": 6, "cheese": 6, "sauce": 10}
BUY_PRICES = {"bun": 2.0, "patty": 4.0, "lettuce": 1.0, "cheese": 1.5, "sauce": 0.5}
MENU = {
    "经典": {"recipe": {"bun":1,"patty":1,"lettuce":1,"cheese":1,"sauce":1}, "price":12.0},
    "芝士": {"recipe": {"bun":1,"patty":1,"cheese":2,"sauce":1}, "price":14.0},
    "素食": {"recipe": {"bun":1,"lettuce":2,"cheese":1,"sauce":1}, "price":10.0},
    "双层": {"recipe": {"bun":1,"patty":2,"lettuce":1,"cheese":1,"sauce":1}, "price":18.0},
}

DEFAULT_SLOTS = 3  # 默认工位数

# 密码哈希参数
PBKDF2_ITERS = 120_000
SALT_BYTES = 16
DK_BYTES = 32

# ----------------------------
# 日志与文件工具
# ----------------------------
def app_log(s: str):
    ts = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
    line = f"[{ts}] {s}"
    try:
        with open(LOG_FILE, "a", encoding="utf-8") as f:
            f.write(line + "\n")
    except Exception:
        print(line)

def read_json(path: str, default=None):
    try:
        with open(path, "r", encoding="utf-8") as f:
            return json.load(f)
    except Exception:
        return default

def write_json(path: str, data) -> bool:
    try:
        with open(path, "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        return True
    except Exception as e:
        app_log(f"write_json error: {e}")
        return False

# ----------------------------
# 用户管理（本地）
# ----------------------------
def ensure_users_db():
    if not os.path.exists(USERS_DB):
        write_json(USERS_DB, {"users": {}})

def load_users() -> Dict[str, Any]:
    ensure_users_db()
    d = read_json(USERS_DB, {"users": {}})
    return d.get("users", {})

def save_users(users: Dict[str, Any]):
    write_json(USERS_DB, {"users": users})

def hash_password(password: str, salt: Optional[bytes]=None) -> Dict[str, Any]:
    if salt is None:
        salt = secrets.token_bytes(SALT_BYTES)
    dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, PBKDF2_ITERS, dklen=DK_BYTES)
    return {"salt": salt.hex(), "dk": dk.hex(), "iters": PBKDF2_ITERS}

def verify_password(password: str, salt_hex: str, dk_hex: str, iters: int) -> bool:
    try:
        salt = bytes.fromhex(salt_hex)
        expected = bytes.fromhex(dk_hex)
        dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iters, dklen=len(expected))
        return hmac.compare_digest(dk, expected)
    except Exception:
        return False

# ----------------------------
# 投诉箱（所有人可见）
# ----------------------------
def ensure_complaints_db():
    if not os.path.exists(COMPLAINTS_DB):
        write_json(COMPLAINTS_DB, {"complaints": []})

def submit_complaint(user: Optional[str], display_name: Optional[str], ctype: str, subject: str, message: str, lang: str="zh"):
    ensure_complaints_db()
    db = read_json(COMPLAINTS_DB, {"complaints": []})
    rec = {
        "id": f"{int(time.time()*1000)}-{random.randint(1000,9999)}",
        "user": user,
        "display_name": display_name,
        "type": ctype,
        "subject": subject,
        "message": message,
        "lang": lang,
        "status": "open",
        "admin_reply": "",
        "created_at": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
    }
    db["complaints"].insert(0, rec)
    write_json(COMPLAINTS_DB, db)
    app_log(f"Complaint submitted: {rec['id']} by {user or 'guest'}")
    return rec

def list_complaints(limit=100):
    ensure_complaints_db()
    db = read_json(COMPLAINTS_DB, {"complaints": []})
    return db.get("complaints", [])[:limit]

def save_complaints_db(data: Dict[str, Any]):
    write_json(COMPLAINTS_DB, data)

def export_complaints_csv(path: Optional[str]=None) -> str:
    ensure_complaints_db()
    complaints = list_complaints(10000)
    if path is None:
        path = os.path.join(APP_DIR, f"complaints_{int(time.time())}.csv")
    try:
        with open(path, "w", newline="", encoding="utf-8") as f:
            writer = csv.writer(f)
            writer.writerow(["id","user","display_name","type","subject","message","lang","status","admin_reply","created_at"])
            for c in complaints:
                writer.writerow([c.get(k,"") for k in ["id","user","display_name","type","subject","message","lang","status","admin_reply","created_at"]])
        return path
    except Exception as e:
        app_log(f"export_complaints_csv error: {e}")
        return ""

def export_users_csv(path: Optional[str]=None) -> str:
    users = load_users()
    if path is None:
        path = os.path.join(APP_DIR, f"users_{int(time.time())}.csv")
    try:
        with open(path, "w", newline="", encoding="utf-8") as f:
            writer = csv.writer(f)
            writer.writerow(["username","display","created_at"])
            for uname, rec in users.items():
                writer.writerow([uname, rec.get("display",uname), rec.get("created_at","")])
        return path
    except Exception as e:
        app_log(f"export_users_csv error: {e}")
        return ""

# ----------------------------
# 游戏核心：模型与状态
# ----------------------------
@dataclass
class Order:
    name: str
    recipe: dict
    price: float

@dataclass
class CookTask:
    name: str
    recipe: dict
    price: float
    started_at: int
    duration_ms: int
    slot_index: int

@dataclass
class Customer:
    order: Order
    arrival: int = field(default_factory=lambda: int(time.time()*1000))
    patience_ms: int = 14000
    timeout_at: int = 0
    avatar_seed: int = 0

    def setup(self, base_patience):
        self.patience_ms = base_patience
        self.timeout_at = self.arrival + self.patience_ms

class GameState:
    def __init__(self, slots=DEFAULT_SLOTS):
        self.money: float = START_MONEY
        self.stock: Dict[str,int] = INIT_STOCK.copy()
        self.total_income: float = 0.0
        self.served: int = 0
        self.rating: float = 5.0
        self.customers: List[Customer] = []
        self.slots = slots
        self.tasks: List[Optional[CookTask]] = [None]*self.slots
        self.achievements: Dict[str, bool] = {}
        self.cumulative_income = 0.0

    def has_stock(self, recipe):
        for k, v in recipe.items():
            if self.stock.get(k, 0) < v:
                return False
        return True

    def consume(self, recipe):
        for k, v in recipe.items():
            self.stock[k] = max(0, self.stock.get(k, 0) - v)

    def buy(self, item, qty, price):
        cost = qty * price
        if self.money >= cost:
            self.money -= cost
            self.stock[item] = self.stock.get(item, 0) + qty
            return True
        return False

    def add_customer(self, order_name):
        entry = MENU.get(order_name)
        if not entry:
            return None
        o = Order(order_name, entry["recipe"], entry["price"])
        c = Customer(o, avatar_seed=random.randint(0, 9999))
        c.setup(14000)
        self.customers.append(c)
        return c

    def remove_customer(self, customer):
        try:
            self.customers.remove(customer)
        except ValueError:
            pass

    def check_timeouts(self):
        now = int(time.time()*1000)
        removed = []
        for c in list(self.customers):
            if c.timeout_at <= now:
                self.customers.remove(c)
                self.rating = max(1.0, self.rating - 0.2)
                removed.append(c)
        return removed

    def start_task_on_slot(self, slot_idx: int, order_name: str, duration_ms: Optional[int]=None):
        if slot_idx < 0 or slot_idx >= self.slots:
            return False, "invalid_slot"
        if self.tasks[slot_idx] is not None:
            return False, "slot_busy"
        entry = MENU.get(order_name)
        if not entry:
            return False, "no_such_menu"
        if not self.has_stock(entry["recipe"]):
            return False, "no_stock"
        self.consume(entry["recipe"])
        dur = duration_ms if duration_ms is not None else 3000
        task = CookTask(name=order_name, recipe=entry["recipe"], price=entry["price"],
                        started_at=int(time.time()*1000), duration_ms=dur, slot_index=slot_idx)
        self.tasks[slot_idx] = task
        return True, task

    def finish_task(self, slot_idx: int):
        task = self.tasks[slot_idx]
        if not task:
            return False
        self.money += task.price
        self.total_income += task.price
        self.cumulative_income += task.price
        self.served += 1
        if not self.achievements.get("first_sale"):
            self.achievements["first_sale"] = True
        self.tasks[slot_idx] = None
        return True

    def to_dict(self):
        return {
            "money": self.money,
            "stock": self.stock,
            "total_income": self.total_income,
            "served": self.served,
            "rating": self.rating,
            "slots": self.slots,
            "achievements": self.achievements,
            "cumulative_income": self.cumulative_income
        }

    def load_dict(self, d):
        self.money = d.get("money", self.money)
        self.stock = d.get("stock", self.stock)
        self.total_income = d.get("total_income", self.total_income)
        self.served = d.get("served", self.served)
        self.rating = d.get("rating", self.rating)
        self.slots = d.get("slots", self.slots)
        self.achievements = d.get("achievements", self.achievements)
        self.cumulative_income = d.get("cumulative_income", self.cumulative_income)
        self.tasks = [None]*self.slots

GAME = GameState()

# ----------------------------
# 图像生成（Pillow）
# ----------------------------
def make_hamburger_image(width=140):
    if Image is None:
        return None
    h = int(width*0.7)
    img = Image.new("RGBA", (width, h), (255,255,255,0))
    d = ImageDraw.Draw(img)
    center = width//2
    d.ellipse([center-60, 0, center+60, 28], fill=(239,184,92))  # top bun
    d.rectangle([center-54, 28, center+54, 48], fill=(120,60,40))  # patty
    d.rectangle([center-58, 48, center+58, 62], fill=(255,200,60))  # cheese
    d.rectangle([center-62, 62, center+62, 74], fill=(110,180,90))  # lettuce
    d.ellipse([center-60, 74, center+60, 104], fill=(239,184,92))  # bottom bun
    return img

def make_avatar(size=64, seed=None):
    if Image is None:
        return None
    if seed is None:
        seed = random.randint(0,9999)
    random.seed(seed)
    img = Image.new("RGBA", (size,size), (0,0,0,0))
    d = ImageDraw.Draw(img)
    r = (seed % 100) + 120
    g = ((seed // 7) % 100) + 100
    b = ((seed // 13) % 100) + 80
    d.ellipse([4,4,size-4,size-4], fill=(r%256,g%256,b%256))
    d.ellipse([int(size*0.3), int(size*0.35), int(size*0.38), int(size*0.45)], fill=(20,20,20))
    d.ellipse([int(size*0.62), int(size*0.35), int(size*0.70), int(size*0.45)], fill=(20,20,20))
    d.arc([int(size*0.28), int(size*0.5), int(size*0.72), int(size*0.8)], start=20, end=160, fill=(60,30,20), width=2)
    return img

def make_coin(size=40):
    if Image is None:
        return None
    img = Image.new("RGBA",(size,size),(0,0,0,0))
    d = ImageDraw.Draw(img)
    d.ellipse([2,2,size-2,size-2], fill=(255,215,0))
    try:
        font = ImageFont.truetype("arial.ttf", int(size*0.6))
        d.text((size*0.28, size*0.05), "$", font=font, fill=(150,90,0))
    except Exception:
        d.text((size*0.32, size*0.08), "$", fill=(150,90,0))
    return img

def pil2tk(img, master=None):
    if img is None or ImageTk is None:
        return None
    return ImageTk.PhotoImage(img, master=master)

# ----------------------------
# 成就定义
# ----------------------------
ACHIEVEMENTS_DEF = {
    "first_sale": {"title": "首次出餐", "desc": "完成第一次出餐"},
    "earn_100": {"title": "累计收入100", "desc": "累计收入达到100"},
    "multi_slot": {"title": "并行大师", "desc": "同时完成2个或以上工位的出餐"},
}

# ----------------------------
# 主界面与窗口
# ----------------------------
class BurgerApp:
    def __init__(self, master):
        self.master = master
        master.title("汉堡店（测试版）")
        self.users = load_users()
        self.current_user: Optional[Dict[str, Any]] = None
        self._prepare_images()
        self._build_ui()

    def _prepare_images(self):
        self.ham_tk = pil2tk(make_hamburger_image(140), master=self.master)
        self.coin_tk = pil2tk(make_coin(40), master=self.master)

    def _build_ui(self):
        top = ttk.Frame(self.master)
        top.pack(side=tk.TOP, fill=tk.X, padx=6, pady=6)
        ttk.Label(top, text="汉堡店", font=("Arial",16,"bold")).pack(side=tk.LEFT)
        ttk.Button(top, text="导出用户 CSV", command=self._export_users).pack(side=tk.RIGHT, padx=4)
        ttk.Button(top, text="导出投诉 CSV", command=self._export_complaints).pack(side=tk.RIGHT, padx=4)
        ttk.Button(top, text="投诉箱", command=self._open_complaint_viewer).pack(side=tk.RIGHT, padx=4)
        ttk.Button(top, text="注册", command=self._open_register).pack(side=tk.RIGHT, padx=4)
        ttk.Button(top, text="登录", command=self._open_login).pack(side=tk.RIGHT, padx=4)

        main = ttk.Frame(self.master)
        main.pack(fill=tk.BOTH, expand=True, padx=6, pady=6)
        self.canvas = tk.Canvas(main, width=720, height=480, bg="#f7f7f7")
        self.canvas.pack(side=tk.LEFT, padx=6, pady=6)
        right = ttk.Frame(main, width=320)
        right.pack(side=tk.RIGHT, fill=tk.Y, padx=6, pady=6)

        self.money_var = tk.StringVar(value=f"${GAME.money:.2f}")
        ttk.Label(right, textvariable=self.money_var, font=("Arial",14,"bold")).pack(anchor="nw")
        ttk.Label(right, text="库存:").pack(anchor="nw")
        self.stock_vars = {}
        for k,v in GAME.stock.items():
            var = tk.StringVar(value=f"{k}: {v}")
            self.stock_vars[k] = var
            ttk.Label(right, textvariable=var).pack(anchor="nw")
        ttk.Separator(right).pack(fill=tk.X, pady=6)
        ttk.Label(right, text="采购:").pack(anchor="nw")
        for item, price in BUY_PRICES.items():
            ttk.Button(right, text=f"购买 5 {item} (${price*5:.1f})", command=lambda it=item: self.buy_item(it,5)).pack(fill=tk.X, pady=2)

        ttk.Separator(right).pack(fill=tk.X, pady=6)
        ttk.Label(right, text="厨房工位:").pack(anchor="nw")
        self.slot_frames = []
        self.slot_progress = []
        for i in range(GAME.slots):
            frame = ttk.Frame(right, relief=tk.RIDGE, padding=6)
            frame.pack(fill=tk.X, pady=4)
            ttk.Label(frame, text=f"工位 {i+1}").pack(anchor="w")
            prog = ttk.Progressbar(frame, orient="horizontal", length=200, mode="determinate")
            prog.pack(anchor="w", pady=4)
            lbl = ttk.Label(frame, text="空闲")
            lbl.pack(anchor="w")
            self.slot_frames.append((frame,lbl))
            self.slot_progress.append(prog)

        ttk.Button(right, text="接单（自动分配）", command=self.take_order).pack(fill=tk.X, pady=6)
        ttk.Button(right, text="一键开工", command=self.start_all_free_slots).pack(fill=tk.X, pady=2)
        ttk.Separator(right).pack(fill=tk.X, pady=6)
        ttk.Button(right, text="投诉/建议", command=self._open_complaint_form).pack(fill=tk.X, pady=2)
        ttk.Button(right, text="保存存档", command=self._save_game).pack(fill=tk.X, pady=6)
        ttk.Button(right, text="载入存档", command=self._load_game).pack(fill=tk.X, pady=2)

        self._draw_static()
        self._refresh_ui()
        self.master.after(3000, self._spawn_customer_periodic)
        self.master.after(200, self._tick)

    def _open_register(self):
        RegisterWindow(self)

    def _open_login(self):
        LoginWindow(self)

    def _open_complaint_form(self):
        ComplaintFormWindow(self)

    def _open_complaint_viewer(self):
        ComplaintViewer(self)

    def _draw_static(self):
        self.canvas.delete("all")
        self.canvas.create_rectangle(10,10,700,460, fill="#fff", outline="#ddd")
        self.canvas.create_text(80,20, text="顾客区", font=("Arial",12,"bold"))
        self.canvas.create_text(540,20, text="厨房区", font=("Arial",12,"bold"))
        if self.ham_tk:
            self.canvas.create_image(540,180, image=self.ham_tk)

    def _refresh_ui(self):
        self.money_var.set(f"${GAME.money:.2f}")
        for k, v in GAME.stock.items():
            if k in self.stock_vars:
                self.stock_vars[k].set(f"{k}: {v}")
        now = int(time.time()*1000)
        for idx in range(GAME.slots):
            task = GAME.tasks[idx]
            prog = self.slot_progress[idx]
            lbl = self.slot_frames[idx][1]
            if task:
                elapsed = now - task.started_at
                ratio = min(1.0, elapsed / task.duration_ms)
                prog['value'] = ratio*100
                remaining = max(0, int((task.duration_ms - elapsed)/1000))
                lbl.config(text=f"{task.name} ({remaining}s)")
                if elapsed >= task.duration_ms:
                    GAME.finish_task(idx)
                    self._show_coin_animation()
                    prog['value'] = 0
                    lbl.config(text="空闲")
            else:
                prog['value'] = 0
                lbl.config(text="空闲")
        self._draw_customers()

    def _draw_customers(self):
        x0 = 30
        y0 = 40
        gap = 72
        self.canvas.delete("cust")
        for i, c in enumerate(GAME.customers[:6]):
            av = make_avatar(64, c.avatar_seed)
            tkav = pil2tk(av, master=self.master)
            if tkav:
                self.canvas.create_image(x0, y0 + i*gap, image=tkav, anchor="nw", tags=("cust",))
                setattr(self, f"_avatar_{i}", tkav)
            self.canvas.create_text(x0+80, y0 + i*gap + 20, text=c.order.name, anchor="w", tags=("cust",))
            remain = max(0, int((c.timeout_at - int(time.time()*1000))/1000))
            ratio = remain / (c.patience_ms/1000) if c.patience_ms>0 else 0
            self.canvas.create_rectangle(x0+80, y0 + i*gap + 32, x0+220, y0 + i*gap + 40, outline="#666", tags=("cust",))
            self.canvas.create_rectangle(x0+80, y0 + i*gap + 32, x0+80+int(140*ratio), y0 + i*gap + 40, fill="#76c893", outline="", tags=("cust",))

    def _spawn_customer_periodic(self):
        if len(GAME.customers) < 8:
            name = random.choices(list(MENU.keys()), weights=[0.4,0.2,0.25,0.15], k=1)[0]
            GAME.add_customer(name)
            self._draw_customers()
        self.master.after(3000, self._spawn_customer_periodic)

    def _tick(self):
        GAME.check_timeouts()
        self._refresh_ui()
        self.master.after(200, self._tick)

    def buy_item(self, item, qty):
        price = BUY_PRICES.get(item,1.0)
        ok = GAME.buy(item, qty, price)
        if not ok:
            messagebox.showwarning("购买失败", "资金不足")
        self._refresh_ui()

    def take_order(self):
        if not GAME.customers:
            messagebox.showinfo("提示", "当前没有顾客")
            return
        c = GAME.customers.pop(0)
        for idx in range(GAME.slots):
            if GAME.tasks[idx] is None:
                ok, t = GAME.start_task_on_slot(idx, c.order.name)
                if ok:
                    messagebox.showinfo("接单", f"已将 {c.order.name} 分配到工位 {idx+1}")
                else:
                    messagebox.showwarning("无法开工", str(t))
                break
        self._refresh_ui()

    def start_all_free_slots(self):
        for idx in range(GAME.slots):
            if GAME.tasks[idx] is None and GAME.customers:
                c = GAME.customers.pop(0)
                ok, t = GAME.start_task_on_slot(idx, c.order.name)
                if not ok:
                    messagebox.showwarning("无法开工", str(t))
        self._refresh_ui()

    def _show_coin_animation(self):
        if self.coin_tk is None:
            return
        x = 560 + random.randint(-20,20)
        y = 220 + random.randint(-10,10)
        idc = self.canvas.create_image(x, y, image=self.coin_tk)
        def anim(step=0):
            if step>20:
                try: self.canvas.delete(idc)
                except: pass
                return
            try: self.canvas.move(idc, 0, -4)
            except: pass
            self.master.after(30, lambda: anim(step+1))
        anim()

    def _open_complaint_viewer(self):
        ComplaintViewer(self)

    def _open_complaint_form(self):
        ComplaintFormWindow(self)

    def _export_complaints(self):
        path = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV files","*.csv")], initialfile="complaints.csv")
        if not path:
            path = os.path.join(APP_DIR, f"complaints_{int(time.time())}.csv")
        out = export_complaints_csv(path)
        if out:
            messagebox.showinfo("导出完成", f"导出到 {out}")
        else:
            messagebox.showwarning("导出失败", "导出失败")

    def _export_users(self):
        path = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV files","*.csv")], initialfile="users.csv")
        if not path:
            path = os.path.join(APP_DIR, f"users_{int(time.time())}.csv")
        out = export_users_csv(path)
        if out:
            messagebox.showinfo("导出完成", f"导出到 {out}")
        else:
            messagebox.showwarning("导出失败", "导出失败")

    def _save_game(self):
        ok = write_json(SAVE_FILE, GAME.to_dict())
        if ok:
            messagebox.showinfo("保存", "游戏已保存")
        else:
            messagebox.showwarning("保存失败", "保存失败")

    def _load_game(self):
        d = read_json(SAVE_FILE, None)
        if d:
            GAME.load_dict(d)
            messagebox.showinfo("载入", "存档已载入")
        else:
            messagebox.showwarning("载入失败", "未找到存档或载入失败")

# ----------------------------
# 注册 / 登录 / 投诉 窗口实现
# ----------------------------
class RegisterWindow:
    def __init__(self, app: BurgerApp):
        self.app = app
        self.top = tk.Toplevel(app.master)
        self.top.title("注册")
        ttk.Label(self.top, text="用户名").grid(row=0, column=0, sticky="w")
        self.username = ttk.Entry(self.top); self.username.grid(row=0, column=1)
        ttk.Label(self.top, text="显示名").grid(row=1, column=0, sticky="w")
        self.display = ttk.Entry(self.top); self.display.grid(row=1, column=1)
        ttk.Label(self.top, text="密码").grid(row=2, column=0, sticky="w")
        self.pwd = ttk.Entry(self.top, show="*"); self.pwd.grid(row=2, column=1)
        ttk.Label(self.top, text="确认密码").grid(row=3, column=0, sticky="w")
        self.pwd2 = ttk.Entry(self.top, show="*"); self.pwd2.grid(row=3, column=1)
        ttk.Button(self.top, text="注册", command=self.register).grid(row=4, column=0, columnspan=2, pady=6)

    def register(self):
        user = self.username.get().strip()
        display = self.display.get().strip() or user
        p1 = self.pwd.get()
        p2 = self.pwd2.get()
        if not user:
            messagebox.showwarning("错误", "用户名必填")
            return
        if p1 != p2:
            messagebox.showwarning("错误", "两次密码不一致")
            return
        if len(p1) < 6:
            messagebox.showwarning("错误", "密码至少6位")
            return
        users = load_users()
        if user in users:
            messagebox.showwarning("错误", "用户名已存在")
            return
        ph = hash_password(p1)
        users[user] = {"display": display, "salt": ph["salt"], "dk": ph["dk"], "iters": ph["iters"], "created_at": time.strftime("%Y-%m-%d %H:%M:%S")}
        save_users(users)
        messagebox.showinfo("成功", "注册成功，您现在可以登录")
        app_log(f"用户注册: {user}")
        self.top.destroy()

class LoginWindow:
    def __init__(self, app: BurgerApp):
        self.app = app
        self.top = tk.Toplevel(app.master)
        self.top.title("登录")
        ttk.Label(self.top, text="用户名").grid(row=0, column=0, sticky="w")
        self.username = ttk.Entry(self.top); self.username.grid(row=0, column=1)
        ttk.Label(self.top, text="密码").grid(row=1, column=0, sticky="w")
        self.pwd = ttk.Entry(self.top, show="*"); self.pwd.grid(row=1, column=1)
        ttk.Button(self.top, text="登录", command=self.login).grid(row=2, column=0, columnspan=2, pady=6)

    def login(self):
        user = self.username.get().strip()
        pwd = self.pwd.get()
        users = load_users()
        rec = users.get(user)
        if not rec:
            messagebox.showwarning("失败", "用户不存在")
            return
        ok = verify_password(pwd, rec["salt"], rec["dk"], rec.get("iters", PBKDF2_ITERS))
        if ok:
            self.app.current_user = {"username": user, "display": rec.get("display", user)}
            messagebox.showinfo("欢迎", f"欢迎 {self.app.current_user['display']}")
            app_log(f"用户登录: {user}")
            self.top.destroy()
        else:
            messagebox.showwarning("失败", "登录失败")

class ComplaintFormWindow:
    def __init__(self, app: BurgerApp):
        self.app = app
        self.top = tk.Toplevel(app.master)
        self.top.title("提交投诉/建议")
        ttk.Label(self.top, text="主题").grid(row=0, column=0, sticky="w")
        self.subject = ttk.Entry(self.top, width=50); self.subject.grid(row=0, column=1, pady=4)
        ttk.Label(self.top, text="内容").grid(row=1, column=0, sticky="nw")
        self.msg = tk.Text(self.top, width=40, height=8); self.msg.grid(row=1, column=1, pady=4)
        ttk.Button(self.top, text="提交", command=self.submit).grid(row=2, column=0, columnspan=2, pady=6)

    def submit(self):
        subj = self.subject.get().strip()
        msg = self.msg.get("1.0", "end").strip()
        user = self.app.current_user["username"] if self.app.current_user else None
        display = self.app.current_user["display"] if self.app.current_user else None
        if not subj or not msg:
            messagebox.showwarning("错误", "主题与内容不能为空")
            return
        submit_complaint(user, display, "suggestion", subj, msg, "zh")
        messagebox.showinfo("感谢", "感谢你的反馈")
        self.top.destroy()

# ----------------------------
# 投诉箱查看器：所有人可见并显示提交者名字，支持编辑状态与管理员回复
# ----------------------------
class ComplaintViewer:
    def __init__(self, app: BurgerApp):
        self.app = app
        self.top = tk.Toplevel(app.master)
        self.top.title("投诉箱（所有人可见）")
        frm = ttk.Frame(self.top, padding=6)
        frm.pack(fill=tk.BOTH, expand=True)

        # Filter and list
        filter_frame = ttk.Frame(frm)
        filter_frame.pack(fill=tk.X)
        ttk.Label(filter_frame, text="筛选（用户名/关键字）").pack(side=tk.LEFT)
        self.filter_entry = ttk.Entry(filter_frame)
        self.filter_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4)
        ttk.Button(filter_frame, text="筛选", command=self.reload_list).pack(side=tk.LEFT, padx=4)
        ttk.Button(filter_frame, text="导出当前", command=self.export_current).pack(side=tk.LEFT, padx=4)

        list_frame = ttk.Frame(frm)
        list_frame.pack(fill=tk.BOTH, expand=True, pady=6)
        self.listbox = tk.Listbox(list_frame, height=12)
        self.listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        self.listbox.bind("<<ListboxSelect>>", self.on_select)

        scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=self.listbox.yview)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.listbox.config(yscrollcommand=scrollbar.set)

        # Detail / edit pane
        detail_frame = ttk.Frame(frm)
        detail_frame.pack(fill=tk.X, pady=6)
        ttk.Label(detail_frame, text="主题:").grid(row=0, column=0, sticky="w")
        self.subject_var = tk.StringVar()
        ttk.Entry(detail_frame, textvariable=self.subject_var, width=60).grid(row=0, column=1, sticky="w")
        ttk.Label(detail_frame, text="提交者:").grid(row=1, column=0, sticky="w")
        self.user_var = tk.StringVar()
        ttk.Entry(detail_frame, textvariable=self.user_var).grid(row=1, column=1, sticky="w")
        ttk.Label(detail_frame, text="时间:").grid(row=2, column=0, sticky="w")
        self.time_var = tk.StringVar()
        ttk.Entry(detail_frame, textvariable=self.time_var).grid(row=2, column=1, sticky="w")
        ttk.Label(detail_frame, text="内容:").grid(row=3, column=0, sticky="nw")
        self.msg_text = tk.Text(detail_frame, width=60, height=8)
        self.msg_text.grid(row=3, column=1, sticky="w")
        ttk.Label(detail_frame, text="状态:").grid(row=4, column=0, sticky="w")
        self.status_var = tk.StringVar()
        ttk.Combobox(detail_frame, textvariable=self.status_var, values=["open","in_progress","closed"]).grid(row=4, column=1, sticky="w")
        ttk.Label(detail_frame, text="管理员回复:").grid(row=5, column=0, sticky="nw")
        self.reply_text = tk.Text(detail_frame, width=60, height=4)
        self.reply_text.grid(row=5, column=1, sticky="w")
        btn_frame = ttk.Frame(frm)
        btn_frame.pack(fill=tk.X, pady=6)
        ttk.Button(btn_frame, text="保存修改", command=self.save_edit).pack(side=tk.LEFT, padx=4)
        ttk.Button(btn_frame, text="刷新", command=self.reload_list).pack(side=tk.LEFT, padx=4)
        ttk.Button(btn_frame, text="关闭", command=self.top.destroy).pack(side=tk.RIGHT, padx=4)

        self.records = []
        self.reload_list()

    def reload_list(self):
        key = self.filter_entry.get().strip().lower()
        recs = list_complaints(1000)
        if key:
            recs = [r for r in recs if (r.get("user") and key in r.get("user","").lower()) or key in (r.get("subject","")+" "+r.get("message","")).lower()]
        self.records = recs
        self.listbox.delete(0, tk.END)
        for r in recs:
            display = f"[{r.get('created_at')}] {r.get('display_name') or r.get('user') or '匿名'} - {r.get('subject')}"
            self.listbox.insert(tk.END, display)

    def on_select(self, event):
        idxs = self.listbox.curselection()
        if not idxs:
            return
        idx = idxs[0]
        rec = self.records[idx]
        self.subject_var.set(rec.get("subject",""))
        user_display = f"{rec.get('display_name') or ''} ({rec.get('user') or '匿名'})"
        self.user_var.set(user_display)
        self.time_var.set(rec.get("created_at",""))
        self.msg_text.delete("1.0","end"); self.msg_text.insert("1.0", rec.get("message",""))
        self.status_var.set(rec.get("status","open"))
        self.reply_text.delete("1.0","end"); self.reply_text.insert("1.0", rec.get("admin_reply",""))

    def save_edit(self):
        idxs = self.listbox.curselection()
        if not idxs:
            messagebox.showwarning("提示", "请选择一条记录")
            return
        idx = idxs[0]
        rec = self.records[idx]
        rec["subject"] = self.subject_var.get().strip()
        # 尝试从 user_var 中解析用户名（格式 display (username)）
        # 但我们不改 submitter username here
        rec["message"] = self.msg_text.get("1.0","end").strip()
        rec["status"] = self.status_var.get()
        rec["admin_reply"] = self.reply_text.get("1.0","end").strip()
        # persist
        db = read_json(COMPLAINTS_DB, {"complaints":[]})
        for i,d in enumerate(db.get("complaints",[])):
            if d.get("id") == rec.get("id"):
                db["complaints"][i] = rec
                break
        write_json(COMPLAINTS_DB, db)
        messagebox.showinfo("保存", "已保存修改")
        self.reload_list()

    def export_current(self):
        path = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV files","*.csv")], initialfile="complaints_filtered.csv")
        if not path:
            path = os.path.join(APP_DIR, f"complaints_{int(time.time())}.csv")
        try:
            with open(path, "w", newline="", encoding="utf-8") as f:
                writer = csv.writer(f)
                writer.writerow(["id","user","display_name","type","subject","message","lang","status","admin_reply","created_at"])
                for rec in self.records:
                    writer.writerow([rec.get(k,"") for k in ["id","user","display_name","type","subject","message","lang","status","admin_reply","created_at"]])
            messagebox.showinfo("导出", f"导出到 {path}")
        except Exception as e:
            messagebox.showwarning("导出失败", str(e))

# ----------------------------
# Complaint submit form (simple)
# ----------------------------
class ComplaintFormWindow:
    def __init__(self, app: BurgerApp):
        self.app = app
        self.top = tk.Toplevel(app.master)
        self.top.title("提交投诉/建议")
        ttk.Label(self.top, text="主题").grid(row=0, column=0, sticky="w")
        self.subject = ttk.Entry(self.top, width=50); self.subject.grid(row=0, column=1, pady=4)
        ttk.Label(self.top, text="内容").grid(row=1, column=0, sticky="nw")
        self.msg = tk.Text(self.top, width=40, height=8); self.msg.grid(row=1, column=1, pady=4)
        ttk.Button(self.top, text="提交", command=self.submit).grid(row=2, column=0, columnspan=2, pady=6)

    def submit(self):
        subj = self.subject.get().strip()
        msg = self.msg.get("1.0","end").strip()
        user = self.app.current_user["username"] if self.app.current_user else None
        display = self.app.current_user["display"] if self.app.current_user else None
        if not subj or not msg:
            messagebox.showwarning("错误", "主题与内容不能为空")
            return
        submit_complaint(user, display, "suggestion", subj, msg, "zh")
        messagebox.showinfo("感谢", "感谢你的反馈")
        self.top.destroy()

# ----------------------------
# Main
# ----------------------------
def main():
    root = tk.Tk()
    root.geometry("1150x901")
    app = BurgerApp(root)
    root.mainloop()

if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        print("发生错误：", e)
        import traceback
        traceback.print_exc()