import eventlet
eventlet.monkey_patch()
from flask import Flask, request, jsonify, render_template, send_from_directory
from flask_cors import CORS
import os
import json
import sqlite3
import hashlib
import time
import threading
from threading import Lock
import logging
import random
import math
from copy import deepcopy
from greenlet import GreenletExit
from collections import defaultdict, deque
from combat import GameEngine, generate_uuid
from flask_socketio import SocketIO, disconnect, join_room, leave_room, emit
from db_storage import save_state_to_tables, load_state_from_tables, start_async_save_worker, async_save_state, save_state_to_tables_with_conn
import chat
from admin import admin_bp
from chat import add_system_message, get_db, close_game_connection
from online_state import online_cache, cache_lock
from werkzeug.security import generate_password_hash, check_password_hash
from auction import auction_bp, init_auction_table, check_expired_auctions
from craft import CraftSystem
from state_manager import player_state_manager
from notification import send_public, send_private
from activity_manager import activity_manager
from farm import FarmSystem
from parking import parking_bp, init_parking, set_game_engine as set_parking_engine, ensure_parking_spots
from static_db import db
from jwt_utils import generate_token, verify_token, get_current_user_from_jwt
from config_center import CONFIG   # 配置中心

app = Flask(__name__)
app.secret_key = CONFIG.get('flask.secret_key')
CORS(app)
from flask_compress import Compress
compress = Compress(app)
app.config['COMPRESS_MIMETYPES'] = ['application/json', 'text/html', 'text/css', 'application/javascript', 'text/plain']
app.config['COMPRESS_LEVEL'] = 6
app.config['COMPRESS_MIN_SIZE'] = 500

from world_boss import WorldBossManager

# ---------- 先定义 socketio ----------
socketio = SocketIO(
    app,
    cors_allowed_origins="*",
    async_mode='eventlet',
    ping_timeout=60,          # 增加超时容忍度
    ping_interval=25,         # 心跳间隔
    max_http_buffer_size=1e7,
    engineio_logger=False,
    logger=False,
    **{'async_handlers': True}
)

app.extensions['socketio'] = socketio

app.socketio = socketio
import notification
notification.init_notification(socketio)
app.register_blueprint(chat.chat_bp)
app.register_blueprint(auction_bp)
app.register_blueprint(admin_bp)
chat.socketio = socketio

DATA_DIR = 'data'
CONFIG_FILE = 'config.json'

logger = logging.getLogger(__name__)

if not os.path.exists(DATA_DIR):
    os.makedirs(DATA_DIR)

import copy

def compute_inventory_diff(old_inv, new_inv):
    old_map = {item['uuid']: item for item in old_inv}
    new_map = {item['uuid']: item for item in new_inv}
    
    added = []
    removed = []
    modified = []
    
    # 新增或数量变化
    for uuid, new_item in new_map.items():
        if uuid not in old_map:
            added.append(new_item)
        else:
            old_qty = old_map[uuid].get('quantity', 1)
            new_qty = new_item.get('quantity', 1)
            if old_qty != new_qty:
                modified.append({'uuid': uuid, 'quantity': new_qty})
    
    # 删除
    for uuid in old_map:
        if uuid not in new_map:
            removed.append(uuid)
    
    return {'added': added, 'removed': removed, 'modified': modified}

def compute_state_diff(old, new):
    diff = {}
    # 处理普通字段（全量）
    for key in ['player', 'hero', 'warehouse', 'equipment',
                'currentMap', 'monsterGroup', 'task', 'farm', 'zodiac_equipped']:
        if old.get(key) != new.get(key):
            diff[key] = new[key]
    
    # 背包特殊处理：增量
    if old.get('inventory') != new.get('inventory'):
        diff['inventory_diff'] = compute_inventory_diff(old.get('inventory', []), new.get('inventory', []))
    
    old_buffs = old.get('temporary_buffs', {})
    new_buffs = new.get('temporary_buffs', {})
    if old_buffs != new_buffs:
        diff['temporary_buffs'] = new_buffs
    return diff

def hash_password(password):
    return generate_password_hash(password)

def verify_password(password_hash, password):
    return check_password_hash(password_hash, password)

def load_state_from_db(name):
    conn = None
    try:
        conn = chat.get_game_db()
        c = conn.cursor()
        c.execute('SELECT data FROM characters WHERE name = ?', (name,))
        row = c.fetchone()
        if row and row[0]:
            state = json.loads(row[0])
            ensure_uuid_for_items(state)
            if 'temporary_buffs' not in state:
                state['temporary_buffs'] = {}
            return state
        return None
    except Exception as e:
        logger.error(f"从数据库加载状态失败: {name}, {e}")
        return None
    finally:
        if conn:
            chat.close_game_connection(conn)

def broadcast_map_stats(map_id):
    try:
        map_id_int = int(map_id)
        # 始终返回普通怪物池的数量
        monster_count = len(global_monster_pools.get(map_id_int, []))
        boss_info = world_boss_manager.get_boss_info(map_id_int)
        has_boss = boss_info.get('exists') and boss_info.get('boss_hp', 0) > 0
        with map_counts_lock:
            players_count = map_player_counts.get(str(map_id_int), 0)
        socketio.emit('map_stats', {
            'map_id': map_id_int,
            'monsters_remaining': monster_count,
            'has_world_boss': has_boss,          # 新增字段，供前端判断是否显示世界 Boss 入口
            'players_online': players_count
        }, room=f'map_{map_id_int}')
    except Exception as e:
        logger.error(f"广播地图统计失败: {map_id}, {e}")

map_broadcast_timers = {}
map_broadcast_lock = Lock()
MAP_BROADCAST_DELAY = CONFIG.get('world.map_stats_broadcast_delay')
auto_combat_tasks = {}
auto_combat_lock = Lock()
_auto_sell_cache = {}                     # key: f"{user_id}:{character_name}" -> (data, expire_time)
_AUTO_SELL_CACHE_TTL = 10                # 缓存有效期（秒）
_AUTO_SELL_CACHE_LOCK = threading.Lock() # 可选，用于高并发时保护缓存写操作

def schedule_broadcast_map_stats(map_id):
    with map_broadcast_lock:
        if map_id in map_broadcast_timers:
            map_broadcast_timers[map_id].cancel()
        timer = threading.Timer(MAP_BROADCAST_DELAY, broadcast_map_stats, args=[map_id])
        timer.daemon = True
        timer.start()
        map_broadcast_timers[map_id] = timer

def init_game_db():
    conn = chat.get_game_db()
    try:
        c = conn.cursor()
        c.execute('''
            CREATE TABLE IF NOT EXISTS users (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                username TEXT UNIQUE NOT NULL,
                password_hash TEXT NOT NULL,
                created_at REAL DEFAULT (strftime('%s','now'))
            )
        ''')
        c.execute('''
            CREATE TABLE IF NOT EXISTS characters (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id INTEGER NOT NULL,
                name TEXT NOT NULL UNIQUE,
                data_file TEXT NOT NULL,
                data TEXT,
                created_at REAL DEFAULT (strftime('%s','now')),
                FOREIGN KEY(user_id) REFERENCES users(id)
            )
        ''')
        c.execute('''
            CREATE TABLE IF NOT EXISTS world_boss_kills (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                boss_name TEXT NOT NULL,
                killer_name TEXT NOT NULL,
                kill_time INTEGER NOT NULL,
                map_id INTEGER NOT NULL
            )
        ''')
        c.execute('''
            CREATE TABLE IF NOT EXISTS system_config (
                key TEXT PRIMARY KEY,
                value TEXT NOT NULL
            )
        ''')
        c.execute('INSERT OR IGNORE INTO system_config (key, value) VALUES (?, ?)', ('qingming_active', '0'))
        c.execute('INSERT OR IGNORE INTO system_config (key, value) VALUES (?, ?)', ('qingming_drop_rate', '0.1'))
        c.execute('INSERT OR IGNORE INTO system_config (key, value) VALUES (?, ?)', ('qingming_item_id', '9999'))
        c.execute('''
            CREATE TABLE IF NOT EXISTS player_logs (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                player_name TEXT NOT NULL,
                event_type TEXT NOT NULL,
                description TEXT NOT NULL,
                details TEXT,
                created_at INTEGER NOT NULL
            )
        ''')
        c.execute('CREATE INDEX IF NOT EXISTS idx_player_logs_name ON player_logs(player_name)')
        c.execute('CREATE INDEX IF NOT EXISTS idx_player_logs_time ON player_logs(created_at)')
        c.execute('CREATE INDEX IF NOT EXISTS idx_player_logs_type ON player_logs(event_type)')
        c.execute('''
            CREATE TABLE IF NOT EXISTS monsters (
                id INTEGER PRIMARY KEY,
                name TEXT NOT NULL,
                type TEXT,
                level INTEGER,
                hp INTEGER,
                maxHp INTEGER,
                attack_min INTEGER,
                attack_max INTEGER,
                defense_min INTEGER,
                defense_max INTEGER,
                exp INTEGER,
                gold INTEGER,
                is_boss INTEGER DEFAULT 0,
                is_world_boss INTEGER DEFAULT 0,
                spawn_rate REAL DEFAULT 1.0,
                skills TEXT,
                icon TEXT
            )
        ''')

        c.execute("SELECT COUNT(*) FROM monsters")
        if c.fetchone()[0] == 0:
            monsters = CONFIG.get('monsters', [])
            for m in monsters:
                c.execute('''
                    INSERT INTO monsters (
                        id, name, type, level, hp, maxHp, attack_min, attack_max,
                        defense_min, defense_max, exp, gold, is_boss, is_world_boss,
                        spawn_rate, skills, icon
                    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                ''', (
                    m['id'], m['name'], m.get('type', 'normal'), m.get('level', 1),
                    m.get('hp', 100), m.get('maxHp', 100), m.get('attackMin', 5), m.get('attackMax', 10),
                    m.get('defenseMin', 1), m.get('defenseMax', 3), m.get('exp', 10), m.get('gold', 5),
                    1 if m.get('isBoss', False) else 0,
                    1 if m.get('isWorldBoss', False) else 0,
                    m.get('spawnRate', 1.0), json.dumps(m.get('skills', [])), m.get('icon', '')
                ))
            conn.commit()

        # 1. 玩家基础属性表
        c.execute('''
            CREATE TABLE IF NOT EXISTS players (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id INTEGER NOT NULL,
                name TEXT UNIQUE NOT NULL,
                level INTEGER DEFAULT 1,
                exp INTEGER DEFAULT 0,
                max_exp INTEGER DEFAULT 100,
                hp INTEGER DEFAULT 100,
                max_hp INTEGER DEFAULT 100,
                mp INTEGER DEFAULT 50,
                max_mp INTEGER DEFAULT 50,
                attack_min INTEGER DEFAULT 5,
                attack_max INTEGER DEFAULT 10,
                defense_min INTEGER DEFAULT 2,
                defense_max INTEGER DEFAULT 5,
                rebirth INTEGER DEFAULT 0,
                gold INTEGER DEFAULT 1000,
                attack_enhance INTEGER DEFAULT 0,
                defense_enhance INTEGER DEFAULT 0,
                exp_multiplier REAL DEFAULT 1.0,
                game_speed REAL DEFAULT 1.0,
                skills TEXT,
                accuracy INTEGER DEFAULT 1,
                dodge INTEGER DEFAULT 1,
                avatar TEXT DEFAULT '',
                warehouse_unlocked INTEGER DEFAULT 0,
                max_inventory_slots INTEGER DEFAULT 500,
                max_warehouse_slots INTEGER DEFAULT 500,
                created_at INTEGER,
                config_json TEXT,         -- 新增
                FOREIGN KEY(user_id) REFERENCES users(id)
            )
        ''')
        
        # 2. 英雄表
        c.execute('''
            CREATE TABLE IF NOT EXISTS heroes (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                player_id INTEGER NOT NULL UNIQUE,
                summoned INTEGER DEFAULT 0,
                level INTEGER DEFAULT 1,
                exp INTEGER DEFAULT 0,
                max_exp INTEGER DEFAULT 100,
                hp INTEGER DEFAULT 80,
                max_hp INTEGER DEFAULT 80,
                mp INTEGER DEFAULT 40,
                max_mp INTEGER DEFAULT 40,
                attack_min INTEGER DEFAULT 4,
                attack_max INTEGER DEFAULT 8,
                defense_min INTEGER DEFAULT 1,
                defense_max INTEGER DEFAULT 3,
                rebirth INTEGER DEFAULT 0,
                attack_enhance INTEGER DEFAULT 0,
                defense_enhance INTEGER DEFAULT 0,
                exp_multiplier REAL DEFAULT 1.0,
                game_speed REAL DEFAULT 1.0,
                skills TEXT,  -- JSON
                accuracy INTEGER DEFAULT 1,
                dodge INTEGER DEFAULT 1,
                avatar TEXT DEFAULT '',
                FOREIGN KEY(player_id) REFERENCES players(id)
            )
        ''')
        
        # 3. 背包物品表
        c.execute('''
            CREATE TABLE IF NOT EXISTS inventory (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                player_id INTEGER NOT NULL,
                uuid TEXT NOT NULL,
                item_id INTEGER,
                name TEXT,
                type TEXT,
                quantity INTEGER DEFAULT 1,
                enhance_level INTEGER DEFAULT 0,
                quality TEXT,
                data TEXT,  -- 存储其他变长属性（JSON）
                acquired_time INTEGER,
                dropped_by TEXT,
                FOREIGN KEY(player_id) REFERENCES players(id)
            )
        ''')
        c.execute('CREATE INDEX IF NOT EXISTS idx_inventory_player ON inventory(player_id)')
        
        # 4. 仓库物品表
        c.execute('''
            CREATE TABLE IF NOT EXISTS warehouse (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                player_id INTEGER NOT NULL,
                uuid TEXT NOT NULL,
                item_id INTEGER,
                name TEXT,
                type TEXT,
                quantity INTEGER DEFAULT 1,
                enhance_level INTEGER DEFAULT 0,
                quality TEXT,
                data TEXT,
                acquired_time INTEGER,
                dropped_by TEXT,
                FOREIGN KEY(player_id) REFERENCES players(id)
            )
        ''')
        c.execute('CREATE INDEX IF NOT EXISTS idx_warehouse_player ON warehouse(player_id)')
        
        # 5. 玩家装备表
        c.execute('''
            CREATE TABLE IF NOT EXISTS player_equipment (
                player_id INTEGER NOT NULL,
                slot TEXT NOT NULL,  -- weapon, armor, ...
                item_uuid TEXT,
                data TEXT,  -- 快照物品属性（JSON）
                PRIMARY KEY(player_id, slot),
                FOREIGN KEY(player_id) REFERENCES players(id)
            )
        ''')
        
        # 6. 英雄装备表
        c.execute('''
            CREATE TABLE IF NOT EXISTS hero_equipment (
                player_id INTEGER NOT NULL,
                slot TEXT NOT NULL,
                item_uuid TEXT,
                data TEXT,
                PRIMARY KEY(player_id, slot),
                FOREIGN KEY(player_id) REFERENCES players(id)
            )
        ''')
        
        # 7. 任务表
        c.execute('''
            CREATE TABLE IF NOT EXISTS tasks (
                player_id INTEGER NOT NULL PRIMARY KEY,
                completed_count INTEGER DEFAULT 0,
                points INTEGER DEFAULT 0,
                last_reset_date TEXT,
                last_abandoned INTEGER DEFAULT 0,
                current_task TEXT,  -- JSON
                FOREIGN KEY(player_id) REFERENCES players(id)
            )
        ''')
        
        # 8. 农场表
        c.execute('''
            CREATE TABLE IF NOT EXISTS farms (
                player_id INTEGER NOT NULL PRIMARY KEY,
                plots TEXT,      -- JSON 数组
                unlocked_plots INTEGER,
                FOREIGN KEY(player_id) REFERENCES players(id)
            )
        ''')
        
        # 9. 十二生肖装备表
        c.execute('''
            CREATE TABLE IF NOT EXISTS zodiac_equipped (
                player_id INTEGER NOT NULL,
                zodiac_id INTEGER NOT NULL,
                zodiac_uuid TEXT,
                slot_index INTEGER,
                PRIMARY KEY(player_id, zodiac_id),
                FOREIGN KEY(player_id) REFERENCES players(id)
            )
        ''')
        c.execute("PRAGMA table_info(zodiac_equipped)")
        columns = [col[1] for col in c.fetchall()]
        if 'data' not in columns:
            c.execute("ALTER TABLE zodiac_equipped ADD COLUMN data TEXT")
        # 10. 种子仓库表
        c.execute('''
            CREATE TABLE IF NOT EXISTS seed_storage (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                player_id INTEGER NOT NULL,
                seed_uuid TEXT NOT NULL,
                name TEXT,
                quantity INTEGER DEFAULT 1,
                data TEXT,
                FOREIGN KEY(player_id) REFERENCES players(id)
            )
        ''')
        
        # 11. 临时增益表
        c.execute('''
            CREATE TABLE IF NOT EXISTS temporary_buffs (
                player_id INTEGER NOT NULL,
                buff_type TEXT NOT NULL,
                value REAL,
                expire_time INTEGER,
                PRIMARY KEY(player_id, buff_type),
                FOREIGN KEY(player_id) REFERENCES players(id)
            )
        ''')
        c.execute("PRAGMA table_info(temporary_buffs)")
        existing_cols = {col[1] for col in c.fetchall()}
        if 'target' not in existing_cols:
            c.execute("ALTER TABLE temporary_buffs ADD COLUMN target TEXT DEFAULT 'player'")
        if 'source' not in existing_cols:
            c.execute("ALTER TABLE temporary_buffs ADD COLUMN source TEXT DEFAULT 'normal'")
        if 'unique_id' not in existing_cols:
            c.execute("ALTER TABLE temporary_buffs ADD COLUMN unique_id TEXT")
        c.execute("PRAGMA table_info(players)")
        player_cols = {col[1] for col in c.fetchall()}
        player_new_cols = {
            'crit_rate': 'REAL DEFAULT 0',
            'crit_damage': 'REAL DEFAULT 1.5',
            'lethal_rate': 'REAL DEFAULT 0',
            'lethal_damage': 'REAL DEFAULT 3.0',
            'poison_rate': 'REAL DEFAULT 0',
            'poison_damage': 'INTEGER DEFAULT 0'
        }
        for col_name, col_def in player_new_cols.items():
            if col_name not in player_cols:
                c.execute(f"ALTER TABLE players ADD COLUMN {col_name} {col_def}")

        # 确保 heroes 表包含所有必要列
        c.execute("PRAGMA table_info(heroes)")
        hero_cols = {col[1] for col in c.fetchall()}
        hero_new_cols = {
            'crit_rate': 'REAL DEFAULT 0',
            'crit_damage': 'REAL DEFAULT 1.5',
            'lethal_rate': 'REAL DEFAULT 0',
            'lethal_damage': 'REAL DEFAULT 3.0',
            'poison_rate': 'REAL DEFAULT 0',
            'poison_damage': 'INTEGER DEFAULT 0'
        }
        for col_name, col_def in hero_new_cols.items():
            if col_name not in hero_cols:
                c.execute(f"ALTER TABLE heroes ADD COLUMN {col_name} {col_def}")

        # 为 players 表添加其他列（如果缺失）
        for col_name, col_def in [('current_map', 'INTEGER DEFAULT 1'), ('current_monster', 'INTEGER DEFAULT 1'), ('monster_burn_turns', 'INTEGER DEFAULT 0'), ('save_time', 'INTEGER DEFAULT 0')]:
            if col_name not in player_cols:
                c.execute(f"ALTER TABLE players ADD COLUMN {col_name} {col_def}")

        # 添加 config_json 列（如果缺失）
        if 'config_json' not in player_cols:
            c.execute("ALTER TABLE players ADD COLUMN config_json TEXT")

        # ---- 新增：创建 player_combat 表 ----
        c.execute('''
            CREATE TABLE IF NOT EXISTS player_combat (
                player_id INTEGER PRIMARY KEY,
                current_combat TEXT,
                monster_group TEXT,
                FOREIGN KEY(player_id) REFERENCES players(id) ON DELETE CASCADE
            )
        ''')
        
        # 为 characters 表添加标记列（表示已迁移到分表）
        c.execute("PRAGMA table_info(characters)")
        columns = [col[1] for col in c.fetchall()]
        if 'migrated' not in columns:
            c.execute("ALTER TABLE characters ADD COLUMN migrated INTEGER DEFAULT 0")
        c.execute("PRAGMA table_info(players)")
        columns = [col[1] for col in c.fetchall()]
        if 'config_json' not in columns:
            c.execute("ALTER TABLE players ADD COLUMN config_json TEXT")
        # 在 init_game_db 中，commit() 之前或之后添加
        c.execute("CREATE INDEX IF NOT EXISTS idx_players_name_user ON players(name, user_id)")
        conn.commit()
    except Exception as e:
        logger.error(f"初始化分表失败: {e}")
    finally:
        chat.close_game_connection(conn)

# ---------- 初始化数据库连接池和表 ----------
chat.init_game_db_pool()
chat.init_chat_db_pool()
init_game_db()
chat.init_chat_db()
init_auction_table()

# =================== 全局怪物池 ====================
global_monster_pools = {}
pool_locks = {}
MONSTERS_PER_MAP = CONFIG.get('world.monsters_per_map')
REFILL_THRESHOLD = CONFIG.get('world.monster_refill_threshold')

map_player_counts = defaultdict(int)
map_counts_lock = Lock()

def init_global_monster_pools():
    maps = CONFIG.get('maps', [])
    for map_data in maps:
        map_id = map_data['id']
        pool = deque()
        for _ in range(MONSTERS_PER_MAP):
            monster_template = game_engine._select_monster_for_map(map_id)
            if monster_template:
                monster_instance = game_engine._create_monster_instance(monster_template)
                pool.append(monster_instance)
        global_monster_pools[map_id] = pool
        pool_locks[map_id] = Lock()
    threading.Thread(target=monster_pool_refiller, daemon=True).start()

def monster_pool_refiller():
    while True:
        time.sleep(CONFIG.get('world.pool_refill_interval_seconds'))
        maps = CONFIG.get('maps', [])
        for map_data in maps:
            map_id = map_data['id']
            with pool_locks[map_id]:
                pool = global_monster_pools.get(map_id, [])
                if len(pool) < REFILL_THRESHOLD:
                    need = MONSTERS_PER_MAP - len(pool)
                    for _ in range(need):
                        monster_template = game_engine._select_monster_for_map(map_id)
                        if monster_template:
                            monster_instance = game_engine._create_monster_instance(monster_template)
                            pool.append(monster_instance)
                    schedule_broadcast_map_stats(map_id)

def draw_monsters_from_pool(map_id, count):
    if map_id not in global_monster_pools:
        return []
    with pool_locks[map_id]:
        pool = global_monster_pools[map_id]
        if len(pool) < count:
            # 补充到满
            need = MONSTERS_PER_MAP - len(pool)
            for _ in range(need):
                monster_template = game_engine._select_monster_for_map(map_id)
                if monster_template:
                    monster_instance = game_engine._create_monster_instance(monster_template)
                    pool.append(monster_instance)
        # 取出 count 只（从左边）
        selected = []
        for _ in range(min(count, len(pool))):
            selected.append(pool.popleft())
        schedule_broadcast_map_stats(map_id)
        return selected

def return_monsters_to_pool(map_id, monsters):
    if not monsters:
        return
    with pool_locks[map_id]:
        global_monster_pools[map_id].extend(monsters)
        schedule_broadcast_map_stats(map_id)

def get_monsters_from_global_pool(map_id, count):
    return draw_monsters_from_pool(map_id, count)

def return_monsters_to_global_pool(map_id, monsters):
    return_monsters_to_pool(map_id, monsters)

def task_update_callback(player_name, task_state):
    socketio.emit('update_task', {
        'task': task_state.get('currentTask'),
        'completedCount': task_state.get('completedCount', 0),
        'points': task_state.get('points', 0)
    }, room=player_name)

game_engine = GameEngine(
    monster_pool_getter=get_monsters_from_global_pool,
    monster_pool_returner=return_monsters_to_global_pool,
    use_db=True,
    map_stats_callback=broadcast_map_stats,
    task_update_callback=task_update_callback
)
set_parking_engine(game_engine)
craft_system = CraftSystem(game_engine.config, game_engine.new_equip_templates)

# ========== 定义自动战斗停止回调（必须在 WorldBossManager 之前） ==========
def stop_auto_combat_for_player(player_name):
    try:
        with auto_combat_lock:
            task = auto_combat_tasks.pop(player_name, None)
            if task and not task.dead:
                task.kill()
        socketio.emit('auto_combat_stopped', {}, room=player_name)
    except Exception as e:
        logger.error(f"停止玩家 {player_name} 自动战斗时出错: {e}")

world_boss_manager = WorldBossManager(socketio, game_engine, stop_auto_combat_callback=stop_auto_combat_for_player)
game_engine.world_boss_manager = world_boss_manager

farm_system = FarmSystem(game_engine)
game_engine.farm_system = farm_system
init_global_monster_pools()
init_parking(app, game_engine, socketio)

@app.route('/data/<path:filename>')
def serve_data_file(filename):
    return send_from_directory(DATA_DIR, filename, conditional=True, max_age=604800)

@app.route('/api/map/stats/<int:map_id>', methods=['GET'])
def get_map_stats(map_id):
    monster_count = len(global_monster_pools.get(map_id, []))
    boss_info = world_boss_manager.get_boss_info(map_id)
    has_boss = boss_info.get('exists') and boss_info.get('boss_hp', 0) > 0
    with cache_lock:
        online_in_map = sum(1 for info in online_cache.values() if info.get('map') == str(map_id))
    return jsonify({
        'map_id': map_id,
        'monsters_remaining': monster_count,
        'has_world_boss': has_boss,
        'players_online': online_in_map
    })

@app.route('/api/map/all_stats', methods=['GET'])
def get_all_map_stats():
    stats = []
    for map_id, pool in global_monster_pools.items():
        boss_info = world_boss_manager.get_boss_info(map_id)
        has_boss = boss_info.get('exists') and boss_info.get('boss_hp', 0) > 0
        with map_counts_lock:
            players = map_player_counts.get(str(map_id), 0)
        stats.append({
            'map_id': map_id,
            'monsters_remaining': len(pool),
            'has_world_boss': has_boss,
            'players_online': players
        })
    return jsonify(stats)

def ensure_uuid_for_items(data):
    if not data:
        return
    if data.get('uuid_migrated'):
        return
    for item in data.get('inventory', []):
        if 'uuid' not in item:
            item['uuid'] = generate_uuid()
        if 'enhanceLevel' not in item:
            item['enhanceLevel'] = 0
        if 'quality' not in item or item['quality'] is None:
            item['quality'] = '普通'
    for item in data.get('warehouse', []):
        if 'uuid' not in item:
            item['uuid'] = generate_uuid()
        if 'enhanceLevel' not in item:
            item['enhanceLevel'] = 0
        if 'quality' not in item or item['quality'] is None:
            item['quality'] = '普通'
    for slot, item in data.get('equipment', {}).get('player', {}).items():
        if item and 'uuid' not in item:
            item['uuid'] = generate_uuid()
        if item and 'enhanceLevel' not in item:
            item['enhanceLevel'] = 0
        if item and ('quality' not in item or item['quality'] is None):
            item['quality'] = '普通'
    for slot, item in data.get('equipment', {}).get('hero', {}).items():
        if item and 'uuid' not in item:
            item['uuid'] = generate_uuid()
        if item and 'enhanceLevel' not in item:
            item['enhanceLevel'] = 0
        if item and ('quality' not in item or item['quality'] is None):
            item['quality'] = '普通'
            
    data['uuid_migrated'] = True

def load_player_data(name, user_id=None):
    state = player_state_manager.get(name)
    if state:
        if user_id is not None and state.get('userId') != user_id:
            return None, False
        return state, False
    state = load_state_from_tables(name)
    if state:
        if user_id is not None and state.get('userId') != user_id:
            return None, False
        player_state_manager.set(name, state)
        return state, False
    conn = chat.get_game_db()
    try:
        c = conn.cursor()
        c.execute('SELECT user_id, data, data_file FROM characters WHERE name = ?', (name,))
        row = c.fetchone()
        if row and row[1]:
            db_user_id, data_json, data_file = row
            if user_id is not None and db_user_id != user_id:
                return None, False
            state = json.loads(data_json)
            ensure_uuid_for_items(state)
            char_id = None
            c.execute('SELECT id FROM characters WHERE name = ?', (name,))
            row_id = c.fetchone()
            if row_id:
                char_id = row_id[0]
                save_state_to_tables(char_id, db_user_id, name, state)
                c.execute('UPDATE characters SET data = NULL, migrated = 1 WHERE name = ?', (name,))
                conn.commit()
                logger.info(f"角色 {name} 已从 JSON 自动迁移到分表")
            player_state_manager.set(name, state)
            return state, True
        return None, False
    except Exception as e:
        logger.error(f"加载角色失败: {name}, {e}")
        return None, False
    finally:
        chat.close_game_connection(conn)

def broadcast_online():
    with cache_lock:
        users = [{'name': name, 'level': info['level'], 'rebirth': info['rebirth'], 'map': info['map']}
                 for name, info in online_cache.items()]
    socketio.emit('online_list', users)

def dirty_save_worker():
    """优化版：仅标记脏，实际保存由定时线程批量执行"""
    while True:
        time.sleep(CONFIG.get('database.dirty_save_interval_seconds', 5))
        dirty_states = player_state_manager.get_dirty_states()
        if not dirty_states:
            continue
        
        for name, state in dirty_states.items():
            try:
                # 使用 chat.get_game_db() 而不是 get_game_db
                conn = chat.get_game_db()
                c = conn.cursor()
                c.execute('SELECT id, user_id FROM players WHERE name = ?', (name,))
                row = c.fetchone()
                if not row:
                    continue
                char_id, user_id = row
                conn.execute('BEGIN IMMEDIATE')
                conn.execute(f'SAVEPOINT save_{char_id}')
                try:
                    save_state_to_tables_with_conn(conn, char_id, user_id, name, state)
                    conn.execute(f'RELEASE SAVEPOINT save_{char_id}')
                    player_state_manager.mark_clean(name)
                except Exception as e:
                    conn.execute(f'ROLLBACK TO SAVEPOINT save_{char_id}')
                    logger.error(f"保存玩家 {name} 状态失败: {e}")
                conn.commit()
            except Exception as e:
                logger.error(f"脏保存失败: {e}")
                if 'conn' in locals():
                    try:
                        conn.rollback()
                    except:
                        pass
            finally:
                if 'conn' in locals():
                    chat.close_game_connection(conn)

threading.Thread(target=dirty_save_worker, daemon=True).start()

from eventlet import sleep
def _stop_auto_combat(player_name):
    with auto_combat_lock:
        task = auto_combat_tasks.pop(player_name, None)
        if task and not task.dead:
            try:
                task.kill()
            except GreenletExit:
                pass
            except Exception as e:
                logger.error(f"停止自动战斗协程失败 {player_name}: {e}")
    # 发送停止事件，但不清空怪物组（交由 handler 按条件处理）
    socketio.emit('auto_combat_stopped', {}, room=player_name)
    
def register_socket_handlers(socketio):
    @socketio.on('set_background')
    def handle_set_background(data):
        player_name = data.get('name')
        if not player_name:
            return
        with cache_lock:
            info = online_cache.get(player_name)
            if info and info.get('map'):
                map_id = info['map']
                leave_room(f'map_{map_id}', sid=request.sid)
                info['in_background'] = True

    @socketio.on('set_foreground')
    def handle_set_foreground(data):
        player_name = data.get('name')
        if not player_name:
            return
        with cache_lock:
            info = online_cache.get(player_name)
            if info and info.get('map'):
                map_id = info['map']
                join_room(f'map_{map_id}', sid=request.sid)
                info['in_background'] = False
        # 主动推送一个通知，让前端立即同步（可选）
        socketio.emit('force_sync', room=player_name)

    @socketio.on('connect')
    def handle_connect(auth=None):
        token = request.args.get('token')
        if not token and auth and isinstance(auth, dict):
            token = auth.get('token')
        if not token:
            return False

        payload = verify_token(token)
        if not payload:
            return False

        user_id = payload['user_id']
        username = payload['username']
        name = request.args.get('name')
        level = request.args.get('level', 1, type=int)
        rebirth = request.args.get('rebirth', 0, type=int)
        map_id = request.args.get('map', '1')

        if not name:
            return

        with cache_lock:
            old_info = online_cache.get(name)
            if old_info:
                old_sid = old_info['sid']
                old_map = old_info.get('map')
                _stop_auto_combat(name)
                # 先删除缓存和减少计数
                del online_cache[name]
                if old_map:
                    with map_counts_lock:
                        map_player_counts[str(old_map)] -= 1
                # 再断开旧连接（此时 handle_disconnect 会因为玩家不存在而跳过计数操作）
                if old_sid != request.sid:
                    disconnect(old_sid)

        join_room(name)
        join_room(f'map_{map_id}')

        with cache_lock:
            online_cache[name] = {
                'level': level,
                'rebirth': rebirth,
                'map': map_id,
                'sid': request.sid,
                'last_seen': time.time()
            }
        with map_counts_lock:
            map_player_counts[str(map_id)] += 1

        broadcast_online()
        schedule_broadcast_map_stats(int(map_id))
        try:
            boss_info = world_boss_manager.get_boss_info(int(map_id))
            if boss_info.get('exists'):
                socketio.emit('world_boss_status', boss_info, room=name)
        except Exception as e:
            logger.error(f"发送世界Boss状态失败: {e}")

        # 主动向该玩家推送一次当前地图统计（已有定时广播，这里确保进图立刻收到）
        broadcast_map_stats(int(map_id))
        
    @socketio.on('register')
    def handle_register(data):
        name = data.get('name')
        if not name:
            return
        join_room(name)
        state = player_state_manager.get(name)
        if state is None:
            state, _ = load_player_data(name)  # load_player_data 返回 (state, migrated)
            if state:
                player_state_manager.set(name, state)
        if state:
            state_copy = state.copy()
            state_copy.pop('log_messages', None)
            emit('game_state', state_copy, room=request.sid)

    @socketio.on('disconnect')
    def handle_disconnect():
        sid = request.sid
        removed_name = None
        map_id = None
        with cache_lock:
            for name, info in list(online_cache.items()):
                if info['sid'] == sid:
                    removed_name = name
                    map_id = info.get('map')
                    del online_cache[name]
                    break
        if removed_name and map_id:
            _stop_auto_combat(removed_name)
            with map_counts_lock:
                key = str(map_id)
                if map_player_counts.get(key, 0) > 0:
                    map_player_counts[key] -= 1
                else:
                    logger.warning(f"玩家 {removed_name} 从地图 {map_id} 断开时计数已为 0，跳过减少")
            schedule_broadcast_map_stats(int(map_id))
            # 立即广播一次统计，让地图内其他玩家立即看到玩家数量变化
            broadcast_map_stats(int(map_id))
            socketio.start_background_task(broadcast_online)

    @socketio.on('heartbeat')
    def handle_heartbeat(data):
        name = data.get('name')
        level = data.get('level')
        rebirth = data.get('rebirth')
        new_map = str(data.get('map')) if data.get('map') is not None else None
        if name:
            join_room(name)
        if name and new_map:
            now = time.time()
            changed = False
            old_map = None
            with cache_lock:
                info = online_cache.get(name)
                if info:
                    old_map = info.get('map')
                    if old_map != new_map:
                        leave_room(f'map_{old_map}', sid=request.sid)
                        join_room(f'map_{new_map}', sid=request.sid)
                        changed = True

                        # 广播旧地图统计（给旧地图房间）
                        broadcast_map_stats(int(old_map))
                        # 广播新地图统计（给新地图房间）
                        broadcast_map_stats(int(new_map))

                        # 向该玩家单独推送新地图的世界Boss状态（已有，保持不变）
                        try:
                            boss_info = world_boss_manager.get_boss_info(int(new_map))
                            if boss_info.get('exists'):
                                socketio.emit('world_boss_status', boss_info, room=name)
                        except Exception as e:
                            logger.error(f"发送世界Boss状态失败: {e}")
                    if info['level'] != level or info['rebirth'] != rebirth or info['map'] != new_map:
                        info['level'] = level
                        info['rebirth'] = rebirth
                        info['map'] = new_map
                        changed = True
                    info['last_seen'] = now
                    info['sid'] = request.sid
                else:
                    online_cache[name] = {
                        'level': level,
                        'rebirth': rebirth,
                        'map': new_map,
                        'sid': request.sid,
                        'last_seen': now
                    }
                    join_room(f'map_{new_map}', sid=request.sid)
                    changed = True
            if changed:
                if old_map is not None:
                    with map_counts_lock:
                        key = str(old_map)
                        if map_player_counts.get(key, 0) > 0:
                            map_player_counts[key] -= 1
                        else:
                            logger.warning(f"玩家 {name} 从地图 {old_map} 离开时计数已为 0，跳过减少")
                if new_map is not None:
                    with map_counts_lock:
                        map_player_counts[str(new_map)] += 1
                    schedule_broadcast_map_stats(int(new_map))
                broadcast_online()

    @socketio.on('request_online')
    def handle_request_online():
        with cache_lock:
            users = [{'name': name, 'level': info['level'], 'rebirth': info['rebirth'], 'map': info['map']}
                     for name, info in online_cache.items()]
        emit('online_list', users)

    @socketio.on('chat_send')
    def handle_chat_send(data):
        msg_type = data.get('type', 'public')
        sender = data.get('sender')
        content = data.get('content')
        target = data.get('target')

        if not sender or not content:
            emit('chat_error', {'error': '缺少必要参数'})
            return

        import time
        import threading
        temp_id = -int(time.time() * 1000000)
        temp_timestamp = time.time()

        message = {
            'id': temp_id,
            'sender': sender,
            'content': content,
            'type': msg_type,
            'timestamp': temp_timestamp
        }
        if msg_type == 'private':
            message['target'] = target

        if msg_type == 'public':
            socketio.emit('chat_message', message)
        else:
            socketio.emit('chat_message', message, room=target)
            socketio.emit('chat_message', message, room=sender)

        def save_to_db():
            result = chat.save_chat_message(sender, content, msg_type, target)
            if not result['success']:
                logger.error(f"异步保存聊天消息失败: {result.get('error')}")

        threading.Thread(target=save_to_db, daemon=True).start()
        emit('chat_send_ack', {'status': 'ok', 'temp_id': temp_id})

    @socketio.on('chat_private_history')
    def handle_private_history(data):
        current_user = data.get('current_user')
        target = data.get('target')
        since = data.get('since', 0)
        if not current_user or not target:
            emit('chat_private_history', {'error': '缺少参数'})
            return
        messages = chat.get_private_messages(current_user, target, since)
        emit('chat_private_history', {'target': target, 'messages': messages})

    @socketio.on('chat_board_history')
    def handle_board_history(data, callback=None):
        messages = chat.get_board_messages(limit=CONFIG.get('board.default_fetch_limit'))
        if callback:
            callback({'messages': messages})
        else:
            emit('chat_board_history', {'messages': messages})

    @socketio.on('chat_board_send')
    def handle_chat_board_send(data):
        sender = data.get('sender')
        content = data.get('content')
        if not sender or not content:
            emit('chat_board_error', {'error': '缺少参数'})
            return
        temp_id = -int(time.time() * 1000000)
        temp_ts = time.time()
        board_msg = {
            'id': temp_id,
            'sender': sender,
            'content': content,
            'timestamp': temp_ts
        }
        socketio.emit('board_message', board_msg)
        def save_board():
            result = chat.save_board_message(sender, content)
            if not result.get('success'):
                logger.error(f"留言板保存失败: {result.get('error')}")
        threading.Thread(target=save_board, daemon=True).start()
        emit('chat_board_ack', {'status': 'ok', 'temp_id': temp_id})

    @socketio.on('get_public_history')
    def handle_get_public_history(data):
        limit = data.get('limit', 50)
        conn = chat.get_chat_db()
        conn.row_factory = sqlite3.Row
        c = conn.cursor()
        c.execute('''
            SELECT id, sender, content, type, timestamp
            FROM chat_messages
            WHERE type = 'public'
            ORDER BY id DESC LIMIT ?
        ''', (limit,))
        rows = c.fetchall()
        conn.close()
        messages = [dict(row) for row in reversed(rows)]
        emit('get_public_history', {'messages': messages})

    @socketio.on('request_map_stats')
    def handle_request_map_stats(data):
        player_name = data.get('player_name')
        if not player_name:
            return
        state = player_state_manager.get(player_name)
        if not state:
            return
        map_id = state.get('currentMap')
        if map_id is None:
            return

        # 检查玩家当前是否正在与世界BOSS战斗
        mg = state.get('monsterGroup')
        if mg and any(m.get('isWorldBoss') for m in mg.get('monsters', [])):
            monster_count = 1
        else:
            monster_count = len(global_monster_pools.get(map_id, []))

        boss_info = world_boss_manager.get_boss_info(map_id)
        has_boss = boss_info.get('exists') and boss_info.get('boss_hp', 0) > 0
        with map_counts_lock:
            players_count = map_player_counts.get(str(map_id), 0)
        emit('map_stats', {
            'map_id': map_id,
            'monsters_remaining': monster_count,
            'has_world_boss': has_boss,
            'players_online': players_count
        }, room=player_name)

    @socketio.on('request_online_list')
    def handle_request_online_list(data=None):
        with cache_lock:
            users = [
                {
                    'name': name,
                    'level': info['level'],
                    'rebirth': info['rebirth'],
                    'map': info['map']
                }
                for name, info in online_cache.items()
            ]
        emit('online_list', users)

    @socketio.on('parking_buy')
    def handle_parking_buy(data):
        player_name = data.get('player_name')
        vehicle_id = data.get('vehicle_id')
        if not player_name or not vehicle_id:
            emit('parking_buy_result', {'success': False, 'error': '参数不足'}, room=request.sid)
            return
        try:
            from parking import buy_vehicle_logic
            success, message, data = buy_vehicle_logic(player_name, vehicle_id)
            emit('parking_buy_result', {'success': success, 'message': message, 'data': data}, room=request.sid)
        except Exception as e:
            logger.error(f"购买车辆失败: {e}")
            emit('parking_buy_result', {'success': False, 'error': str(e)}, room=request.sid)
    
    @socketio.on('parking_upgrade')
    def handle_parking_upgrade(data):
        player_name = data.get('player_name')
        instance_id = data.get('instance_id')
        if not player_name or not instance_id:
            emit('parking_upgrade_result', {'success': False, 'error': '参数不足'}, room=request.sid)
            return
        try:
            from parking import upgrade_vehicle_logic
            success, message, data = upgrade_vehicle_logic(player_name, instance_id)
            emit('parking_upgrade_result', {'success': success, 'message': message, 'data': data}, room=request.sid)
        except Exception as e:
            logger.error(f"升级车辆失败: {e}")
            emit('parking_upgrade_result', {'success': False, 'error': str(e)}, room=request.sid)
    
    @socketio.on('parking_claim')
    def handle_parking_claim(data):
        player_name = data.get('player_name')
        spot_owner = data.get('spot_owner')
        spot_index = data.get('spot_index')
        if not player_name or not spot_owner or spot_index is None:
            emit('parking_claim_result', {'success': False, 'error': '参数不足'}, room=request.sid)
            return
        try:
            from parking import claim_exp_logic
            success, message, data = claim_exp_logic(player_name, spot_owner, spot_index)
            emit('parking_claim_result', {'success': success, 'message': message, 'data': data}, room=request.sid)
        except Exception as e:
            logger.error(f"取回车辆失败: {e}")
            emit('parking_claim_result', {'success': False, 'error': str(e)}, room=request.sid)
    
    @socketio.on('parking_park')
    def handle_parking_park(data):
        player_name = data.get('player_name')
        target_owner = data.get('target_owner')
        spot_index = data.get('spot_index')
        instance_id = data.get('instance_id')
        if not all([player_name, target_owner, spot_index is not None, instance_id]):
            emit('parking_park_result', {'success': False, 'error': '参数不足'}, room=request.sid)
            return
        try:
            from parking import park_vehicle_logic
            success, message, data = park_vehicle_logic(player_name, target_owner, spot_index, instance_id)
            emit('parking_park_result', {'success': success, 'message': message, 'data': data}, room=request.sid)
        except Exception as e:
            logger.error(f"停放车辆失败: {e}")
            emit('parking_park_result', {'success': False, 'error': str(e)}, room=request.sid)

register_socket_handlers(socketio)

def get_user_id_by_name(name):
    st = player_state_manager.get(name)
    if st and st.get('userId'):
        return st['userId']
    st, _ = load_player_data(name)  # 忽略迁移标志
    if st:
        return st.get('userId')
    return None

@socketio.on('game_action')
def handle_game_action(data):
    action = data.get('action')
    params = data.get('params', {})
    player_name = data.get('player_name')

    if not player_name:
        return {'error': '缺少玩家名'}

    state = player_state_manager.get(player_name)
    if state is None:
        state, _ = load_player_data(player_name)
        if state is None:
            return {'error': f'玩家 {player_name} 状态不存在'}
        player_state_manager.set(player_name, state)

    # 获取用户ID（必须在 try 之前或内部开头，但不能在 try 后中断）
    def get_user_id_by_name(name):
        st = player_state_manager.get(name)
        if st and st.get('userId'):
            return st['userId']
        st, _ = load_player_data(name)
        if st:
            return st.get('userId')
        return None

    user_id = get_user_id_by_name(player_name)
    if not user_id:
        return {'error': '无法获取用户ID，请重新登录'}

    try:
        old_state = copy.deepcopy(state)

        if action == 'use_potion':
            if 'uuid' in params and params['uuid']:
                new_state, msg = game_engine.use_potion(
                    state,
                    params['uuid'],
                    params['target'],
                    is_uuid=True
                )
            elif 'index' in params:
                new_state, msg = game_engine.use_potion(
                    state,
                    params['index'],
                    params['target'],
                    expected_id=params.get('expected_id'),
                    expected_name=params.get('expected_name'),
                    is_uuid=False
                )
            else:
                return {'error': '缺少物品标识（uuid 或 index）'}
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'equip':
            new_state, msg = game_engine.equip_item(
                state,
                params['uuid'],
                params['target'],
                params.get('slot_key')
            )
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'refine_equipment':
            target = params.get('target')
            slot_key = params.get('slot_key')
            if not target or not slot_key:
                result = {'error': '缺少参数 target 或 slot_key'}
            else:
                new_state, msg = game_engine.refine_equipment(state, target, slot_key)
                if '成功' in msg:
                    player_state_manager.set(player_name, new_state)
                    diff = compute_state_diff(old_state, new_state)
                    result = {'diff': diff, 'message': msg}
                else:
                    result = {'error': msg}

        elif action == 'get_enhance_info':
            slot_key = params['slot_key']
            target = params['target']
            info, err = game_engine.get_enhance_info(state, slot_key, target)
            if err:
                result = {'error': err}
            else:
                result = info

        elif action == 'sell':
            if 'uuid' in params and params['uuid']:
                new_state, msg = game_engine.sell_item(state, params['uuid'], is_uuid=True)
            elif 'index' in params:
                new_state, msg = game_engine.sell_item(
                    state,
                    params['index'],
                    is_uuid=False,
                    expected_id=params.get('expected_id'),
                    expected_name=params.get('expected_name')
                )
            else:
                return {'error': '缺少物品标识（uuid 或 index）'}
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'warehouse_deposit':
            if 'uuid' in params and params['uuid']:
                new_state, msg = game_engine.warehouse_deposit(state, uuid=params['uuid'])
            elif 'index' in params:
                new_state, msg = game_engine.warehouse_deposit(
                    state,
                    index=params['index'],
                    expected_id=params.get('expected_id'),
                    expected_name=params.get('expected_name')
                )
            else:
                return {'error': '缺少物品标识（uuid 或 index）'}
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'warehouse_withdraw':
            if 'uuid' in params and params['uuid']:
                new_state, msg = game_engine.warehouse_withdraw(state, uuid=params['uuid'])
            elif 'index' in params:
                new_state, msg = game_engine.warehouse_withdraw(
                    state,
                    index=params['index'],
                    expected_id=params.get('expected_id'),
                    expected_name=params.get('expected_name')
                )
            else:
                return {'error': '缺少物品标识（uuid 或 index）'}
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'warehouse_sell':
            if 'uuid' in params and params['uuid']:
                new_state, msg = game_engine.warehouse_sell(state, uuid=params['uuid'])
            elif 'index' in params:
                new_state, msg = game_engine.warehouse_sell(
                    state,
                    index=params['index'],
                    expected_id=params.get('expected_id'),
                    expected_name=params.get('expected_name')
                )
            else:
                return {'error': '缺少物品标识（uuid 或 index）'}
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'enhance_equipment':
            use_protect = params.get('use_protect', False)
            new_state, msg = game_engine.enhance_equipment(state, params['slot_key'], params['target'], use_protect)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'unequip':
            new_state, msg = game_engine.unequip_item(state, params['slot'], params['target'])
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'unlock_warehouse':
            new_state, msg = game_engine.unlock_warehouse(state)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'combat':
            old_state = copy.deepcopy(state)
            new_state = game_engine.combat(state)
            logs = new_state.pop('log_messages', [])
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'logs': logs}

        elif action == 'change_map':
            map_id = params.get('map_id')
            old_map = state['currentMap']
            _stop_auto_combat(player_name)
            new_state, msg = game_engine.change_map(state, map_id)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}
            if old_map != map_id:
                with cache_lock:
                    if player_name in online_cache:
                        online_cache[player_name]['map'] = str(map_id)
                with map_counts_lock:
                    old_key = str(old_map)
                    new_key = str(map_id)
                    if map_player_counts.get(old_key, 0) > 0:
                        map_player_counts[old_key] -= 1
                    else:
                        logger.warning(f"玩家 {player_name} 从地图 {old_map} 离开时计数已为 0，跳过减少")
                    map_player_counts[new_key] += 1
                monster_count = len(global_monster_pools.get(map_id, []))
                with map_counts_lock:
                    players_count = map_player_counts.get(str(map_id), 0)
                socketio.emit('map_stats', {
                    'map_id': map_id,
                    'monsters_remaining': monster_count,
                    'players_online': players_count
                }, room=player_name)
                broadcast_online()
                schedule_broadcast_map_stats(old_map)
                schedule_broadcast_map_stats(map_id)
                try:
                    boss_info = world_boss_manager.get_boss_info(map_id)
                    if boss_info.get('exists'):
                        socketio.emit('world_boss_status', boss_info, room=player_name)
                except Exception as e:
                    logger.error(f"推送世界Boss状态给 {player_name} 失败: {e}")

        elif action == 'buy_potion':
            potion_id = params.get('potion_id')
            quantity = params.get('quantity', 1)
            new_state, msg = game_engine.buy_potion(state, potion_id, quantity)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'sell_all_equipment':
            new_state, msg = game_engine.sell_all_equipment(state)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'sell_all_items':
            new_state, msg = game_engine.sell_all_items(state)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'repair_all':
            new_state, msg = game_engine.repair_all(state)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'summon_hero':
            new_state, msg = game_engine.summon_hero(state)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'rebirth':
            target = params.get('target')
            if target:
                new_state, msg = game_engine.rebirth(state, target)
            else:
                new_state, msg = game_engine.rebirth(state)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'enhance':
            enhance_type = params.get('type')
            new_state, msg = game_engine.enhance(state, enhance_type)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'craft_equipment':
            use_points = params.get('usePoints', False)
            new_state, msg = craft_system.craft_equipment(state, params['uuids'], usePoints=use_points)
            if any(key in msg for key in ['不足', '不存在', '不一致', '无法', '❌']):
                result = {'error': msg}
            else:
                player_state_manager.set(player_name, new_state)
                diff = compute_state_diff(old_state, new_state)
                result = {'diff': diff, 'message': msg}

        elif action == 'sell_all_by_quality':
            quality = params.get('quality')
            new_state, message = game_engine.sell_all_by_quality(state, quality)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': message}

        elif action == 'lottery':
            times = params.get('times', 1)
            new_state, lottery_result = game_engine.lottery(state, times)
            if 'error' in lottery_result:
                result = {'error': lottery_result['error']}
            else:
                player_state_manager.set(player_name, new_state)
                diff = compute_state_diff(old_state, new_state)
                if lottery_result.get('specialRings'):
                    send_public(f'恭喜 {player_name} 在抽奖中获得 {", ".join(lottery_result["specialRings"])}！')
                result = {
                    'diff': diff,
                    'results': lottery_result['results'],
                    'totalGoldReward': lottery_result['totalGoldReward'],
                    'specialRings': lottery_result['specialRings']
                }

        elif action == 'buy_tool':
            tool_id = params.get('tool_id')
            quantity = params.get('quantity', 1)
            new_state, msg = farm_system.buy_tool(state, tool_id, quantity)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'maintain':
            plot_index = params.get('plot_index')
            action_type = params.get('action_type')
            new_state, msg = farm_system.maintain(state, plot_index, action_type)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'get_tool_list':
            tools = game_engine.config.get('farm_tools', [])
            result = {'tools': tools}

        elif action == 'generate_task':
            new_state, task_result = game_engine.generate_task(state)
            if 'error' in task_result:
                result = {'error': task_result['error']}
            else:
                player_state_manager.set(player_name, new_state)
                diff = compute_state_diff(old_state, new_state)
                result = {
                    'diff': diff,
                    'task': task_result.get('task'),
                    'completedCount': task_result.get('completedCount', 0)
                }

        elif action == 'complete_task':
            new_state, task_result = game_engine.complete_task(state)
            if 'error' in task_result:
                result = {'error': task_result['error']}
            else:
                player_state_manager.set(player_name, new_state)
                diff = compute_state_diff(old_state, new_state)
                result = {'diff': diff, 'message': task_result.get('message')}

        elif action == 'abandon_task':
            new_state, task_result = game_engine.abandon_task(state)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': task_result.get('message')}

        elif action == 'get_task':
            new_state, task_result = game_engine.get_task(state)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {
                'diff': diff,
                'task': task_result.get('task'),
                'completedCount': task_result.get('completedCount', 0)
            }

        elif action == 'use_activity_item':
            uuid = params.get('uuid')
            new_state, msg, reward = game_engine.use_activity_item(state, uuid)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg, 'reward': reward}

        elif action == 'use_consumable':
            uuid = params.get('uuid')
            new_state, msg, reward = game_engine.use_consumable(state, uuid)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg, 'reward': reward}

        elif action == 'equip_zodiac':
            zodiac_uuid = params.get('uuid')
            zodiac_id = params.get('zodiac_id')
            new_state, msg = game_engine.equip_zodiac(state, zodiac_uuid, zodiac_id)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'unequip_zodiac':
            zodiac_id = params.get('zodiac_id')
            new_state, msg = game_engine.unequip_zodiac(state, zodiac_id)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'enhance_zodiac':
            slot_id = params.get('slot_id')
            new_state, msg = game_engine.enhance_zodiac(state, slot_id)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'decompose_item':
            uuid = params.get('uuid')
            new_state, msg = game_engine.decompose_item(state, uuid)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'get_craftable_items':
            craftable = craft_system.get_craftable_items(state)
            result = {'craftable': craftable}

        elif action == 'get_farm_state':
            plots = farm_system.get_farm_state(state)
            result = {'plots': plots}

        elif action == 'plant':
            plot_index = params.get('plot_index')
            seed_uuid = params.get('seed_uuid')
            new_state, msg = farm_system.plant(state, plot_index, seed_uuid)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'harvest':
            plot_index = params.get('plot_index')
            new_state, msg = farm_system.harvest(state, plot_index)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'use_fruit':
            fruit_uuid = params.get('fruit_uuid')
            new_state, msg, reward = farm_system.use_fruit(state, fruit_uuid)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg, 'reward': reward}

        elif action == 'buy_seed':
            crop_id = params.get('crop_id')
            quantity = params.get('quantity', 1)
            new_state, msg = farm_system.buy_seed(state, crop_id, quantity)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'get_seed_list':
            seeds = farm_system.get_seed_list()
            result = {'seeds': seeds}

        elif action == 'get_seed_storage':
            result = farm_system.get_seed_storage(state)

        elif action == 'withdraw_seed':
            seed_uuid = params.get('seed_uuid')
            quantity = params.get('quantity', 1)
            new_state, msg = farm_system.withdraw_seed(state, seed_uuid, quantity)
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)
            result = {'diff': diff, 'message': msg}

        elif action == 'batch_sell':
            uuids = params.get('uuids', [])
            if not uuids:
                result = {'error': '缺少物品UUID列表'}
            else:
                new_state, msg, sold_count, total_gold = game_engine.batch_sell_by_uuids(state, uuids)
                if new_state:
                    player_state_manager.set(player_name, new_state)
                    diff = compute_state_diff(old_state, new_state)
                    result = {'diff': diff, 'message': msg, 'sold_count': sold_count, 'total_gold': total_gold}
                else:
                    result = {'error': msg}

        elif action == 'auction_add':
            from auction import add_auction_logic
            new_state, msg = add_auction_logic(state, params, user_id)
            if new_state:
                player_state_manager.set(player_name, new_state)
                diff = compute_state_diff(old_state, new_state)
                result = {'diff': diff, 'message': msg}
            else:
                result = {'error': msg}

        elif action == 'auction_bid':
            from auction import place_bid_logic
            new_state, msg = place_bid_logic(state, params, user_id)
            if new_state:
                player_state_manager.set(player_name, new_state)
                diff = compute_state_diff(old_state, new_state)
                result = {'diff': diff, 'message': msg}
            else:
                result = {'error': msg}

        elif action == 'auction_buyout':
            from auction import buyout_auction_logic
            new_state, msg = buyout_auction_logic(state, params, user_id)
            if new_state:
                player_state_manager.set(player_name, new_state)
                diff = compute_state_diff(old_state, new_state)
                result = {'diff': diff, 'message': msg}
            else:
                result = {'error': msg}

        elif action == 'auction_cancel':
            from auction import cancel_auction_logic
            new_state, msg = cancel_auction_logic(state, params, user_id)
            if new_state:
                player_state_manager.set(player_name, new_state)
                diff = compute_state_diff(old_state, new_state)
                result = {'diff': diff, 'message': msg}
            else:
                result = {'error': msg}

        elif action == 'auction_claim':
            from auction import claim_expired_logic
            new_state, msg = claim_expired_logic(state, params, user_id)
            if new_state:
                player_state_manager.set(player_name, new_state)
                diff = compute_state_diff(old_state, new_state)
                result = {'diff': diff, 'message': msg}
            else:
                result = {'error': msg}

        elif action == 'craft_from_fragments':
            equip_type = params.get('equip_type')
            if not equip_type:
                result = {'error': '缺少装备类型参数'}
            else:
                new_state, msg = game_engine.craft_from_fragments(state, equip_type)
                if '成功' in msg:
                    player_state_manager.set(player_name, new_state)
                    diff = compute_state_diff(old_state, new_state)
                    result = {'diff': diff, 'message': msg}
                else:
                    result = {'error': msg}

        elif action == 'auto_sell':
            new_state, msg, sold_count, total_gold = game_engine.auto_sell(state)
            if sold_count > 0:
                player_state_manager.set(player_name, new_state)
                diff = compute_state_diff(old_state, new_state)
                result = {'diff': diff, 'message': msg, 'sold_count': sold_count, 'total_gold': total_gold}
            else:
                result = {'message': msg, 'sold_count': 0, 'total_gold': 0}

        elif action == 'get_state':
            result = {'state': state}

        else:
            result = {'error': '未知动作'}

        return result

    except Exception as e:
        logger.exception(f"处理游戏动作失败: {action}, {player_name}")
        return {'error': str(e)}

@app.route('/api/admin/stats', methods=['GET'])
def admin_stats():
    auth = request.authorization
    if not auth or auth.username != 'admin' or auth.password != 'admin123':
        return jsonify({'error': '未授权'}), 401
    return jsonify({
        'state_manager': player_state_manager.get_stats(),
        'dirty_states': len(player_state_manager.get_dirty_states()),
        'online_players': len(online_cache)
    })

@app.route('/api/admin/cleanup_bots', methods=['POST'])
def admin_cleanup_bots():
    conn = None
    try:
        conn = chat.get_game_db()
        c = conn.cursor()
        c.execute("SELECT id, username FROM users WHERE username LIKE 'bot_user_%'")
        bot_users = c.fetchall()
        if not bot_users:
            return jsonify({'status': 'ok', 'message': '没有需要清理的假人数据'})
        user_ids = [row[0] for row in bot_users]
        placeholders = ','.join('?' for _ in user_ids)
        c.execute(f"SELECT name FROM characters WHERE user_id IN ({placeholders})", user_ids)
        char_names = [row[0] for row in c.fetchall()]
        if char_names:
            name_placeholders = ','.join('?' for _ in char_names)
            c.execute(f"DELETE FROM auction_items WHERE seller_character_name IN ({name_placeholders})", char_names)
            c.execute(f"DELETE FROM auction_bids WHERE bidder_character_name IN ({name_placeholders})", char_names)
            c.execute(f"DELETE FROM player_logs WHERE player_name IN ({name_placeholders})", char_names)
        c.execute(f"DELETE FROM characters WHERE user_id IN ({placeholders})", user_ids)
        c.execute(f"DELETE FROM users WHERE id IN ({placeholders})", user_ids)
        conn.commit()
        return jsonify({
            'status': 'ok',
            'message': f'已删除 {len(user_ids)} 个假人账号，{len(char_names)} 个角色'
        })
    except Exception as e:
        if conn:
            conn.rollback()
        logger.exception("清理假人失败")
        return jsonify({'error': str(e)}), 500
    finally:
        if conn:
            chat.close_game_connection(conn)

@app.route('/')
def index():
    version = int(time.time())
    return render_template('index.html', version=version)

@app.route('/api/config')
def get_config():
    config = CONFIG.get_all()
    config_json = json.dumps(config, sort_keys=True)
    etag = hashlib.md5(config_json.encode()).hexdigest()
    if request.headers.get('If-None-Match') == f'"{etag}"':
        return '', 304
    response = jsonify(config)
    response.headers['ETag'] = f'"{etag}"'
    response.headers['Cache-Control'] = 'public, max-age=60'
    return response

@app.route('/api/static/equipment')
def get_static_equipment():
    return jsonify(db.get_equipment())

@app.route('/api/static/potions')
def get_static_potions():
    return jsonify(db.get_potions())

@app.route('/api/static/skills')
def get_static_skills():
    return jsonify(db.get_skills())

@app.route('/api/static/monsters')
def get_static_monsters():
    return jsonify(db.get_monsters())

@app.route('/api/register', methods=['POST'])
def register():
    data = request.json
    username = data.get('username')
    password = data.get('password')
    if not username or not password:
        return jsonify({'error': '用户名和密码不能为空'}), 400

    conn = None
    try:
        conn = chat.get_game_db()
        conn.execute('BEGIN IMMEDIATE')
        c = conn.cursor()
        c.execute('INSERT INTO users (username, password_hash) VALUES (?, ?)',
                  (username, hash_password(password)))
        user_id = c.lastrowid
        conn.commit()
        token = generate_token(user_id, username)
        return jsonify({'status': 'ok', 'message': '注册成功', 'token': token, 'username': username})
    except sqlite3.IntegrityError:
        if conn:
            conn.rollback()
        return jsonify({'error': '用户名已存在'}), 400
    except Exception as e:
        if conn:
            conn.rollback()
        return jsonify({'error': str(e)}), 500
    finally:
        if conn:
            chat.close_game_connection(conn)

@app.route('/api/login', methods=['POST'])
def login():
    data = request.json
    username = data.get('username')
    password = data.get('password')
    if not username or not password:
        return jsonify({'error': '用户名和密码不能为空'}), 400

    conn = chat.get_game_db()
    try:
        c = conn.cursor()
        c.execute('SELECT id, password_hash FROM users WHERE username = ?', (username,))
        row = c.fetchone()
    finally:
        chat.close_game_connection(conn)

    if row and verify_password(row[1], password):
        user_id = row[0]
        token = generate_token(user_id, username)
        return jsonify({'status': 'ok', 'message': '登录成功', 'token': token, 'username': username})
    else:
        return jsonify({'error': '用户名或密码错误'}), 401

@app.route('/api/logout', methods=['POST'])
def logout():
    return jsonify({'status': 'ok', 'message': '已登出'})

@app.route('/api/create_character', methods=['POST'])
def create_character_http():
    user_id, username, _ = get_current_user_from_jwt()
    if not user_id:
        return jsonify({'error': '未登录或 token 无效'}), 401

    data = request.json
    name = data.get('name')
    if not name:
        return jsonify({'error': '缺少角色名'}), 400

    max_name_len = CONFIG.get('game.max_character_name_length')
    if len(name) > max_name_len:
        return jsonify({'error': f'角色名不能超过{max_name_len}个字'}), 400

    forbidden_words = CONFIG.get('game.forbidden_names')
    for word in forbidden_words:
        if word in name:
            return jsonify({'error': '角色名包含敏感词汇，请重新输入'}), 400

    conn = None
    try:
        conn = chat.get_game_db()
        c = conn.cursor()

        c.execute('SELECT name FROM characters WHERE name = ?', (name,))
        if c.fetchone():
            return jsonify({'error': '角色名已存在'}), 400

        # 从配置读取基础属性
        start_stats = CONFIG.get('player_starting_stats')
        player = {
            "name": name,
            "level": start_stats['level'],
            "exp": start_stats['exp'],
            "maxExp": start_stats['maxExp'],
            "hp": start_stats['hp'],
            "maxHp": start_stats['maxHp'],
            "mp": start_stats['mp'],
            "maxMp": start_stats['maxMp'],
            "attackMin": start_stats['attackMin'],
            "attackMax": start_stats['attackMax'],
            "defenseMin": start_stats['defenseMin'],
            "defenseMax": start_stats['defenseMax'],
            "rebirth": start_stats['rebirth'],
            "gold": start_stats['gold'],
            "attackEnhance": start_stats['attackEnhance'],
            "defenseEnhance": start_stats['defenseEnhance'],
            "expMultiplier": start_stats['expMultiplier'],
            "gameSpeed": start_stats['gameSpeed'],
            "skills": start_stats['skills'],
            "accuracy": start_stats['accuracy'],
            "dodge": start_stats['dodge']
        }

        hero = {"summoned": False}
        equipment = {
            "player": {k: None for k in
                       ["weapon", "armor", "helmet", "necklace", "medal", "leftBracelet", "rightBracelet",
                        "leftRing", "rightRing", "belt", "shoes"]},
            "hero": {k: None for k in
                     ["weapon", "armor", "helmet", "necklace", "medal", "leftBracelet", "rightBracelet",
                      "leftRing", "rightRing", "belt", "shoes"]}
        }
        inventory = []

        initial_config = game_engine.config.get('initialItems', {})
        equipment_list = initial_config.get('equipment', [])
        potion_list = initial_config.get('potions', [])

        for equip_cfg in equipment_list:
            equip_name = equip_cfg.get('name')
            equip_type = equip_cfg.get('type')
            quality = equip_cfg.get('quality', '普通')
            if not equip_name or not equip_type:
                continue
            template = None
            for category, items in game_engine.config.get('equipmentData', {}).items():
                for item in items:
                    if item.get('name') == equip_name and item.get('type') == equip_type:
                        template = item
                        break
                if template:
                    break
            if not template:
                continue
            new_item = template.copy()
            new_item['uuid'] = generate_uuid()
            new_item['enhanceLevel'] = 0
            new_item['quality'] = quality
            new_item['acquiredTime'] = int(time.time())
            new_item['droppedBy'] = '系统赠送'
            new_item['quantity'] = 1
            if quality != '普通':
                bonus_map = {'极品': 2, '神品': 5, '超神品': 10}
                bonus = bonus_map.get(quality, 0)
                if bonus:
                    if 'attackMin' in new_item and new_item.get('attackMin', 0) > 0:
                        new_item['attackMin'] = new_item.get('attackMin', 0) + bonus
                        new_item['attackMax'] = new_item.get('attackMax', 0) + bonus
                    if 'defenseMin' in new_item and new_item.get('defenseMin', 0) > 0:
                        new_item['defenseMin'] = new_item.get('defenseMin', 0) + bonus
                        new_item['defenseMax'] = new_item.get('defenseMax', 0) + bonus
            inventory.append(new_item)

        for potion_cfg in potion_list:
            if 'activity_id' in potion_cfg:
                activity_id = potion_cfg['activity_id']
                item_info = activity_manager.get_activity_item(activity_id)
                if not item_info:
                    continue
                new_potion = item_info.copy()
                new_potion['uuid'] = generate_uuid()
                new_potion['quantity'] = potion_cfg.get('quantity', 1)
                new_potion['droppedBy'] = '系统赠送'
                new_potion['activity_item'] = True
                new_potion['activity_id'] = activity_id
                new_potion['type'] = 'potion'
                if 'price' not in new_potion:
                    new_potion['price'] = 0
                if 'maxStack' not in new_potion:
                    new_potion['maxStack'] = 9999
                if 'hp' not in new_potion:
                    new_potion['hp'] = 0
                inventory.append(new_potion)
            else:
                potion_name = potion_cfg.get('name')
                if not potion_name:
                    continue
                template = None
                for p in game_engine.config.get('potions', []):
                    if p.get('name') == potion_name:
                        template = p
                        break
                if not template:
                    continue
                new_potion = template.copy()
                new_potion['uuid'] = generate_uuid()
                new_potion['quantity'] = potion_cfg.get('quantity', 1)
                new_potion['droppedBy'] = '系统赠送'
                if 'maxStack' not in new_potion:
                    new_potion['maxStack'] = 99
                inventory.append(new_potion)

        current_map = 1
        current_monster = 1
        if game_engine.config and game_engine.config.get('maps'):
            map_data = next((m for m in game_engine.config['maps'] if m['id'] == current_map), None)
            if map_data and map_data.get('monsters') and len(map_data['monsters']) > 0:
                current_monster = map_data['monsters'][0]
        monster = None
        if game_engine.config and game_engine.config.get('monsters'):
            monster = next((m for m in game_engine.config['monsters'] if m['id'] == current_monster), None)
        current_combat = {}
        if monster:
            current_combat = {
                "monsterHp": monster['hp'],
                "monsterMaxHp": monster['maxHp'],
                "monsterUuid": generate_uuid()
            }

        task = {
            'completedCount': 0,
            'lastResetDate': time.strftime('%Y-%m-%d'),
            'lastAbandoned': 0,
            'currentTask': None,
            'points': 0
        }

        new_state = {
            "player": player,
            "hero": hero,
            "inventory": inventory,
            "warehouse": [],
            "warehouseUnlocked": False,
            "equipment": equipment,
            "currentMap": current_map,
            "currentMonster": current_monster,
            "currentCombat": current_combat,
            "monsterBurnTurns": 0,
            "maxInventorySlots": CONFIG.get('game.max_inventory_slots'),
            "maxWarehouseSlots": CONFIG.get('game.max_warehouse_slots'),
            "userId": user_id,
            "saveTime": int(time.time()),
            "task": task,
            "temporary_buffs": {},
            "zodiac_equipped": {},
            "seed_storage": []
        }

        c.execute('INSERT INTO characters (user_id, name, data_file, data, created_at) VALUES (?, ?, ?, ?, ?)',
                  (user_id, name, '', json.dumps(new_state), int(time.time())))
        char_id = c.lastrowid
        conn.commit()
        save_state_to_tables(char_id, user_id, name, new_state)
        conn.commit()
        from online_state import add_player_via_http
        add_player_via_http(
            name=name,
            level=new_state['player']['level'],
            rebirth=new_state['player']['rebirth'],
            map_id=new_state.get('currentMap', 1)
        )
        try:
            broadcast_online()
        except Exception as e:
            logger.error(f"广播在线列表失败: {e}")
        return jsonify({'state': new_state, 'message': '角色创建成功'})

    except sqlite3.IntegrityError as e:
        if conn:
            conn.rollback()
        return jsonify({'error': f'角色创建失败: 角色名可能已存在 - {str(e)}'}), 400
    except Exception as e:
        if conn:
            conn.rollback()
        logger.exception(f"角色创建失败: {e}")
        return jsonify({'error': f'角色创建失败: {str(e)}'}), 500
    finally:
        if conn:
            chat.close_game_connection(conn)

@app.route('/api/user/characters')
def get_user_characters():
    user_id, username, _ = get_current_user_from_jwt()
    if not user_id:
        return jsonify({'error': '未登录'}), 401

    conn = chat.get_game_db()
    try:
        c = conn.cursor()
        # 1. 获取已迁移到 players 表的角色
        c.execute('''
            SELECT name, level, rebirth FROM players WHERE user_id = ? ORDER BY created_at DESC
        ''', (user_id,))
        migrated_rows = c.fetchall()
        chars = [{'name': row[0], 'level': row[1], 'rebirth': row[2]} for row in migrated_rows]

        # 2. 检查 characters 表中未迁移的旧数据（migrated=0 或 data 不为空）
        c.execute('''
            SELECT id, name, data, data_file FROM characters 
            WHERE user_id = ? AND (migrated = 0 OR data IS NOT NULL)
        ''', (user_id,))
        pending = c.fetchall()

        for char_id, name, data_json, data_file in pending:
            # 尝试从 data 字段加载 JSON
            state = None
            if data_json:
                try:
                    state = json.loads(data_json)
                except:
                    pass
            # 如果 data 字段为空，尝试从 data_file 指定的文件加载
            if not state and data_file:
                file_path = os.path.join(DATA_DIR, data_file)
                if os.path.exists(file_path):
                    try:
                        with open(file_path, 'r', encoding='utf-8') as f:
                            state = json.load(f)
                    except:
                        pass
            if state:
                ensure_uuid_for_items(state)
                # 迁移到分表
                save_state_to_tables(char_id, user_id, name, state)
                # 更新 characters 表标记已迁移
                c.execute('UPDATE characters SET data = NULL, migrated = 1 WHERE id = ?', (char_id,))
                conn.commit()
                # 添加到返回列表
                chars.append({
                    'name': name,
                    'level': state['player']['level'],
                    'rebirth': state['player']['rebirth']
                })
            else:
                # 无效数据，直接标记为已迁移避免反复尝试
                c.execute('UPDATE characters SET migrated = 1 WHERE id = ?', (char_id,))
                conn.commit()

        # 去重
        seen = set()
        unique_chars = []
        for ch in chars:
            if ch['name'] not in seen:
                seen.add(ch['name'])
                unique_chars.append(ch)

        return jsonify({'username': username, 'characters': unique_chars})
    finally:
        chat.close_game_connection(conn)

@app.route('/api/character/<name>', methods=['DELETE'])
def delete_character(name):
    user_id, username, _ = get_current_user_from_jwt()
    if not user_id:
        return jsonify({'error': '未登录'}), 401

    with cache_lock:
        if name in online_cache:
            return jsonify({'error': '角色当前在线，请先退出游戏后再删除'}), 400

    filename = get_filename(name)

    conn_game = None
    try:
        conn_game = chat.get_game_db()
        c = conn_game.cursor()
        c.execute('BEGIN IMMEDIATE')
        c.execute('DELETE FROM characters WHERE name = ? AND user_id = ?', (name, user_id))
        # 删除分表数据
        c.execute('SELECT id FROM players WHERE name = ?', (name,))
        player_row = c.fetchone()
        if player_row:
            player_id = player_row[0]
            c.execute('DELETE FROM temporary_buffs WHERE player_id = ?', (player_id,))
            c.execute('DELETE FROM seed_storage WHERE player_id = ?', (player_id,))
            c.execute('DELETE FROM zodiac_equipped WHERE player_id = ?', (player_id,))
            c.execute('DELETE FROM farms WHERE player_id = ?', (player_id,))
            c.execute('DELETE FROM tasks WHERE player_id = ?', (player_id,))
            c.execute('DELETE FROM hero_equipment WHERE player_id = ?', (player_id,))
            c.execute('DELETE FROM player_equipment WHERE player_id = ?', (player_id,))
            c.execute('DELETE FROM warehouse WHERE player_id = ?', (player_id,))
            c.execute('DELETE FROM inventory WHERE player_id = ?', (player_id,))
            c.execute('DELETE FROM heroes WHERE player_id = ?', (player_id,))
            c.execute('DELETE FROM players WHERE id = ?', (player_id,))
            c.execute('DELETE FROM player_combat WHERE player_id = ?', (player_id,))
        if c.rowcount == 0:
            conn_game.rollback()
            return jsonify({'error': '无权删除此角色'}), 403

        c.execute('DELETE FROM player_logs WHERE player_name = ?', (name,))
        c.execute('DELETE FROM world_boss_kills WHERE killer_name = ?', (name,))
        c.execute('DELETE FROM auction_bids WHERE bidder_character_name = ?', (name,))
        c.execute('DELETE FROM auction_bids WHERE auction_id IN (SELECT id FROM auction_items WHERE seller_character_name = ?)', (name,))
        c.execute('DELETE FROM auction_items WHERE seller_character_name = ?', (name,))
        conn_game.commit()
    except Exception as e:
        if conn_game:
            conn_game.rollback()
        logger.exception(f"删除角色游戏数据库失败: {e}")
        return jsonify({'error': f'删除失败: {str(e)}'}), 500
    finally:
        if conn_game:
            chat.close_game_connection(conn_game)

    conn_chat = None
    try:
        conn_chat = chat.get_chat_db()
        c = conn_chat.cursor()
        c.execute('BEGIN IMMEDIATE')
        c.execute('DELETE FROM chat_messages WHERE sender = ? OR target = ?', (name, name))
        c.execute('DELETE FROM board_messages WHERE sender = ?', (name,))
        conn_chat.commit()
    except Exception as e:
        if conn_chat:
            conn_chat.rollback()
        logger.exception(f"删除角色社交数据库失败: {e}")
        return jsonify({'error': f'删除失败: {str(e)}'}), 500
    finally:
        if conn_chat:
            chat.close_chat_connection(conn_chat)

    if os.path.exists(filename):
        try:
            os.remove(filename)
        except Exception as e:
            logger.warning(f"删除旧存档文件失败 {filename}: {e}")

    try:
        player_state_manager.remove(name)
    except Exception as e:
        logger.warning(f"清理状态管理器失败: {e}")

    try:
        world_boss_manager.remove_player_data(name)
    except Exception as e:
        logger.warning(f"清理世界Boss管理器失败: {e}")

    return jsonify({'status': 'ok', 'message': '角色已删除'})

@app.route('/api/load', methods=['GET'])
def load_game():
    user_id, username, _ = get_current_user_from_jwt()
    if not user_id:
        return jsonify({'error': '请先登录'}), 401

    name = request.args.get('name')
    if not name:
        return jsonify({'error': '缺少名字参数'}), 400

    data, migrated = load_player_data(name, user_id)
    if data is None:
        return jsonify({'error': '存档不存在或无权访问'}), 404

    from online_state import add_player_via_http
    add_player_via_http(
        name=name,
        level=data['player']['level'],
        rebirth=data['player']['rebirth'],
        map_id=data.get('currentMap', 1)
    )
    try:
        broadcast_online()
    except Exception as e:
        logger.error(f"广播在线列表失败: {e}")

    return jsonify(data)

@app.route('/api/check', methods=['GET'])
def check_name():
    name = request.args.get('name')
    if not name:
        return jsonify({'error': '缺少名字参数'}), 400

    conn = chat.get_game_db()
    try:
        c = conn.cursor()
        c.execute('SELECT user_id FROM characters WHERE name = ?', (name,))
        row = c.fetchone()
    finally:
        chat.close_game_connection(conn)

    user_id, username, _ = get_current_user_from_jwt()

    if row:
        if user_id and row[0] == user_id:
            return jsonify({'exists': True, 'owner': True})
        else:
            return jsonify({'exists': True, 'owner': False})

    filename = get_filename(name)
    if os.path.exists(filename):
        try:
            with open(filename, 'r', encoding='utf-8') as f:
                data = json.load(f)
            file_user_id = data.get('userId')
            if user_id and file_user_id == user_id:
                return jsonify({'exists': True, 'owner': True})
            else:
                return jsonify({'exists': True, 'owner': False})
        except:
            pass

    return jsonify({'exists': False})

@app.route('/api/sort_inventory', methods=['POST'])
def sort_inventory():
    user_id, username, _ = get_current_user_from_jwt()
    if not user_id:
        return jsonify({'error': '未登录'}), 401

    data = request.json
    character_name = data.get('character_name')
    if not character_name:
        return jsonify({'error': '缺少角色名'}), 400

    conn = chat.get_game_db()
    try:
        c = conn.cursor()
        # 获取 player_id 并验证归属
        c.execute('SELECT id FROM players WHERE name = ? AND user_id = ?', (character_name, user_id))
        row = c.fetchone()
        if not row:
            return jsonify({'error': '角色不存在或无权限'}), 404
        player_id = row[0]

        # 从 inventory 表读取所有物品
        c.execute('SELECT * FROM inventory WHERE player_id = ?', (player_id,))
        items = []
        for item in c.fetchall():
            inv_item = {
                "uuid": item[2], "id": item[3], "name": item[4], "type": item[5],
                "quantity": item[6], "enhanceLevel": item[7], "quality": item[8],
                "acquiredTime": item[10], "droppedBy": item[11]
            }
            extra = json.loads(item[9]) if item[9] else {}
            inv_item.update(extra)
            items.append(inv_item)

        # 排序逻辑
        quality_order = {'超神品': 4, '神品': 3, '极品': 2, '普通': 1}
        type_order = {
            'weapon':1, 'armor':2, 'helmet':3, 'necklace':4, 'medal':5,
            'bracelet':6, 'ring':7, 'special_ring':8, 'belt':9, 'shoes':10,
            'zodiac':11, 'bambooHat':12, 'cloak':13, 'horseToken':14,
            'potion':99, 'fragment':100, 'seed':101, 'fruit':102, 'consumable':103
        }
        def sort_key(item):
            return (
                type_order.get(item.get('type'), 999),
                -quality_order.get(item.get('quality', '普通'), 0),
                -item.get('level', 0),
                item.get('name', '')
            )
        sorted_items = sorted(items, key=sort_key)

        c.execute('DELETE FROM inventory WHERE player_id = ?', (player_id,))
        for item in sorted_items:
            c.execute('''
                INSERT INTO inventory
                (player_id, uuid, item_id, name, type, quantity, enhance_level, quality, data,
                 acquired_time, dropped_by)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            ''', (player_id, item.get('uuid'), item.get('id'), item.get('name'), item.get('type'),
                  item.get('quantity', 1), item.get('enhanceLevel', 0), item.get('quality'),
                  json.dumps({k: v for k, v in item.items() if k not in ('uuid', 'id', 'name', 'type', 'quantity', 'enhanceLevel', 'quality', 'acquiredTime', 'droppedBy')}),
                  item.get('acquiredTime'), item.get('droppedBy')))
        conn.commit()

        # 同步内存缓存（可选）
        cached = player_state_manager.get(character_name)
        if cached:
            cached['inventory'] = sorted_items
            player_state_manager.set(character_name, cached)

        return jsonify({'inventory': sorted_items})
    except Exception as e:
        logger.exception(f"整理背包失败: {e}")
        return jsonify({'error': str(e)}), 500
    finally:
        chat.close_game_connection(conn)
    
@app.route('/api/world_boss')
def get_world_boss():
    if world_boss_manager.active_boss:
        boss = world_boss_manager.active_boss
        map_id = boss['map_id']
        map_data = next((m for m in CONFIG.get('maps', []) if m['id'] == map_id), None)
        map_name = map_data['name'] if map_data else f'地图{map_id}'
        return jsonify({
            'exists': True,
            'map_id': map_id,
            'map_name': map_name,
            'boss_name': boss['boss']['name'],
            'boss_hp': boss['boss']['currentHp'],
            'boss_max_hp': boss['boss']['maxHp']
        })
    else:
        return jsonify({'exists': False})

@app.route('/api/world_boss/last_kill/<int:map_id>', methods=['GET'])
def get_last_boss_kill(map_id):
    conn = chat.get_game_db()
    try:
        c = conn.cursor()
        c.execute('''
                  SELECT killer_name, kill_time, boss_name
                  FROM world_boss_kills
                  WHERE map_id = ?
                  ORDER BY kill_time DESC
                      LIMIT 1
                  ''', (map_id,))
        row = c.fetchone()
        if row:
            return jsonify({
                'exists': True,
                'killer': row[0],
                'time': row[1],
                'boss_name': row[2]
            })
        else:
            return jsonify({'exists': False})
    finally:
        chat.close_game_connection(conn)

@app.route('/api/list', methods=['GET'])
def list_saves():
    user_id, username, _ = get_current_user_from_jwt()
    if not user_id:
        return jsonify({'error': '未登录'}), 401

    conn = chat.get_game_db()
    try:
        c = conn.cursor()
        c.execute('''
            SELECT name, level, rebirth, created_at FROM players WHERE user_id = ? ORDER BY created_at DESC
        ''', (user_id,))
        rows = c.fetchall()
    finally:
        chat.close_game_connection(conn)

    saves = [{'name': row[0], 'level': row[1], 'rebirth': row[2], 'saveTime': row[3]} for row in rows]
    return jsonify(saves)

def _calculate_actual_stat(state, target, attr_prefix, slots):
    if target == 'player':
        entity = state['player']
        equipment = state['equipment']['player']
    elif target == 'hero':
        if not state['hero'].get('summoned'):
            return 0, 0
        entity = state['hero']
        equipment = state['equipment']['hero']
    else:
        return 0, 0

    base_min = entity.get(f'{attr_prefix}Min', 0) + entity.get(f'{attr_prefix}Enhance', 0)
    base_max = entity.get(f'{attr_prefix}Max', 0) + entity.get(f'{attr_prefix}Enhance', 0)

    for slot in slots:
        item = equipment.get(slot)
        if item:
            base_min += item.get(f'{attr_prefix}Min', 0)
            base_max += item.get(f'{attr_prefix}Max', 0)
    return base_min, base_max

def calculate_actual_attack(state, target='player'):
    attack_slots = ['weapon', 'necklace', 'leftBracelet', 'rightBracelet', 'leftRing', 'rightRing', 'medal']
    return _calculate_actual_stat(state, target, 'attack', attack_slots)

def calculate_actual_defense(state, target='player'):
    defense_slots = ['armor', 'helmet', 'leftBracelet', 'rightBracelet', 'belt', 'shoes']
    return _calculate_actual_stat(state, target, 'defense', defense_slots)

def recalc_cached_stats(state):
    p_att = calculate_actual_attack(state, 'player')
    p_def = calculate_actual_defense(state, 'player')
    state['player']['cachedAttackMin'] = p_att[0]
    state['player']['cachedAttackMax'] = p_att[1]
    state['player']['cachedDefenseMin'] = p_def[0]
    state['player']['cachedDefenseMax'] = p_def[1]

    if state['hero'].get('summoned'):
        h_att = calculate_actual_attack(state, 'hero')
        h_def = calculate_actual_defense(state, 'hero')
        state['hero']['cachedAttackMin'] = h_att[0]
        state['hero']['cachedAttackMax'] = h_att[1]
        state['hero']['cachedDefenseMin'] = h_def[0]
        state['hero']['cachedDefenseMax'] = h_def[1]

@app.route('/api/state/<name>', methods=['GET'])
def get_player_state(name):
    user_id, username, _ = get_current_user_from_jwt()
    if not user_id:
        return jsonify({'error': '未登录'}), 401

    state = player_state_manager.get(name)
    if state:
        return jsonify(state)

    state, _ = load_player_data(name)
    if state:
        player_state_manager.set(name, state)
        return jsonify(state)

    return jsonify({'error': '状态不存在'}), 404

@app.route('/api/player-info', methods=['GET'])
def player_info():
    name = request.args.get('name')
    if not name:
        return jsonify({'error': '缺少名字参数'}), 400

    state = player_state_manager.get(name)
    if state is None:
        data, _ = load_player_data(name)
        if data is None:
            return jsonify({'error': '玩家不存在'}), 404
        state = data

    player = state.get('player', {})
    hero = state.get('hero', {})
    equipment = state.get('equipment', {})

    player_attack_min = player.get('finalAttackMin')
    player_attack_max = player.get('finalAttackMax')
    if player_attack_min is None or player_attack_max is None:
        player_attack_min, player_attack_max = calculate_actual_attack(state, 'player')
    player_defense_min = player.get('finalDefenseMin')
    player_defense_max = player.get('finalDefenseMax')
    if player_defense_min is None or player_defense_max is None:
        player_defense_min, player_defense_max = calculate_actual_defense(state, 'player')

    info = {
        'name': player.get('name'),
        'level': player.get('level'),
        'rebirth': player.get('rebirth', 0),
        'hp': player.get('hp'),
        'maxHp': player.get('maxHp'),
        'attackMin': player_attack_min,
        'attackMax': player_attack_max,
        'defenseMin': player_defense_min,
        'defenseMax': player_defense_max,
        'attackEnhance': player.get('attackEnhance', 0),
        'defenseEnhance': player.get('defenseEnhance', 0),
        'expMultiplier': player.get('expMultiplier', 1),
        'gameSpeed': player.get('gameSpeed', 1),
        'avatar': player.get('avatar', ''),
        'hero': None,
        'equipment': {
            'player': equipment.get('player', {}),
            'hero': equipment.get('hero', {}) if hero.get('summoned') else None
        }
    }

    if hero.get('summoned'):
        hero_attack_min = hero.get('finalAttackMin')
        hero_attack_max = hero.get('finalAttackMax')
        if hero_attack_min is None or hero_attack_max is None:
            hero_attack_min, hero_attack_max = calculate_actual_attack(state, 'hero')
        hero_defense_min = hero.get('finalDefenseMin')
        hero_defense_max = hero.get('finalDefenseMax')
        if hero_defense_min is None or hero_defense_max is None:
            hero_defense_min, hero_defense_max = calculate_actual_defense(state, 'hero')
        info['hero'] = {
            'level': hero.get('level'),
            'rebirth': hero.get('rebirth', 0),
            'hp': hero.get('hp'),
            'maxHp': hero.get('maxHp'),
            'attackMin': hero_attack_min,
            'attackMax': hero_attack_max,
            'defenseMin': hero_defense_min,
            'defenseMax': hero_defense_max,
            'attackEnhance': hero.get('attackEnhance', 0),
            'defenseEnhance': hero.get('defenseEnhance', 0)
        }
        info['equipment']['hero'] = {k: v for k, v in equipment.get('hero', {}).items() if v}

    return jsonify(info)

@app.route('/api/farm/<name>')
def get_player_farm(name):
    state = player_state_manager.get(name)
    if state is None:
        data, _ = load_player_data(name)
        if data is None:
            return jsonify({'error': '玩家不存在'}), 404
        state = data
    if 'farm' not in state:
        state['farm'] = {'plots': [None] * farm_system.max_plots, 'unlocked_plots': farm_system.max_plots}
    plots = farm_system.get_farm_state(state)
    return jsonify({'plots': plots, 'player_name': name})

@app.route('/api/world_boss/<int:map_id>', methods=['GET'])
def get_world_boss_info(map_id):
    info = world_boss_manager.get_boss_info(map_id)
    return jsonify(info)

@app.route('/api/world_boss/rank/<int:map_id>', methods=['GET'])
def get_world_boss_rank(map_id):
    rank = world_boss_manager.get_boss_damage_rank(map_id)
    return jsonify({'rank': rank})

@app.route('/api/world_boss/active', methods=['GET'])
def get_active_world_boss():
    with world_boss_manager.lock:
        if world_boss_manager.active_boss:
            boss = world_boss_manager.active_boss
            return jsonify({
                'exists': True,
                'map_id': boss['map_id'],
                'map_name': boss['map_name'],
                'boss_name': boss['boss']['name'],
                'boss_hp': boss['boss']['currentHp'],
                'boss_max_hp': boss['boss']['maxHp'],
                'boss_hp_percent': (boss['boss']['currentHp'] / boss['boss']['maxHp']) * 100,
                'participants': len(boss['participants']),
                'spawn_time': boss['spawn_time']
            })
    return jsonify({'exists': False})

@app.route('/api/activities', methods=['GET'])
def get_activities():
    all_acts = {}
    activities = activity_manager.get_all_activities()
    for act_id, act in activities.items():
        all_acts[act_id] = {
            'name': act.get('name'),
            'active': activity_manager.is_active(act_id),
            'description': act.get('description', '')
        }
    return jsonify(all_acts)

@socketio.on('start_auto_combat')
def handle_start_auto_combat(data):
    player_name = data.get('player_name')
    if not player_name:
        return
    # 停止旧任务
    _stop_auto_combat(player_name)

    # 获取当前状态并确保怪物组存在
    state = player_state_manager.get(player_name)
    if state is None:
        state, _ = load_player_data(player_name)
        if state is None:
            emit('auto_combat_started', {'error': '无法获取角色状态'}, room=player_name)
            return
        player_state_manager.set(player_name, state)

    monster_group = state.get('monsterGroup')
    if monster_group is None or not monster_group.get('monsters'):
        map_id = state['currentMap']
        new_monsters = game_engine._select_monster_group_for_map(map_id)
        if new_monsters:
            state['monsterGroup'] = {'monsters': new_monsters, 'uuid': generate_uuid()}
            player_state_manager.set(player_name, state)
        else:
            emit('auto_combat_started', {'error': '当前地图无法生成怪物'}, room=player_name)
            return

    # 启动自动战斗协程
    with auto_combat_lock:
        task = eventlet.spawn(auto_combat_worker, player_name)
        auto_combat_tasks[player_name] = task
    emit('auto_combat_started', {'status': 'ok'})

@socketio.on('stop_auto_combat')
def handle_stop_auto_combat(data):
    player_name = data.get('player_name')
    if player_name:
        _stop_auto_combat(player_name)   # 内部已发送 auto_combat_stopped
        state = player_state_manager.get(player_name)
        if state:
            monsters = (state.get('monsterGroup') or {}).get('monsters', [])
            is_world_boss = any(m.get('isWorldBoss') for m in monsters)
            if not is_world_boss:
                state['monsterGroup'] = None
                socketio.emit('combat_update', {'diff': {'monsterGroup': None}}, room=player_name)
            else:
                # 世界 Boss 战斗仅发送停止消息，保留怪物组
                socketio.emit('combat_update', {'diff': {}, 'message': '已停止自动攻击，可手动挑战'}, room=player_name)
                # 可选：刷新一下 Boss 状态
                boss_info = world_boss_manager.get_boss_info(state['currentMap'])
                if boss_info.get('exists'):
                    socketio.emit('world_boss_status', boss_info, room=player_name)

@socketio.on('draw_boss_drop')
def handle_draw_boss_drop(data):
    player_name = data.get('player_name') or session.get('player_name')
    if not player_name:
        return {'error': '未登录或缺少玩家名'}
    success, msg = world_boss_manager.draw_boss_drop(player_name)
    emit('boss_draw_result', {'success': success, 'message': msg}, room=player_name)

@socketio.on('admin_reload')
def handle_admin_reload(data):
    player_name = data.get('player_name')
    if player_name:
        state = player_state_manager.get(player_name)
        if state:
            emit('game_state', state, room=player_name)
    
def auction_worker():
    while True:
        time.sleep(60)
        try:
            check_expired_auctions()
        except Exception as e:
            logger.error(f"拍卖结标出错: {e}")

threading.Thread(target=auction_worker, daemon=True).start()
from log_utils import start_log_worker
start_log_worker()

AVATAR_DIR = os.path.join(DATA_DIR, 'avatars')
if not os.path.exists(AVATAR_DIR):
    os.makedirs(AVATAR_DIR)

def get_avatar_list():
    files = []
    if os.path.exists(AVATAR_DIR):
        for f in os.listdir(AVATAR_DIR):
            if f.lower().endswith(('.jpg', '.jpeg', '.png')):
                files.append(f)
    return files

@app.route('/avatars/<path:filename>')
def serve_avatar(filename):
    return send_from_directory(AVATAR_DIR, filename, conditional=True, max_age=604800)

@app.route('/api/avatars', methods=['GET'])
def list_avatars():
    return jsonify(get_avatar_list())

@app.route('/api/avatar/set', methods=['POST'])
def set_avatar():
    user_id, username, _ = get_current_user_from_jwt()
    if not user_id:
        return jsonify({'error': '未登录'}), 401

    data = request.json
    character_name = data.get('character_name')
    target = data.get('target')
    filename = data.get('filename')

    if not character_name or not target or filename is None:
        return jsonify({'error': '缺少参数'}), 400
    if target not in ('player', 'hero'):
        return jsonify({'error': 'target 参数错误'}), 400

    conn = chat.get_game_db()
    try:
        c = conn.cursor()
        # 获取 player_id
        c.execute('SELECT id FROM players WHERE name = ? AND user_id = ?', (character_name, user_id))
        row = c.fetchone()
        if not row:
            return jsonify({'error': '角色不存在或无权限'}), 404
        player_id = row[0]
        avatar_path = f"/avatars/{filename}" if filename else ''

        if target == 'player':
            c.execute('UPDATE players SET avatar = ? WHERE id = ?', (avatar_path, player_id))
        else:
            c.execute('UPDATE heroes SET avatar = ? WHERE player_id = ?', (avatar_path, player_id))
        conn.commit()

        # 同步内存缓存
        cached = player_state_manager.get(character_name)
        if cached:
            if target == 'player':
                cached['player']['avatar'] = avatar_path
            else:
                cached['hero']['avatar'] = avatar_path
            player_state_manager.set(character_name, cached)

        return jsonify({'avatarUrl': avatar_path, 'message': '头像已更新'})
    except Exception as e:
        logger.exception(f"设置头像失败: {e}")
        return jsonify({'error': str(e)}), 500
    finally:
        chat.close_game_connection(conn)

@app.route('/api/batch_sell', methods=['POST'])
def batch_sell():
    user_id, username, _ = get_current_user_from_jwt()
    if not user_id:
        return jsonify({'error': '未登录'}), 401

    data = request.json
    uuids = data.get('uuids', [])
    character_name = data.get('character_name')
    if not uuids or not character_name:
        return jsonify({'error': '缺少参数'}), 400

    # 从分表加载状态
    state, _ = load_player_data(character_name)
    if not state or state.get('userId') != user_id:
        return jsonify({'error': '角色不存在或无权限'}), 404

    total_gold = 0
    sold_items = []
    failed_uuids = []

    for uuid in uuids:
        new_state, msg = game_engine.sell_item(state, uuid, is_uuid=True)
        if '成功' in msg or '出售' in msg:
            # 查找被出售的物品名称
            for item in state['inventory']:
                if item.get('uuid') == uuid:
                    gold_gained = math.floor(item.get('price', 0) / 2) * item.get('quantity', 1)
                    total_gold += gold_gained
                    sold_items.append(item.get('name', '未知物品'))
                    break
            state = new_state
        else:
            failed_uuids.append(uuid)

    player_state_manager.set(character_name, state)  # 标记脏
    conn = chat.get_game_db()
    try:
        c = conn.cursor()
        c.execute('SELECT id FROM characters WHERE name = ?', (character_name,))
        row = c.fetchone()
        if row:
            char_id = row[0]
            save_state_to_tables(char_id, user_id, character_name, state)
    finally:
        chat.close_game_connection(conn)

    return jsonify({
        'success': True,
        'total_gold': total_gold,
        'sold_count': len(sold_items),
        'sold_items': sold_items,
        'failed_uuids': failed_uuids
    })

@app.route('/api/auto_sell_config', methods=['GET'])
def get_auto_sell_config():
    user_id, username, _ = get_current_user_from_jwt()
    if not user_id:
        return jsonify({'error': '未登录'}), 401

    character_name = request.args.get('character_name')
    if not character_name:
        return jsonify({'error': '缺少角色名'}), 400

    # 1. 优先从内存状态管理器获取（在线玩家一定有完整状态）
    state = player_state_manager.get(character_name)
    if state and 'autoSellConfig' in state:
        return jsonify(state['autoSellConfig'])

    # 2. 检查短期缓存
    cache_key = f"{user_id}:{character_name}"
    now = time.time()
    with _AUTO_SELL_CACHE_LOCK:
        if cache_key in _auto_sell_cache:
            data, expire = _auto_sell_cache[cache_key]
            if now < expire:
                return jsonify(data)
            else:
                del _auto_sell_cache[cache_key]

    # 3. 缓存未命中，从数据库读取
    conn = chat.get_game_db()
    try:
        c = conn.cursor()
        c.execute('SELECT config_json FROM players WHERE name = ? AND user_id = ?', (character_name, user_id))
        row = c.fetchone()
        if not row:
            return jsonify({'error': '角色不存在'}), 404
        config = json.loads(row[0]) if row[0] else {}
        auto_config = config.get('autoSellConfig', {'selectedSets': [], 'qualityMap': {}})

        # 写入短期缓存
        with _AUTO_SELL_CACHE_LOCK:
            _auto_sell_cache[cache_key] = (auto_config, now + _AUTO_SELL_CACHE_TTL)

        mem_state = player_state_manager.get(character_name)
        if mem_state:
            mem_state['autoSellConfig'] = auto_config
            player_state_manager.set(character_name, mem_state)   # 标记脏并保存

        return jsonify(auto_config)
    finally:
        chat.close_game_connection(conn)


@app.route('/api/auto_sell_config', methods=['POST'])
def save_auto_sell_config():
    user_id, username, _ = get_current_user_from_jwt()
    if not user_id:
        return jsonify({'error': '未登录'}), 401

    data = request.json
    character_name = data.get('character_name')
    config = data.get('config', {})
    if not character_name:
        return jsonify({'error': '缺少角色名'}), 400

    conn = chat.get_game_db()
    try:
        c = conn.cursor()
        c.execute('SELECT config_json FROM players WHERE name = ? AND user_id = ?', (character_name, user_id))
        row = c.fetchone()
        if not row:
            return jsonify({'error': '角色不存在'}), 404
        existing = json.loads(row[0]) if row[0] else {}
        existing['autoSellConfig'] = config
        c.execute('UPDATE players SET config_json = ? WHERE name = ? AND user_id = ?',
                  (json.dumps(existing), character_name, user_id))
        conn.commit()

        # 更新内存中的状态
        state = player_state_manager.get(character_name)
        if state:
            state['autoSellConfig'] = config
            player_state_manager.set(character_name, state)   # 标记脏

        # 更新短期缓存
        cache_key = f"{user_id}:{character_name}"
        with _AUTO_SELL_CACHE_LOCK:
            _auto_sell_cache[cache_key] = (config, time.time() + _AUTO_SELL_CACHE_TTL)

        return jsonify({'success': True})
    finally:
        chat.close_game_connection(conn)

def find_map_without_world_boss(current_state, world_boss_mgr):
    player_level = current_state['player']['level']
    maps = CONFIG.get('maps', [])
    eligible_maps = []
    for m in maps:
        if m.get('minLevel', 1) > player_level:
            continue
        if not m.get('monsters'):
            continue
        try:
            boss_info = world_boss_mgr.get_boss_info(m['id'])
            if boss_info.get('exists'):
                continue
        except Exception as e:
            logger.error(f"获取地图 {m['id']} 世界 Boss 状态失败: {e}")
            pass
        eligible_maps.append(m)
    if eligible_maps:
        return eligible_maps[0]['id']
    for m in maps:
        if m.get('monsters'):
            return m['id']
    return 1

def auto_combat_worker(player_name):
    import traceback
    current_greenlet = eventlet.getcurrent()
    logger.info(f"自动战斗协程启动: {player_name}")

    AUTO_POTION_COOLDOWN = 1.5
    last_auto_potion_time = 0

    while True:
        # 检查任务是否已被手动移除
        with auto_combat_lock:
            if auto_combat_tasks.get(player_name) != current_greenlet:
                logger.info(f"自动战斗: 玩家 {player_name} 的任务已被移除，协程退出")
                break

        try:
            state = player_state_manager.get(player_name)
            if state is None:
                state, _ = load_player_data(player_name)
                if state is None:
                    logger.error(f"自动战斗: 无法加载玩家 {player_name} 状态，等待 10 秒后重试")
                    eventlet.sleep(10)
                    continue
                player_state_manager.set(player_name, state)

            # 玩家死亡则停止
            if state['player']['hp'] <= 0:
                logger.info(f"自动战斗: 玩家 {player_name} 已死亡，停止")
                send_private(player_name, "你已死亡，自动战斗停止。")
                socketio.emit('auto_combat_stopped', {}, room=player_name)
                break

            # 若当前是世界 Boss 但怪物组异常，尝试修复
            boss_info = world_boss_manager.get_boss_info(state['currentMap'])
            if boss_info.get('exists') and boss_info.get('boss_hp', 0) > 0:
                mg = state.get('monsterGroup')
                is_wb = mg and any(m.get('isWorldBoss') for m in mg.get('monsters', []))
                if not is_wb:
                    boss = world_boss_manager.get_boss_raw(state['currentMap'])
                    if boss and boss.get('currentHp', 0) > 0:
                        boss_copy = deepcopy(boss)
                        boss_copy['isWorldBoss'] = True
                        state['monsterGroup'] = {'monsters': [boss_copy], 'uuid': generate_uuid()}
                        player_state_manager.set(player_name, state)
                        logger.info(f"自动战斗: 为玩家 {player_name} 重建世界 Boss 怪物组")

            # 无怪物组则生成（普通怪物或世界 Boss 已处理）
            if state.get('monsterGroup') is None or not state['monsterGroup'].get('monsters'):
                map_id = state['currentMap']
                new_monsters = game_engine._select_monster_group_for_map(map_id)
                if new_monsters:
                    state['monsterGroup'] = {'monsters': new_monsters, 'uuid': generate_uuid()}
                    player_state_manager.set(player_name, state)
                else:
                    eventlet.sleep(5)
                    continue

            # 计算攻击等待时间
            now = time.time()
            next_times = []
            player_interval = game_engine._calc_attack_interval(
                state['player'], game_engine.player_base_attack_interval
            )
            last_player = state.get('last_player_attack_time', 0)
            if last_player:
                next_player = last_player + player_interval
                if next_player > now:
                    next_times.append(next_player)

            hero = state['hero']
            hero_alive = hero.get('summoned', False) and hero.get('hp', 0) > 0
            if hero_alive:
                hero_interval = game_engine._calc_attack_interval(
                    hero, game_engine.hero_base_attack_interval
                )
                last_hero = state.get('last_hero_attack_time', 0)
                if last_hero:
                    next_hero = last_hero + hero_interval
                    if next_hero > now:
                        next_times.append(next_hero)

            monster_group = state.get('monsterGroup')
            if monster_group and monster_group.get('monsters'):
                for m in monster_group['monsters']:
                    if m.get('currentHp', 0) <= 0:
                        continue
                    last_attack = m.get('last_attack_time', 0)
                    monster_interval_actual = m.get('attack_interval', game_engine.monster_base_attack_interval)
                    next_monster = last_attack + monster_interval_actual
                    if next_monster > now:
                        next_times.append(next_monster)

            if next_times:
                wait = min(next_times) - now
                wait = max(0.05, min(wait, 2.0))
            else:
                wait = 0.5

            # 执行一次战斗
            old_state = copy.deepcopy(state)
            new_state = game_engine.combat(state)
            logs = new_state.pop('log_messages', [])
            player_state_manager.set(player_name, new_state)
            diff = compute_state_diff(old_state, new_state)

            # ---------- 自动用药----------
            if now - last_auto_potion_time >= AUTO_POTION_COOLDOWN:
                # 玩家自动用药
                game_engine._auto_use_potion(new_state, 'player')
                # 英雄自动用药
                if hero_alive:
                    game_engine._auto_use_potion(new_state, 'hero')
                last_auto_potion_time = now
                # 用药后可能血量变化，重新保存状态并更新 diff（可选）
                player_state_manager.set(player_name, new_state)
                # 重新计算差异（用药可能导致 hp/mp 变化，需同步前端）
                diff = compute_state_diff(old_state, new_state)

            # 判断是否后台，决定推送
            with cache_lock:
                info = online_cache.get(player_name)
                is_background = info and info.get('in_background', False) if info else False

            if not is_background:
                socketio.emit('combat_update', {
                    'diff': diff,
                    'logs': logs
                }, room=player_name)

            if new_state['player']['hp'] <= 0:
                logger.info(f"自动战斗: 玩家 {player_name} 战斗中死亡，停止")
                send_private(player_name, "你已死亡，自动战斗停止。")
                socketio.emit('auto_combat_stopped', {}, room=player_name)
                break

            eventlet.sleep(wait)

        except GreenletExit:
            logger.info(f"自动战斗: 玩家 {player_name} 收到 GreenletExit，协程退出")
            break
        except Exception as e:
            logger.error(f"自动战斗协程异常 {player_name}: {e}\n{traceback.format_exc()}")
            eventlet.sleep(5)   # 发生异常时等待后继续，不退出
            continue

    # 清理任务
    with auto_combat_lock:
        if auto_combat_tasks.get(player_name) == current_greenlet:
            auto_combat_tasks.pop(player_name, None)
    logger.info(f"自动战斗协程退出: {player_name}")

def get_filename(name):
    hash_val = hashlib.md5(name.encode('utf-8')).hexdigest()
    return os.path.join(DATA_DIR, f"{hash_val}.json")

def natural_recovery_worker():
    """优化版：批量获取状态，减少锁持有时间，仅在有变化时写回"""
    while True:
        eventlet.sleep(3)
        # 1. 快速获取在线玩家列表（短锁）
        with cache_lock:
            online_players = list(online_cache.keys())
        if not online_players:
            continue

        # 2. 批量获取状态
        states = player_state_manager.get_batch(online_players)
        updates = []   # 存储 (player_name, state, diff) 三元组
        now = time.time()

        for name, state in states.items():
            if state is None:
                # 缓存未命中，尝试从数据库加载（极少发生）
                state, _ = load_player_data(name)
                if state is None:
                    continue
                player_state_manager.set(name, state)
            # 跳过自动战斗中的玩家
            with auto_combat_lock:
                if name in auto_combat_tasks:
                    continue

            player = state['player']
            old_hp = player.get('hp')
            old_mp = player.get('mp')
            max_hp = player.get('maxHp', 100)
            max_mp = player.get('maxMp', 100)

            new_hp = min(max_hp, old_hp + max(1, max_hp // 100))
            new_mp = min(max_mp, old_mp + max(1, max_mp // 100))

            hero_changed = False
            hero = state['hero']
            if hero.get('summoned') and hero.get('hp', 0) > 0:
                hero_max_hp = hero.get('maxHp', 80)
                hero_max_mp = hero.get('maxMp', 100)
                hero_old_hp = hero['hp']
                hero_old_mp = hero.get('mp', 0)
                hero_new_hp = min(hero_max_hp, hero_old_hp + max(1, hero_max_hp // 100))
                hero_new_mp = min(hero_max_mp, hero_old_mp + max(1, hero_max_mp // 100))
                if hero_new_hp != hero_old_hp or hero_new_mp != hero_old_mp:
                    hero['hp'] = hero_new_hp
                    hero['mp'] = hero_new_mp
                    hero_changed = True

            if new_hp != old_hp or new_mp != old_mp or hero_changed:
                if new_hp != old_hp:
                    player['hp'] = new_hp
                if new_mp != old_mp:
                    player['mp'] = new_mp
                # 构建 diff
                diff = {}
                if new_hp != old_hp or new_mp != old_mp:
                    diff['player'] = {}
                    if new_hp != old_hp:
                        diff['player']['hp'] = new_hp
                    if new_mp != old_mp:
                        diff['player']['mp'] = new_mp
                if hero_changed:
                    diff['hero'] = {}
                    if 'hp' in hero:
                        diff['hero']['hp'] = hero['hp']
                    if 'mp' in hero:
                        diff['hero']['mp'] = hero['mp']
                updates.append((name, state, diff))

        # 3. 批量写回并推送（每个玩家单独 set，但避免了重复的 get 锁）
        for name, state, diff in updates:
            player_state_manager.set(name, state)
            if diff:
                socketio.emit('combat_update', {'diff': diff}, room=name)

if __name__ == '__main__':
    eventlet.spawn(natural_recovery_worker)
    socketio.run(app, debug=False, host='0.0.0.0', port=5000)