"""
系统更新 API

提供系统版本查询、离线升级包更新、备份管理、以及知识库/案由/裁量数据的手动更新。

V1.6.7 改进：
- 升级前自动断开数据库连接池，释放 system.db 文件锁（解决 v1.6.6 升级失败问题）
- 升级成功后自动重启应用，使新加密机制立即生效
- 回滚失败时提供更友好的错误提示
"""
import os
import json
import hashlib
import logging
import shutil
import tempfile
import zipfile
import subprocess
import asyncio
import time
import traceback
from datetime import datetime
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Query, Request
from fastapi.responses import StreamingResponse
from typing import Optional

from ...core.config import settings
from ...core.database import get_db, get_system_db, _has_split_databases, close_pool
from ...core.permission import get_current_user, is_admin_role
from ...core.crypto import read_text_file, is_encrypted
from ...core.email import send_feedback_email

router = APIRouter()
logger = logging.getLogger(__name__)


async def _system_db():
    """Dependency: system.db when split databases exist, otherwise fallback to get_db()"""
    _db_gen = get_system_db() if _has_split_databases() else get_db()
    async for conn, cur in _db_gen:
        yield conn, cur

# 路径解析：使用 paths 模块兼容 EXE 模式和开发模式
from ...core.paths import get_exe_dir, get_data_dir
_EXE_DIR = get_exe_dir()
_DATA_DIR = get_data_dir()
ZHISHIKU_BASE = os.path.join(_DATA_DIR, "data", "zhishiku")
DISCRETION_BASE = os.path.join(_EXE_DIR, "单机版", "处罚裁量")
VERSION_FILE = os.path.join(_EXE_DIR, "VERSION")
HISTORY_FILE = os.path.join(_DATA_DIR, "data", "update_history.json")
TEMP_DIR = os.path.join(_DATA_DIR, "data", "temp_update")
ROLLBACK_FILE = os.path.join(_DATA_DIR, "bff-api", "data", "rollback_state.json")
BACKUPS_DIR = os.path.join(_DATA_DIR, "bff-api", "data", "backups")


def _require_admin(request: Request):
    user = get_current_user(request)
    if not user or not is_admin_role(user.get('role', '')):
        raise HTTPException(status_code=403, detail="需要管理员权限")
    return user


def _count_files(directory: str, ext: str = ".md") -> int:
    """递归统计指定扩展名的文件数（同时支持加密文件 .ext.enc）"""
    count = 0
    if os.path.isdir(directory):
        for root, dirs, files in os.walk(directory):
            for f in files:
                if f.endswith(ext) or f.endswith(ext + '.enc'):
                    count += 1
    return count


def _sha256_file(filepath: str) -> str:
    """计算文件 SHA256"""
    h = hashlib.sha256()
    with open(filepath, "rb") as f:
        for chunk in iter(lambda: f.read(8192), b""):
            h.update(chunk)
    return f"sha256:{h.hexdigest()}"


def _read_history() -> list:
    """读取升级历史"""
    if not os.path.exists(HISTORY_FILE):
        return []
    try:
        with open(HISTORY_FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    except Exception:
        return []


def _write_history(entry: dict):
    """追加升级历史"""
    history = _read_history()
    history.insert(0, entry)
    os.makedirs(os.path.dirname(HISTORY_FILE), exist_ok=True)
    with open(HISTORY_FILE, "w", encoding="utf-8") as f:
        json.dump(history, f, ensure_ascii=False, indent=2)


# ═══════════════════════════════════════════════════════════════════
# 回滚状态管理
# ═══════════════════════════════════════════════════════════════════

def _read_rollback_state() -> dict | None:
    """读取回滚状态文件"""
    if not os.path.exists(ROLLBACK_FILE):
        return None
    try:
        with open(ROLLBACK_FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    except Exception:
        return None


def _write_rollback_state(state: dict):
    """写入回滚状态文件"""
    os.makedirs(os.path.dirname(ROLLBACK_FILE), exist_ok=True)
    with open(ROLLBACK_FILE, "w", encoding="utf-8") as f:
        json.dump(state, f, ensure_ascii=False, indent=2)


def _delete_rollback_state():
    """删除回滚状态文件"""
    if os.path.exists(ROLLBACK_FILE):
        os.remove(ROLLBACK_FILE)


def _mark_step(step_name: str):
    """标记某步骤已完成"""
    state = _read_rollback_state()
    if state and step_name not in state.get("steps_completed", []):
        state["steps_completed"].append(step_name)
        _write_rollback_state(state)


def _create_backup(version_before: str, target_version: str, has_backend: bool, has_frontend: bool):
    """升级前创建备份，写入回滚状态文件"""
    logger.info("开始创建升级前备份...")
    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
    backup_dir = os.path.join(BACKUPS_DIR, ts)
    os.makedirs(backup_dir, exist_ok=True)

    rollback = {
        "upgrade_started_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        "version_before": version_before,
        "version_target": target_version,
        "steps_completed": [],
        "backups": {},
        "new_files": {"knowledge": [], "discretion": []},
        "backup_dir": backup_dir,
    }

    # ① 备份用户数据库（只备份用户数据，不备份 system.db）
    # system.db 包含知识库索引/裁量规则等大量数据，体积大且可从源文件重建，
    # 升级失败时无需回滚，重新同步即可。真正需要保护的是用户数据。
    data_dir = os.path.join(_PROJECT_ROOT, "bff-api", "data")
    db_files = {
        "user.db": os.path.join(data_dir, "user.db"),
        "runtime.db": os.path.join(data_dir, "runtime.db"),
    }
    # 兼容旧版：如果存在 zfzs_standalone.db 也备份（旧版单库模式）
    standalone_db = os.path.join(data_dir, "zfzs_standalone.db")
    if os.path.exists(standalone_db):
        db_files["zfzs_standalone.db"] = standalone_db

    for db_name, db_path in db_files.items():
        if os.path.exists(db_path):
            db_bak = os.path.join(backup_dir, db_name)
            shutil.copy2(db_path, db_bak)
            rollback["backups"][f"db_{db_name}"] = db_bak
            logger.info(f"数据库 {db_name} 已备份到: {db_bak}")

    # ② 备份 VERSION
    if os.path.exists(VERSION_FILE):
        ver_bak = os.path.join(backup_dir, "VERSION")
        shutil.copy2(VERSION_FILE, ver_bak)
        rollback["backups"]["version_file"] = ver_bak

    # ③ 后端代码备份（如果有后端更新，_replace_dir 会自动 rename → .bak）
    # 这里在备份目录中也保留一份
    if has_backend:
        app_path = os.path.join(_PROJECT_ROOT, "bff-api", "app")
        if os.path.exists(app_path):
            app_bak_dir = os.path.join(backup_dir, "app")
            shutil.copytree(app_path, app_bak_dir)
            rollback["backups"]["backend"] = app_bak_dir

    # ④ 前端代码备份
    if has_frontend:
        dist_path = os.path.join(_PROJECT_ROOT, "web", "dist")
        if os.path.exists(dist_path):
            dist_bak_dir = os.path.join(backup_dir, "dist")
            shutil.copytree(dist_path, dist_bak_dir)
            rollback["backups"]["frontend"] = dist_bak_dir

    # ⑤ 写入回滚状态
    _write_rollback_state(rollback)

    # ⑦ 同时保存备份元数据
    backup_info = {
        "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        "version": version_before,
        "type": "auto",
        "description": f"升级 v{version_before} → v{target_version} 前自动备份",
    }
    with open(os.path.join(backup_dir, "backup_info.json"), "w", encoding="utf-8") as f:
        json.dump(backup_info, f, ensure_ascii=False, indent=2)

    logger.info(f"备份完成，回滚状态已写入 {ROLLBACK_FILE}")


def _do_rollback() -> str:
    """执行自动回滚，返回结果消息"""
    state = _read_rollback_state()
    if not state:
        # V1.6.7 改进：当回滚状态文件不存在时，给出更具体的诊断信息
        backups_exist = os.path.exists(BACKUPS_DIR) and os.listdir(BACKUPS_DIR)
        if backups_exist:
            backup_list = sorted(os.listdir(BACKUPS_DIR), reverse=True)[:3]
            return (
                f"未找到回滚状态文件（{ROLLBACK_FILE}），无法自动回滚。\n"
                f"可能原因：备份阶段因文件被占用而失败（如数据库连接未断开）。\n"
                f"可用备份目录: {', '.join(backup_list)}\n"
                f"建议：请通过「备份管理」页面手动恢复最近的备份，或重启应用后重试升级。"
            )
        return (
            "未找到回滚状态文件和任何备份，无法自动回滚。\n"
            "说明：升级在备份阶段即已失败，原始数据未被修改，系统处于安全状态。\n"
            "建议：关闭并重新打开应用，然后重新上传升级包进行升级。"
        )

    logger.warning("开始自动回滚...")
    results = []
    steps = state.get("steps_completed", [])

    # 按逆序回滚
    # 恢复 VERSION
    if "version_write" in steps:
        try:
            ver_bak = state.get("backups", {}).get("version_file")
            if ver_bak and os.path.exists(ver_bak):
                shutil.copy2(ver_bak, VERSION_FILE)
                results.append("VERSION 已恢复")
        except Exception as e:
            results.append(f"恢复 VERSION 失败: {e}")

    # 删除新增的裁量文件
    if "discretion_copy" in steps:
        for f in state.get("new_files", {}).get("discretion", []):
            try:
                if os.path.exists(f):
                    os.remove(f)
            except Exception:
                pass
        results.append("已清理新增裁量文件")

    # 删除新增的知识库文件
    if "knowledge_copy" in steps:
        for f in state.get("new_files", {}).get("knowledge", []):
            try:
                if os.path.exists(f):
                    os.remove(f)
            except Exception:
                pass
        results.append("已清理新增知识库文件")

    # 恢复前端代码
    if "frontend_replace" in steps:
        try:
            dist_path = os.path.join(_PROJECT_ROOT, "web", "dist")
            dist_bak = dist_path + ".bak"
            frontend_bak = state.get("backups", {}).get("frontend")
            if os.path.exists(dist_bak):
                if os.path.exists(dist_path):
                    shutil.rmtree(dist_path)
                shutil.move(dist_bak, dist_path)
                results.append("前端代码已恢复")
            elif frontend_bak and os.path.exists(frontend_bak):
                if os.path.exists(dist_path):
                    shutil.rmtree(dist_path)
                shutil.copytree(frontend_bak, dist_path)
                results.append("前端代码已从备份恢复")
        except Exception as e:
            results.append(f"恢复前端代码失败: {e}")

    # 恢复后端代码
    if "backend_replace" in steps:
        try:
            app_path = os.path.join(_PROJECT_ROOT, "bff-api", "app")
            app_bak = app_path + ".bak"
            backend_bak = state.get("backups", {}).get("backend")
            if os.path.exists(app_bak):
                if os.path.exists(app_path):
                    shutil.rmtree(app_path)
                shutil.move(app_bak, app_path)
                results.append("后端代码已恢复")
            elif backend_bak and os.path.exists(backend_bak):
                if os.path.exists(app_path):
                    shutil.rmtree(app_path)
                shutil.copytree(backend_bak, app_path)
                results.append("后端代码已从备份恢复")
        except Exception as e:
            results.append(f"恢复后端代码失败: {e}")

    # 恢复用户数据库（最后做，确保数据一致）
    # 注意：不恢复 system.db（可从源文件重建），只恢复用户数据
    data_dir = os.path.join(_PROJECT_ROOT, "bff-api", "data")
    try:
        for db_name in ["user.db", "runtime.db"]:
            db_bak = state.get("backups", {}).get(f"db_{db_name}")
            if db_bak and os.path.exists(db_bak):
                db_dst = os.path.join(data_dir, db_name)
                shutil.copy2(db_bak, db_dst)
                results.append(f"{db_name} 已恢复")
        # 兼容旧格式
        db_bak = state.get("backups", {}).get("database")
        if db_bak and os.path.exists(db_bak):
            db_path = os.path.join(data_dir, "zfzs_standalone.db")
            shutil.copy2(db_bak, db_path)
            results.append("数据库(旧版)已恢复")
    except Exception as e:
        results.append(f"恢复数据库失败: {e}")

    _delete_rollback_state()
    result_msg = "回滚完成: " + "; ".join(results) if results else "回滚完成（无需恢复的内容）"
    logger.warning(result_msg)
    return result_msg


# ═══════════════════════════════════════════════════════════════════
# 备份管理 API
# ═══════════════════════════════════════════════════════════════════

@router.get("/system/backup/list")
async def list_backups(request: Request):
    """列出所有可用备份"""
    _require_admin(request)
    backups = []
    if not os.path.exists(BACKUPS_DIR):
        return {"success": True, "data": backups}

    for name in sorted(os.listdir(BACKUPS_DIR), reverse=True):
        backup_dir = os.path.join(BACKUPS_DIR, name)
        if not os.path.isdir(backup_dir):
            continue
        info_path = os.path.join(backup_dir, "backup_info.json")
        info = {}
        if os.path.exists(info_path):
            try:
                with open(info_path, "r", encoding="utf-8") as f:
                    info = json.load(f)
            except Exception:
                pass

        # 统计备份内容（用户数据库 + 兼容旧版）
        db_names = ["user.db", "runtime.db", "zfzs_standalone.db"]
        has_db = any(os.path.exists(os.path.join(backup_dir, n)) for n in db_names)
        has_app = os.path.exists(os.path.join(backup_dir, "app"))
        has_dist = os.path.exists(os.path.join(backup_dir, "dist"))
        db_size = sum(
            os.path.getsize(os.path.join(backup_dir, n))
            for n in db_names
            if os.path.exists(os.path.join(backup_dir, n))
        )

        backups.append({
            "id": name,
            "created_at": info.get("created_at", name),
            "version": info.get("version", "?"),
            "type": info.get("type", "unknown"),
            "description": info.get("description", ""),
            "has_database": has_db,
            "has_backend": has_app,
            "has_frontend": has_dist,
            "db_size": db_size,
            "db_size_str": f"{db_size / 1024 / 1024:.1f} MB" if db_size > 1024 * 1024 else f"{db_size / 1024:.1f} KB",
        })

    return {"success": True, "data": backups}


@router.post("/system/backup/restore")
async def restore_backup(request: Request, backup_id: str = Query(...)):
    """恢复指定备份"""
    _require_admin(request)
    backup_dir = os.path.join(BACKUPS_DIR, backup_id)
    if not os.path.exists(backup_dir) or not os.path.isdir(backup_dir):
        raise HTTPException(status_code=400, detail=f"备份不存在: {backup_id}")

    results = []
    errors = []

    # 恢复用户数据库（不恢复 system.db，可从源文件重建）
    data_dir = os.path.join(_PROJECT_ROOT, "bff-api", "data")
    for db_name in ["user.db", "runtime.db"]:
        db_src = os.path.join(backup_dir, db_name)
        if os.path.exists(db_src):
            db_dst = os.path.join(data_dir, db_name)
            try:
                if os.path.exists(db_dst):
                    shutil.copy2(db_dst, db_dst + ".before_restore")
                shutil.copy2(db_src, db_dst)
                results.append(f"{db_name} 已恢复")
            except Exception as e:
                errors.append(f"恢复 {db_name} 失败: {e}")
    # 兼容旧版
    standalone_src = os.path.join(backup_dir, "zfzs_standalone.db")
    if os.path.exists(standalone_src):
        standalone_dst = os.path.join(data_dir, "zfzs_standalone.db")
        try:
            if os.path.exists(standalone_dst):
                shutil.copy2(standalone_dst, standalone_dst + ".before_restore")
            shutil.copy2(standalone_src, standalone_dst)
            results.append("zfzs_standalone.db(旧版) 已恢复")
        except Exception as e:
            errors.append(f"恢复 zfzs_standalone.db 失败: {e}")

    # 恢复后端代码
    app_src = os.path.join(backup_dir, "app")
    if os.path.exists(app_src):
        app_dst = os.path.join(_PROJECT_ROOT, "bff-api", "app")
        try:
            if os.path.exists(app_dst):
                if os.path.exists(app_dst + ".before_restore"):
                    shutil.rmtree(app_dst + ".before_restore", ignore_errors=True)
                shutil.move(app_dst, app_dst + ".before_restore")
            shutil.copytree(app_src, app_dst)
            results.append("后端代码已恢复")
        except Exception as e:
            errors.append(f"恢复后端代码失败: {e}")

    # 恢复前端代码
    dist_src = os.path.join(backup_dir, "dist")
    if os.path.exists(dist_src):
        dist_dst = os.path.join(_PROJECT_ROOT, "web", "dist")
        try:
            if os.path.exists(dist_dst):
                if os.path.exists(dist_dst + ".before_restore"):
                    shutil.rmtree(dist_dst + ".before_restore", ignore_errors=True)
                shutil.move(dist_dst, dist_dst + ".before_restore")
            shutil.copytree(dist_src, dist_dst)
            results.append("前端代码已恢复")
        except Exception as e:
            errors.append(f"恢复前端代码失败: {e}")

    # 恢复 VERSION
    ver_src = os.path.join(backup_dir, "VERSION")
    if os.path.exists(ver_src):
        try:
            shutil.copy2(ver_src, VERSION_FILE)
            results.append("版本号已恢复")
        except Exception as e:
            errors.append(f"恢复版本号失败: {e}")

    if errors:
        raise HTTPException(status_code=500, detail="恢复过程中出现错误: " + "; ".join(errors))

    msg = "备份恢复成功！" + " / ".join(results) + "。请重启后端服务使更改生效。"
    logger.info(msg)
    return {"success": True, "data": {"message": msg}}


@router.post("/system/backup/delete")
async def delete_backup(request: Request, backup_id: str = Query(...)):
    """删除指定备份"""
    _require_admin(request)
    backup_dir = os.path.join(BACKUPS_DIR, backup_id)
    if not os.path.exists(backup_dir):
        raise HTTPException(status_code=400, detail=f"备份不存在: {backup_id}")
    try:
        shutil.rmtree(backup_dir)
        return {"success": True, "data": {"message": f"备份 {backup_id} 已删除"}}
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"删除失败: {e}")


# ═══════════════════════════════════════════════════════════════════
# 系统版本
# ═══════════════════════════════════════════════════════════════════

@router.get("/system/version")
async def get_system_version():
    """获取系统版本信息"""
    version = "0.0.0"
    if os.path.exists(VERSION_FILE):
        try:
            with open(VERSION_FILE, "r", encoding="utf-8") as f:
                version = f.read().strip()
        except Exception:
            pass

    release_date = ""
    if os.path.exists(VERSION_FILE):
        try:
            mtime = os.path.getmtime(VERSION_FILE)
            release_date = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d")
        except Exception:
            pass

    return {
        "success": True,
        "data": {
            "version": version,
            "release_date": release_date,
            "website": "https://www.dlzr.cn",
        },
    }


# ═══════════════════════════════════════════════════════════════════
# 离线升级包更新
# ═══════════════════════════════════════════════════════════════════

@router.post("/system/update/upload")
async def upload_update_package(
    request: Request,
    file: UploadFile = File(...),
):
    """上传升级包并校验，返回升级预览"""
    _require_admin(request)

    if not file.filename or not file.filename.endswith('.zip'):
        raise HTTPException(status_code=400, detail="请上传 .zip 格式的升级包")

    os.makedirs(TEMP_DIR, exist_ok=True)
    zip_path = os.path.join(TEMP_DIR, file.filename)
    content = await file.read()
    with open(zip_path, "wb") as f:
        f.write(content)

    extract_dir = os.path.join(TEMP_DIR, "extracted")
    if os.path.exists(extract_dir):
        shutil.rmtree(extract_dir)
    os.makedirs(extract_dir, exist_ok=True)

    try:
        with zipfile.ZipFile(zip_path, "r") as zf:
            zf.extractall(extract_dir)
    except zipfile.BadZipFile:
        raise HTTPException(status_code=400, detail="升级包文件损坏，无法解压")

    manifest_path = os.path.join(extract_dir, "manifest.json")
    if not os.path.exists(manifest_path):
        shutil.rmtree(extract_dir, ignore_errors=True)
        raise HTTPException(status_code=400, detail="升级包缺少 manifest.json")

    try:
        with open(manifest_path, "r", encoding="utf-8") as f:
            manifest = json.load(f)
    except json.JSONDecodeError:
        shutil.rmtree(extract_dir, ignore_errors=True)
        raise HTTPException(status_code=400, detail="manifest.json 格式错误")

    current_version = "0.0.0"
    if os.path.exists(VERSION_FILE):
        with open(VERSION_FILE, "r", encoding="utf-8") as f:
            current_version = f.read().strip()

    required_prev = manifest.get("previous_version", "0.0.0")
    if not _version_gte(current_version, required_prev):
        raise HTTPException(
            status_code=400,
            detail=f"当前版本 {current_version} 不满足升级要求（需要 >= {required_prev}），请先升级到 {required_prev}",
        )

    checksums = manifest.get("file_checksums", {})
    failures = []
    for zip_path_inner, expected_hash in checksums.items():
        file_path = os.path.join(extract_dir, zip_path_inner.replace("/", os.sep))
        if not os.path.exists(file_path):
            failures.append(f"缺失: {zip_path_inner}")
            continue
        actual_hash = _sha256_file(file_path)
        if actual_hash != expected_hash:
            failures.append(f"校验失败: {zip_path_inner}")

    if failures:
        shutil.rmtree(extract_dir, ignore_errors=True)
        raise HTTPException(status_code=400, detail="文件完整性校验失败:\n" + "\n".join(failures[:5]))

    _save_extract_path(extract_dir)

    components = manifest.get("components", {})
    preview = {
        "version": manifest.get("version", ""),
        "previous_version": manifest.get("previous_version", ""),
        "current_version": current_version,
        "description": manifest.get("description", ""),
        "publisher": manifest.get("publisher", ""),
        "package_date": manifest.get("package_date", ""),
        "components": [],
    }

    if components.get("backend", {}).get("included"):
        preview["components"].append({
            "type": "backend", "name": "后端代码",
            "version": components["backend"].get("version", ""),
            "description": components["backend"].get("description", ""),
            "file_count": components["backend"].get("file_count", 0),
            "migrations": components["backend"].get("migrations", []),
        })

    if components.get("frontend", {}).get("included"):
        preview["components"].append({
            "type": "frontend", "name": "前端代码",
            "version": components["frontend"].get("version", ""),
            "description": components["frontend"].get("description", ""),
            "file_count": components["frontend"].get("file_count", 0),
        })

    if components.get("database", {}).get("included"):
        preview["components"].append({
            "type": "database", "name": "数据库迁移",
            "description": components["database"].get("description", ""),
            "migrations": components["database"].get("migrations", []),
        })

    if components.get("knowledge"):
        k = components["knowledge"]
        parts = []
        for cat, info in k.items():
            if isinstance(info, dict) and info.get("file_count"):
                parts.append(f"{cat} {info['file_count']} 篇")
        if parts:
            preview["components"].append({"type": "knowledge", "name": "知识库", "description": "、".join(parts)})

    if components.get("causes", {}).get("included"):
        preview["components"].append({
            "type": "causes", "name": "案由库",
            "description": f"约 {components['causes'].get('record_count', '?')} 条",
        })

    if components.get("discretion", {}).get("included"):
        regions = components["discretion"].get("regions", [])
        preview["components"].append({
            "type": "discretion", "name": "处罚裁量",
            "description": "、".join(regions) if regions else "裁量数据",
        })

    if components.get("post_script", {}).get("included"):
        preview["components"].append({"type": "post_script", "name": "升级后脚本", "description": ""})

    logger.info(f"升级包校验通过: {file.filename}, 版本 {manifest.get('version')}")
    return {"success": True, "data": preview}


@router.post("/system/update/execute")
async def execute_update(request: Request):
    """执行升级（SSE 流式推送进度），失败时自动回滚
    
    V1.6.7 升级流程改进：
    Step -1: 断开数据库连接池 → 释放 system.db 文件锁
    Step 0:  创建备份（此时文件锁已释放，可正常复制）
    Step 1~7: 原有升级步骤
    Step 8:  自动重启应用（使新加密机制立即生效）
    """
    _require_admin(request)

    extract_dir = _get_extract_path()
    if not extract_dir or not os.path.exists(extract_dir):
        raise HTTPException(status_code=400, detail="请先上传升级包")

    manifest_path = os.path.join(extract_dir, "manifest.json")
    with open(manifest_path, "r", encoding="utf-8") as f:
        manifest = json.load(f)

    components = manifest.get("components", {})
    target_version = manifest.get("version", "")

    async def event_generator():
        has_db = components.get("database", {}).get("included")
        has_backend = components.get("backend", {}).get("included")
        has_frontend = components.get("frontend", {}).get("included")
        has_knowledge = bool(components.get("knowledge"))
        has_discretion = components.get("discretion", {}).get("included")
        has_post = components.get("post_script", {}).get("included")

        def emit(step_name: str, progress: int, message: str, status: str = "running"):
            return json.dumps({
                "step": step_name, "progress": progress, "message": message, "status": status,
            }, ensure_ascii=False)

        current_ver = "0.0.0"
        if os.path.exists(VERSION_FILE):
            with open(VERSION_FILE, "r", encoding="utf-8") as f:
                current_ver = f.read().strip()

        try:
            # ═════════════════════════════════════════════════════
            # Step -1: 断开所有数据库连接池（释放文件锁）
            # ═════════════════════════════════════════════════════
            yield f"data: {emit('准备环境', 0, '正在断开数据库连接...')}\n\n"
            await asyncio.sleep(0.05)
            try:
                await close_pool()
                logger.info("数据库连接池已断开，文件锁已释放")
            except Exception as pool_err:
                # 连接池可能本来就没有活跃连接，不视为致命错误
                logger.warning(f"断开连接池时出现异常（可能无活跃连接）: {pool_err}")
            await asyncio.sleep(0.3)  # 等待操作系统释放文件锁
            yield f"data: {emit('准备环境', 1, '数据库连接已断开')}\n\n"

            # ═════════════════════════════════════════════════════
            # Step 0: 创建完整备份 + 写入回滚状态
            # ═════════════════════════════════════════════════════
            yield f"data: {emit('创建备份', 3, '正在备份数据库...')}\n\n"
            await asyncio.sleep(0.05)
            _create_backup(current_ver, target_version, has_backend, has_frontend)
            yield f"data: {emit('创建备份', 6, '备份完成')}\n\n"
            await asyncio.sleep(0.05)

            # ═════════════════════════════════════════════════════
            # Step 1: 数据库迁移
            # ═════════════════════════════════════════════════════
            if has_db:
                migrations = components["database"].get("migrations", [])
                if migrations:
                    for mig_file in migrations:
                        mig_path = os.path.join(extract_dir, "backend", "scripts", mig_file)
                        if os.path.exists(mig_path):
                            yield f"data: {emit('执行数据库迁移', 10, f'执行迁移: {mig_file}')}\n\n"
                            await asyncio.sleep(0.1)
                            _run_migration(mig_path)
                yield f"data: {emit('执行数据库迁移', 15, '数据库迁移完成')}\n\n"
            else:
                yield f"data: {emit('数据库迁移', 15, '无需迁移')}\n\n"
            await asyncio.sleep(0.1)
            _mark_step("migration")

            # ═════════════════════════════════════════════════════
            # Step 2: 后端代码
            # ═════════════════════════════════════════════════════
            if has_backend:
                yield f"data: {emit('更新后端代码', 20, '更新后端代码...')}\n\n"
                await asyncio.sleep(0.05)
                _replace_dir(
                    os.path.join(extract_dir, "backend", "app"),
                    os.path.join(_PROJECT_ROOT, "bff-api", "app"),
                )
                scripts_src = os.path.join(extract_dir, "backend", "scripts")
                scripts_dst = os.path.join(_PROJECT_ROOT, "bff-api", "scripts")
                if os.path.exists(scripts_src):
                    for fname in os.listdir(scripts_src):
                        src = os.path.join(scripts_src, fname)
                        dst = os.path.join(scripts_dst, fname)
                        if os.path.isfile(src):
                            os.makedirs(scripts_dst, exist_ok=True)
                            shutil.copy2(src, dst)
                req_src = os.path.join(extract_dir, "backend", "requirements.txt")
                req_dst = os.path.join(_PROJECT_ROOT, "bff-api", "requirements.txt")
                if os.path.exists(req_src):
                    shutil.copy2(req_src, req_dst)
                yield f"data: {emit('更新后端代码', 30, '安装依赖...')}\n\n"
                await asyncio.sleep(0.05)
                _install_dependencies()
                yield f"data: {emit('更新后端代码', 35, '后端代码更新完成')}\n\n"
            else:
                yield f"data: {emit('后端代码', 35, '无需更新')}\n\n"
            await asyncio.sleep(0.1)
            _mark_step("backend_replace")

            # ═════════════════════════════════════════════════════
            # Step 3: 前端代码
            # ═════════════════════════════════════════════════════
            if has_frontend:
                yield f"data: {emit('更新前端代码', 40, '更新前端代码...')}\n\n"
                await asyncio.sleep(0.05)
                _replace_dir(
                    os.path.join(extract_dir, "frontend", "dist"),
                    os.path.join(_PROJECT_ROOT, "web", "dist"),
                )
                yield f"data: {emit('更新前端代码', 50, '前端代码更新完成')}\n\n"
            else:
                yield f"data: {emit('前端代码', 50, '无需更新')}\n\n"
            await asyncio.sleep(0.1)
            _mark_step("frontend_replace")

            # ═════════════════════════════════════════════════════
            # Step 4: 数据文件
            # ═════════════════════════════════════════════════════
            yield f"data: {emit('更新数据文件', 55, '更新数据文件...')}\n\n"
            await asyncio.sleep(0.05)

            # 知识库增量追加（追踪新增文件）
            if has_knowledge:
                knowledge_src = os.path.join(extract_dir, "data", "zhishiku")
                if os.path.exists(knowledge_src):
                    new_files = _copy_dir_merge_tracked(knowledge_src, ZHISHIKU_BASE)
                    # 保存新增文件列表到回滚状态
                    state = _read_rollback_state()
                    if state:
                        state["new_files"]["knowledge"] = new_files
                        _write_rollback_state(state)
                    _mark_step("knowledge_copy")
                    yield f"data: {emit('更新数据文件', 60, f'知识库文件已更新（新增 {len(new_files)} 个文件），正在同步数据库...')}\n\n"
                    await asyncio.sleep(0.05)
                    await _sync_knowledge_to_db()
                    yield f"data: {emit('更新数据文件', 65, '知识库数据库已同步')}\n\n"
                    await asyncio.sleep(0.05)

            # 裁量数据（追踪新增文件）
            if has_discretion:
                discretion_src = os.path.join(extract_dir, "discretion")
                if os.path.exists(discretion_src):
                    new_files = _copy_dir_merge_tracked(discretion_src, DISCRETION_BASE)
                    state = _read_rollback_state()
                    if state:
                        state["new_files"]["discretion"] = new_files
                        _write_rollback_state(state)
                    _mark_step("discretion_copy")
                    yield f"data: {emit('更新数据文件', 75, f'裁量数据已更新（新增 {len(new_files)} 个文件）')}\n\n"
                    await asyncio.sleep(0.05)

            yield f"data: {emit('更新数据文件', 78, '数据文件更新完成')}\n\n"
            await asyncio.sleep(0.1)

            # ═════════════════════════════════════════════════════
            # Step 5: 升级后脚本
            # ═════════════════════════════════════════════════════
            if has_post:
                post_script = os.path.join(extract_dir, "scripts", "post_update.py")
                if os.path.exists(post_script):
                    yield f"data: {emit('执行升级后脚本', 80, '执行升级后脚本...')}\n\n"
                    await asyncio.sleep(0.05)
                    _run_post_script(post_script)
                    yield f"data: {emit('执行升级后脚本', 88, '升级后脚本执行完成')}\n\n"
            else:
                yield f"data: {emit('升级后脚本', 88, '无需执行')}\n\n"
            await asyncio.sleep(0.1)

            # ═════════════════════════════════════════════════════
            # Step 6: 更新版本号
            # ═════════════════════════════════════════════════════
            os.makedirs(os.path.dirname(VERSION_FILE), exist_ok=True)
            with open(VERSION_FILE, "w", encoding="utf-8") as f:
                f.write(target_version)
            yield f"data: {emit('更新版本号', 92, f'版本号已更新: v{target_version}')}\n\n"
            await asyncio.sleep(0.05)
            _mark_step("version_write")

            # ═════════════════════════════════════════════════════
            # Step 7: 记录升级历史
            # ═════════════════════════════════════════════════════
            comp_types = []
            if has_backend: comp_types.append("后端")
            if has_frontend: comp_types.append("前端")
            if has_db: comp_types.append("数据库")
            if has_knowledge or has_discretion: comp_types.append("数据")
            _write_history({
                "time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                "from_version": current_ver,
                "to_version": target_version,
                "type": "全量" if len(comp_types) >= 3 else "+".join(comp_types) if comp_types else "未知",
                "description": manifest.get("description", ""),
                "result": "成功",
            })

            _delete_rollback_state()

            # ═════════════════════════════════════════════════════
            # Step 8: 自动重启应用（V1.6.7 新增）
            # ═════════════════════════════════════════════════════
            restart_triggered = False
            restart_script = os.path.join(_PROJECT_ROOT, "restart.bat")
            if os.path.exists(restart_script):
                yield f"data: {emit('重启应用', 96, '升级完成！正在自动重启应用以加载新版本...')}\n\n"
                await asyncio.sleep(0.5)
                try:
                    subprocess.Popen(
                        ["cmd", "/c", "start", restart_script],
                        cwd=_PROJECT_ROOT,
                        creationflags=subprocess.CREATE_NEW_CONSOLE,
                    )
                    restart_triggered = True
                    logger.info("升级成功，已触发自动重启")
                except Exception as restart_err:
                    logger.warning(f"自动重启失败（不影响升级结果）: {restart_err}")
                    yield f"data: {emit('重启应用', 97, f'自动重启失败: {restart_err}，请手动重启应用')}\n\n"
            else:
                logger.info("未找到 restart.bat，跳过自动重启")

            # ═════════════════════════════════════════════════════
            # 完成
            # ═════════════════════════════════════════════════════
            if restart_triggered:
                final_msg = f'升级完成！系统正在重启，即将加载 v{target_version}...'
            else:
                final_msg = f'升级完成！系统已更新到 v{target_version}，请手动重启应用使更改完全生效。'
            yield f"data: {emit('完成', 100, final_msg, 'done')}\n\n"

            _clear_extract_path()
            shutil.rmtree(extract_dir, ignore_errors=True)

        except Exception as e:
            logger.error(f"升级执行失败: {e}\n{traceback.format_exc()}")
            err_msg = str(e)[:200]

            # 检测是否为备份阶段失败（通常是文件被占用）
            is_backup_failure = any(kw in err_msg.lower() for kw in ["backup", "备份", "被占用", "used by another", "permission denied", "process cannot"])

            if is_backup_failure:
                yield f"data: {emit('回滚', 0, '升级失败: 数据库文件被占用，无法备份。', 'error')}\n\n"
                await asyncio.sleep(0.2)
                yield f"data: {emit('提示', 0, '建议：请使用「外部升级工具 upgrade.bat」在关闭应用后进行升级。', 'error')}\n\n"
                yield f"data: {emit('提示', 0, '操作方法：关闭本应用 → 将 update-v1.6.7.zip 和 upgrade.bat 放到同一目录 → 双击 upgrade.bat', 'error')}\n\n"
            else:
                yield f"data: {emit('回滚', 0, f'升级失败: {err_msg}，正在自动回滚...', 'error')}\n\n"
                await asyncio.sleep(0.2)

                try:
                    rollback_msg = _do_rollback()
                    yield f"data: {emit('回滚', 0, rollback_msg + '。请刷新页面。', 'error')}\n\n"
                except Exception as rb_err:
                    logger.error(f"回滚本身也失败了: {rb_err}")
                    yield f"data: {emit('回滚', 0, f'回滚失败: {str(rb_err)[:200]}。建议使用外部升级工具 upgrade.bat。', 'error')}\n\n"

            _clear_extract_path()
            shutil.rmtree(extract_dir, ignore_errors=True)

    return StreamingResponse(
        event_generator(),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
            "X-Accel-Buffering": "no",
        },
    )


@router.get("/system/update/history")
async def get_update_history(request: Request):
    """获取升级历史"""
    _require_admin(request)
    history = _read_history()
    return {"success": True, "data": history}


@router.post("/system/update/restart")
async def restart_service(request: Request):
    """重启后端服务"""
    _require_admin(request)
    restart_script = os.path.join(_PROJECT_ROOT, "restart.bat")
    if not os.path.exists(restart_script):
        raise HTTPException(status_code=500, detail=f"重启脚本不存在: {restart_script}")
    try:
        subprocess.Popen(
            ["cmd", "/c", "start", restart_script],
            cwd=_PROJECT_ROOT,
            creationflags=subprocess.CREATE_NEW_CONSOLE,
        )
        return {"success": True, "data": {"message": "正在重启服务..."}}
    except Exception as e:
        logger.error(f"重启失败: {e}")
        raise HTTPException(status_code=500, detail=f"重启失败: {str(e)}")


# ═══════════════════════════════════════════════════════════════════
# 辅助函数
# ═══════════════════════════════════════════════════════════════════

def _version_gte(v1: str, v2: str) -> bool:
    """比较版本号 v1 >= v2"""
    try:
        parts1 = [int(x) for x in v1.split(".")]
        parts2 = [int(x) for x in v2.split(".")]
        while len(parts1) < 3:
            parts1.append(0)
        while len(parts2) < 3:
            parts2.append(0)
        return tuple(parts1) >= tuple(parts2)
    except Exception:
        return True


def _save_extract_path(path: str):
    marker = os.path.join(TEMP_DIR, "current_extract.txt")
    with open(marker, "w", encoding="utf-8") as f:
        f.write(path)


def _get_extract_path() -> str | None:
    marker = os.path.join(TEMP_DIR, "current_extract.txt")
    if os.path.exists(marker):
        with open(marker, "r", encoding="utf-8") as f:
            return f.read().strip()
    return None


def _clear_extract_path():
    marker = os.path.join(TEMP_DIR, "current_extract.txt")
    if os.path.exists(marker):
        os.remove(marker)


def _replace_dir(src: str, dst: str):
    """整体替换目录：先备份旧目录，再复制新目录"""
    if not os.path.exists(src):
        return
    if os.path.exists(dst):
        bak = dst.rstrip(os.sep) + ".bak"
        if os.path.exists(bak):
            shutil.rmtree(bak, ignore_errors=True)
        shutil.move(dst, bak)
    shutil.copytree(src, dst)


def _copy_dir_merge(src: str, dst: str):
    """增量合并目录：将 src 下所有文件复制到 dst，同名覆盖"""
    for root, dirs, files in os.walk(src):
        rel = os.path.relpath(root, src)
        target_dir = os.path.join(dst, rel)
        os.makedirs(target_dir, exist_ok=True)
        for fname in files:
            s = os.path.join(root, fname)
            d = os.path.join(target_dir, fname)
            shutil.copy2(s, d)


def _copy_dir_merge_tracked(src: str, dst: str) -> list:
    """增量合并目录，返回新增文件的绝对路径列表"""
    new_files = []
    for root, dirs, files in os.walk(src):
        rel = os.path.relpath(root, src)
        target_dir = os.path.join(dst, rel)
        os.makedirs(target_dir, exist_ok=True)
        for fname in files:
            s = os.path.join(root, fname)
            d = os.path.join(target_dir, fname)
            if not os.path.exists(d):
                new_files.append(d)
            shutil.copy2(s, d)
    return new_files


def _run_migration(script_path: str):
    """执行数据库迁移脚本"""
    try:
        result = subprocess.run(
            [shutil.which("python") or "python", script_path],
            capture_output=True, text=True, timeout=60, cwd=_PROJECT_ROOT,
            creationflags=0x08000000,
        )
        if result.returncode != 0:
            logger.warning(f"迁移脚本返回非零: {result.stderr}")
        logger.info(f"迁移脚本输出: {result.stdout}")
    except Exception as e:
        logger.error(f"执行迁移脚本失败: {e}")
        raise


def _install_dependencies():
    """安装 Python 依赖"""
    try:
        pip = os.path.join(_PROJECT_ROOT, "bff-api", "venv", "Scripts", "pip.exe")
        if not os.path.exists(pip):
            pip = shutil.which("pip") or "pip"
        result = subprocess.run(
            [pip, "install", "-r", "requirements.txt"],
            capture_output=True, text=True, timeout=180,
            cwd=os.path.join(_PROJECT_ROOT, "bff-api"),
            creationflags=0x08000000,
        )
        logger.info(f"依赖安装输出: {result.stdout[-500:]}")
    except Exception as e:
        logger.error(f"安装依赖失败: {e}")


def _run_post_script(script_path: str):
    """执行升级后脚本"""
    try:
        result = subprocess.run(
            [shutil.which("python") or "python", script_path],
            capture_output=True, text=True, timeout=120, cwd=_PROJECT_ROOT,
            creationflags=0x08000000,
        )
        logger.info(f"升级后脚本输出: {result.stdout}")
    except Exception as e:
        logger.error(f"执行升级后脚本失败: {e}")



async def _sync_knowledge_to_db():
    """内部知识库同步：扫描 zhishiku/ 目录，将 .md 文档元数据同步到 kb_documents 表"""
    import re as _re
    from ...core.database import get_system_db as _get_system_db_internal, get_db as _get_db_internal, _has_split_databases

    CATEGORY_MAP = {'law': '法律法规', 'case': '处罚案例', 'standard': '处罚裁量'}

    def _parse_fm(text):
        match = _re.match(r'^---\s*\n(.*?)\n---\s*\n', text, _re.DOTALL)
        if not match or not match.group(1):
            return {}
        fm = {}
        list_key = None
        list_items = []
        for line in match.group(1).split('\n'):
            s = line.strip()
            if s.startswith('- '):
                item = s[2:].strip().strip('"').strip("'")
                if list_key is not None:
                    list_items.append(item)
                continue
            ci = line.find(':')
            if ci > 0:
                if list_key and list_items:
                    fm[list_key] = list_items
                    list_key = None
                    list_items = []
                k = line[:ci].strip()
                v = line[ci+1:].strip().strip('"').strip("'")
                if v == '':
                    list_key = k
                    list_items = []
                else:
                    if v.startswith('[') and v.endswith(']'):
                        try:
                            fm[k] = json.loads(v)
                        except Exception:
                            fm[k] = v
                    else:
                        fm[k] = v
        if list_key and list_items:
            fm[list_key] = list_items
        return fm

    async for conn, cur in (_get_system_db_internal() if _has_split_databases() else _get_db_internal()):
        await cur.execute("""
            CREATE TABLE IF NOT EXISTS kb_documents (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                title TEXT NOT NULL, version TEXT DEFAULT '', version_name TEXT DEFAULT '',
                region TEXT DEFAULT '', region_code TEXT DEFAULT '', category TEXT NOT NULL,
                effective_date TEXT DEFAULT '', expiry_date TEXT DEFAULT '', status TEXT DEFAULT '有效',
                source TEXT DEFAULT '', source_url TEXT DEFAULT '', tags TEXT DEFAULT '[]',
                content TEXT DEFAULT '', file_path TEXT DEFAULT '', file_size INTEGER DEFAULT 0,
                sub_dir TEXT DEFAULT '', anyou TEXT DEFAULT '[]', issuer TEXT DEFAULT '',
                import_status TEXT DEFAULT 'pending', view_count INTEGER DEFAULT 0,
                created_at TEXT NOT NULL DEFAULT (datetime('now')),
                updated_at TEXT NOT NULL DEFAULT (datetime('now'))
            )
        """)
        await conn.commit()

        total = new_count = update_count = 0
        for root, dirs, files in os.walk(ZHISHIKU_BASE):
            for f in files:
                if not (f.endswith('.md') or f.endswith('.md.enc')):
                    continue
                full_path = os.path.join(root, f)
                rel_path = os.path.relpath(full_path, ZHISHIKU_BASE).replace('\\', '/')
                if rel_path.endswith('.md.enc'):
                    rel_path = rel_path[:-4]  # 去掉 .enc 后缀，保持与数据库记录一致
                top_dir = rel_path.split('/')[0]
                category = CATEGORY_MAP.get(top_dir, top_dir)

                try:
                    raw = read_text_file(full_path)
                except Exception:
                    continue

                fm = _parse_fm(raw)
                content = _re.sub(r'^---\s*\n.*?\n---\s*\n', '', raw, count=1, flags=_re.DOTALL).strip()
                title = fm.get('title', f.replace('.md.enc', '').replace('.md', ''))
                file_size = os.path.getsize(full_path)
                tags = json.dumps(fm.get('tags', []), ensure_ascii=False) if fm.get('tags') else '[]'
                anyou = json.dumps(fm.get('anyou', []), ensure_ascii=False) if fm.get('anyou') else '[]'
                parts = rel_path.split('/')
                sub_dir = '/'.join(parts[1:-1]) if len(parts) > 2 else ''

                await cur.execute("SELECT id FROM kb_documents WHERE file_path = %s", (rel_path,))
                existing = await cur.fetchone()

                if existing:
                    await cur.execute("""
                        UPDATE kb_documents SET title=%s, version=%s, version_name=%s, region=%s,
                        region_code=%s, category=%s, effective_date=%s, expiry_date=%s, status=%s,
                        source=%s, source_url=%s, tags=%s, content=%s, file_size=%s,
                        sub_dir=%s, anyou=%s, issuer=%s, updated_at=datetime('now')
                        WHERE file_path=%s
                    """, (
                        title, fm.get('version', ''), fm.get('version_name', ''),
                        fm.get('region', ''), fm.get('region_code', ''), category,
                        fm.get('effective_date', ''), fm.get('expiry_date', ''),
                        fm.get('status', '有效'), fm.get('source', ''), fm.get('source_url', ''),
                        tags, content, file_size, sub_dir, anyou, fm.get('issuer', ''), rel_path,
                    ))
                    update_count += 1
                else:
                    await cur.execute("""
                        INSERT INTO kb_documents (title, version, version_name, region, region_code,
                        category, effective_date, expiry_date, status, source, source_url, tags,
                        content, file_path, file_size, sub_dir, anyou, issuer, import_status)
                        VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
                    """, (
                        title, fm.get('version', ''), fm.get('version_name', ''),
                        fm.get('region', ''), fm.get('region_code', ''), category,
                        fm.get('effective_date', ''), fm.get('expiry_date', ''),
                        fm.get('status', '有效'), fm.get('source', ''), fm.get('source_url', ''),
                        tags, content, rel_path, file_size, sub_dir, anyou, fm.get('issuer', ''),
                        'completed',
                    ))
                    new_count += 1
                total += 1

        await conn.commit()
        logger.info(f"知识库同步完成: 总计 {total}, 新增 {new_count}, 更新 {update_count}")
        break


# ═══════════════════════════════════════════════════════════════════
# 原有数据更新接口（保留不变）
# ═══════════════════════════════════════════════════════════════════

@router.get("/system-update/status")
async def get_update_status(request: Request, db=Depends(_system_db)):
    """获取三个数据源的当前状态"""
    _require_admin(request)
    conn, cur = db

    law_count = _count_files(os.path.join(ZHISHIKU_BASE, "law"), ".md")
    case_count = _count_files(os.path.join(ZHISHIKU_BASE, "case"), ".md")
    standard_count = _count_files(os.path.join(ZHISHIKU_BASE, "standard"), ".md") + _count_files(os.path.join(ZHISHIKU_BASE, "standard"), ".csv")

    await cur.execute("SELECT COUNT(*) as cnt FROM case_causes")
    row = await cur.fetchone()
    causes_count = row["cnt"] if row else 0

    await cur.execute("SELECT COUNT(*) as cnt FROM discretion_rules")
    row = await cur.fetchone()
    discretion_count = row["cnt"] if row else 0

    await cur.execute("SELECT source_file, COUNT(*) as cnt FROM discretion_rules GROUP BY source_file")
    discretion_regions = {r["source_file"]: r["cnt"] for r in await cur.fetchall()}

    return {
        "success": True,
        "data": {
            "knowledge": {
                "law_count": law_count,
                "case_count": case_count,
                "standard_count": standard_count,
                "total": law_count + case_count + standard_count,
            },
            "causes": {
                "count": causes_count,
            },
            "discretion": {
                "count": discretion_count,
                "regions": discretion_regions,
                "data_dir": DISCRETION_BASE,
            },
        },
    }


@router.post("/system-update/knowledge/upload")
async def upload_knowledge_file(
    request: Request,
    category: str = Query(..., description="分类: law/case/standard"),
    file: UploadFile = File(...),
):
    _require_admin(request)
    cat_map = {"law": "law", "case": "case", "standard": "standard"}
    if category not in cat_map:
        raise HTTPException(status_code=400, detail=f"无效分类: {category}")
    target_dir = os.path.join(ZHISHIKU_BASE, cat_map[category])
    os.makedirs(target_dir, exist_ok=True)
    filename = file.filename or "unnamed"
    target_path = os.path.join(target_dir, filename)
    content = await file.read()
    with open(target_path, "wb") as f:
        f.write(content)
    logger.info(f"知识库文件已上传: {target_path} ({len(content)} bytes)")
    return {"success": True, "data": {"path": target_path, "size": len(content)}}


@router.post("/system-update/knowledge/reindex")
async def reindex_knowledge(request: Request):
    _require_admin(request)
    try:
        law_count = _count_files(os.path.join(ZHISHIKU_BASE, "law"), ".md")
        case_count = _count_files(os.path.join(ZHISHIKU_BASE, "case"), ".md")
        standard_count = _count_files(os.path.join(ZHISHIKU_BASE, "standard"), ".md")
        return {
            "success": True,
            "data": {
                "message": f"索引重建已排队：法律法规 {law_count} 篇，处罚案例 {case_count} 篇，处罚裁量 {standard_count} 篇",
                "law_count": law_count, "case_count": case_count, "standard_count": standard_count,
            },
        }
    except Exception as e:
        logger.error(f"索引重建失败: {e}")
        raise HTTPException(status_code=500, detail=f"索引重建失败: {str(e)}")


@router.post("/system-update/discretion/upload")
async def upload_discretion_csv(
    request: Request,
    region: str = Query(..., description="地区名称"),
    file: UploadFile = File(...),
):
    _require_admin(request)
    if not file.filename or not file.filename.endswith('.csv'):
        raise HTTPException(status_code=400, detail="请上传 CSV 文件")
    valid_names = {"从轻处罚.csv", "减轻处罚.csv", "不予处罚.csv", "免于强制措施.csv"}
    if file.filename not in valid_names:
        raise HTTPException(status_code=400, detail=f"文件名必须是以下之一: {', '.join(valid_names)}")
    target_dir = os.path.join(DISCRETION_BASE, region)
    os.makedirs(target_dir, exist_ok=True)
    target_path = os.path.join(target_dir, file.filename)
    content = await file.read()
    with open(target_path, "wb") as f:
        f.write(content)
    logger.info(f"裁量 CSV 已上传: {target_path} ({len(content)} bytes)")
    return {"success": True, "data": {"path": target_path, "size": len(content), "region": region}}


@router.post("/system-update/discretion/reimport")
async def reimport_discretion(request: Request, db=Depends(_system_db)):
    _require_admin(request)
    conn, cur = db
    from ...services.discretion_rule_service import clear_and_reimport
    await clear_and_reimport(conn, cur)
    await cur.execute("SELECT COUNT(*) as cnt FROM discretion_rules")
    total = (await cur.fetchone())["cnt"]
    await cur.execute("SELECT source_file, COUNT(*) as cnt FROM discretion_rules GROUP BY source_file")
    regions = {r["source_file"]: r["cnt"] for r in await cur.fetchall()}
    return {"success": True, "data": {"total": total, "regions": regions}}


# ═══════════════════════════════════════════════════
#  意见反馈
# ═══════════════════════════════════════════════════

from typing import List

@router.post("/feedback/submit")
async def submit_feedback(
    request: Request,
    type: str = Form(...),
    content: str = Form(...),
    contact: str = Form(""),
    images: List[UploadFile] = File(default=[]),
):
    """提交意见反馈，发送邮件到管理员邮箱（支持图片附件）"""
    _require_admin(request)

    if not content.strip():
        raise HTTPException(status_code=400, detail="反馈内容不能为空")

    if not settings.SMTP_USER or not settings.SMTP_PASSWORD:
        raise HTTPException(
            status_code=500,
            detail="邮件服务未配置，请在 .env 中配置 SMTP_USER 和 SMTP_PASSWORD",
        )

    # 读取附件
    attachments = []
    if images:
        for img in images:
            if img.filename:
                data = await img.read()
                if data:
                    attachments.append((img.filename, data))

    # 获取当前用户名
    user = get_current_user(request)
    username = user.get("username", "") if user else ""

    success = send_feedback_email(
        feedback_type=type,
        content=content.strip(),
        contact=contact.strip(),
        username=username,
        attachments=attachments if attachments else None,
    )

    if success:
        return {"success": True, "message": "反馈已提交，感谢您的意见！"}
    else:
        raise HTTPException(status_code=500, detail="邮件发送失败，请稍后重试")
