from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from .core.config import settings
from .core.database import close_pool, get_pool, get_system_db, get_user_db, get_runtime_db, _has_split_databases
from .core.llm_client import llm_client
from .core.api_log_middleware import APILogMiddleware
from .core.crypto import read_text_file, is_encrypted
from .core import paths
import os
import sys
import json
import logging
import asyncio

# Windows 编码兼容：确保 UTF-8 模式
os.environ.setdefault("PYTHONUTF8", "1")
if sys.stdout and hasattr(sys.stdout, 'reconfigure'):
    try:
        sys.stdout.reconfigure(encoding='utf-8')
    except Exception:
        pass
if sys.stderr and hasattr(sys.stderr, 'reconfigure'):
    try:
        sys.stderr.reconfigure(encoding='utf-8')
    except Exception:
        pass

logger = logging.getLogger(__name__)


app = FastAPI(
    title="综合执法智能辅助系统 - BFF API",
    description="BFF后端服务，提供管理后台和用户端API接口",
    version="1.0.0",
    docs_url="/docs",
    redoc_url="/redoc",
)

# 构建 CORS 来源列表，支持通过 CORS_EXTRA_ORIGINS 环境变量添加额外来源
_cors_origins = [o.strip() for o in settings.CORS_ORIGINS.split(",") if o.strip()]
if getattr(settings, 'CORS_EXTRA_ORIGINS', None):
    _cors_origins.extend([o.strip() for o in settings.CORS_EXTRA_ORIGINS.split(",") if o.strip()])

app.add_middleware(
    CORSMiddleware,
    allow_origins=_cors_origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.add_middleware(APILogMiddleware)


# ── 反爬虫中间件：频率检测 + 三级升级处罚 + 知识库锁定 ──
from starlette.middleware.base import BaseHTTPMiddleware as _BaseHTTPMiddleware
from starlette.requests import Request as _Request
from starlette.responses import JSONResponse as _JSONResponse
import collections as _collections
import time as _time
import os as _os

# 不做频率检测的路径（认证、健康检查、静态资源、解封接口）
_ANTISCRAPE_SKIP_PREFIXES = ("/api/user/login", "/api/user/register", "/api/user/status",
                             "/api/user/logout", "/api/user/reset", "/api/license",
                             "/api/admin/unlock-knowledge-base",
                             "/health", "/docs", "/redoc", "/openapi.json",
                             "/api/user/password", "/web", "/assets", "/favicon")


def _get_kb_encrypted_marker() -> str:
    """获取 system.db.encrypted 标记文件路径"""
    from .core.paths import get_data_dir
    return _os.path.join(get_data_dir(), "data", "system.db.encrypted")


def _is_kb_locked() -> bool:
    """检查知识库是否被锁定（.encrypted 标记不存在）"""
    marker = _get_kb_encrypted_marker()
    return _os.path.exists(marker + ".locked")


def _lock_knowledge_base(reason: str = "anti_scrape"):
    """锁定知识库：将 .encrypted 标记重命名为 .encrypted.locked"""
    marker = _get_kb_encrypted_marker()
    locked = marker + ".locked"
    if _os.path.exists(marker):
        _os.rename(marker, locked)
        logger.warning(f"[反爬] 知识库已锁定: {marker} → {locked}")
    elif not _os.path.exists(locked):
        logger.warning(f"[反爬] 锁定失败: {marker} 不存在")

    # 写入封禁记录到 runtime.db
    try:
        import json, sqlite3
        from .core.paths import get_data_dir
        runtime_db = _os.path.join(get_data_dir(), "data", "runtime.db")
        if _os.path.exists(runtime_db):
            conn = sqlite3.connect(runtime_db)
            conn.execute(
                "INSERT OR REPLACE INTO system_configs (config_key, config_value) VALUES (?, ?)",
                ("kb_locked", json.dumps({
                    "locked_at": _time.strftime("%Y-%m-%dT%H:%M:%S"),
                    "reason": reason,
                }, ensure_ascii=False))
            )
            conn.commit()
            conn.close()
    except Exception as e:
        logger.error(f"[反爬] 写入封禁记录失败: {e}")


def _unlock_knowledge_base():
    """解封知识库：将 .encrypted.locked 恢复为 .encrypted"""
    marker = _get_kb_encrypted_marker()
    locked = marker + ".locked"
    if _os.path.exists(locked):
        _os.rename(locked, marker)
        logger.info(f"[反爬] 知识库已解封: {locked} → {marker}")

    # 清除封禁记录
    try:
        import sqlite3
        from .core.paths import get_data_dir
        runtime_db = _os.path.join(get_data_dir(), "data", "runtime.db")
        if _os.path.exists(runtime_db):
            conn = sqlite3.connect(runtime_db)
            conn.execute("DELETE FROM system_configs WHERE config_key = 'kb_locked'")
            conn.commit()
            conn.close()
    except Exception:
        pass


class AntiScrapeMiddleware(_BaseHTTPMiddleware):
    """检测异常请求频率，三级升级处罚，最终锁定知识库"""

    # 配置
    MAX_PER_SECOND = 10        # 每秒最大请求数
    WINDOW_SECONDS = 1         # 检测窗口
    MAX_WARNINGS = 2           # 最大警告次数（第 3 次锁定知识库）

    def __init__(self, app):
        super().__init__(app)
        self._max = self.MAX_PER_SECOND * self.WINDOW_SECONDS
        self._window = self.WINDOW_SECONDS
        self._requests: dict[str, list[float]] = _collections.defaultdict(list)
        self._violations: dict[str, int] = _collections.defaultdict(int)

    async def dispatch(self, request: _Request, call_next):
        path = request.url.path

        # 跳过非 API 和认证相关路径
        if any(path.startswith(p) for p in _ANTISCRAPE_SKIP_PREFIXES):
            return await call_next(request)

        # 只检测 API 请求
        if not path.startswith("/api/"):
            return await call_next(request)

        # 本地请求跳过（桌面应用模式，无需反爬）
        client_host = request.client.host if request.client else ""
        if client_host in ("127.0.0.1", "::1", "localhost"):
            return await call_next(request)

        client_ip = request.client.host if request.client else "unknown"
        now = _time.time()
        cutoff = now - self._window

        # 检查知识库是否已锁定
        if _is_kb_locked():
            # 锁定状态下，只允许认证和解封相关请求
            return JSONResponse(
                {"detail": "系统检测到异常访问行为，知识库已被锁定。如需解封，请联系管理员。", "code": "KB_LOCKED"},
                status_code=403,
            )

        # 清理过期记录
        timestamps = self._requests[client_ip]
        self._requests[client_ip] = [t for t in timestamps if t > cutoff]

        # 检查频率
        if len(self._requests[client_ip]) >= self._max:
            self._violations[client_ip] += 1
            violation = self._violations[client_ip]

            if violation >= self.MAX_WARNINGS + 1:
                # 第 3 次违规 → 锁定知识库
                logger.warning(f"[反爬] 第 {violation} 次违规，锁定知识库: {client_ip}")
                _lock_knowledge_base()
                # 清除登录 token
                try:
                    from .api.user.auth import clear_login_token
                    clear_login_token()
                except Exception:
                    pass
                return JSONResponse(
                    {"detail": "系统检测到异常访问行为，知识库已被锁定。如需解封，请联系管理员。", "code": "KB_LOCKED"},
                    status_code=403,
                )
            else:
                # 第 1、2 次违规 → 警告 + 踢 token
                remaining = self.MAX_WARNINGS + 1 - violation
                logger.warning(f"[反爬] 第 {violation} 次违规: {client_ip}, 剩余 {remaining} 次机会")
                try:
                    from .api.user.auth import clear_login_token
                    clear_login_token()
                except Exception:
                    pass
                return JSONResponse(
                    {
                        "detail": f"请求过于频繁，请合理使用系统。否则系统将封禁不可用。（警告 {violation}/{self.MAX_WARNINGS}，第 {self.MAX_WARNINGS + 1} 次将永久锁定知识库）",
                        "code": "RATE_LIMITED",
                        "warning": violation,
                        "max_warnings": self.MAX_WARNINGS,
                    },
                    status_code=401,
                )

        self._requests[client_ip].append(now)
        return await call_next(request)

app.add_middleware(AntiScrapeMiddleware)


# ── 知识库锁定检查中间件 ──
_KB_API_PREFIXES = ("/api/user/knowledge", "/api/admin/knowledge",
                    "/api/admin/knowledge-base", "/api/admin/kb-md-documents")

class KnowledgeBaseLockMiddleware(_BaseHTTPMiddleware):
    """检查知识库是否被锁定，锁定时拦截知识库相关 API"""

    async def dispatch(self, request: _Request, call_next):
        path = request.url.path

        # 只拦截知识库相关 API
        if not any(path.startswith(p) for p in _KB_API_PREFIXES):
            return await call_next(request)

        # 检查是否被锁定
        if _is_kb_locked():
            return JSONResponse(
                {"detail": "知识库已被锁定，如需解封请联系管理员。", "code": "KB_LOCKED"},
                status_code=403,
            )

        return await call_next(request)

app.add_middleware(KnowledgeBaseLockMiddleware)


# 静态数据响应缓存中间件：对不常变的 GET 接口添加 Cache-Control 头
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response, JSONResponse

# URL 前缀 → 缓存时间（秒）
_CACHE_RULES: list[tuple[str, int]] = [
    ("/api/user/features", 60),                # 功能开关：1分钟
    ("/api/user/system-config", 60),           # 系统配置：1分钟
    ("/api/admin/system-config", 60),          # 管理端系统配置：1分钟
    ("/api/user/document-templates", 0),       # 文书模板：不缓存（调试期）
    ("/api/user/doc-gen/template-list", 300),   # 文书模板列表：5分钟
    ("/api/user/welcome-message", 120),        # 欢迎消息：2分钟
    ("/api/user/quick-prompts", 120),           # 快捷提问：2分钟
    ("/api/admin/quick-prompts", 120),          # 管理端快捷提问：2分钟
    ("/api/user/ai/vision-status", 60),         # 视觉模型状态：1分钟
    ("/api/user/ai/scenes", 60),                # AI场景配置：1分钟
]

class CacheControlMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        response: Response = await call_next(request)
        # 仅对 GET 请求添加缓存头
        if request.method == "GET":
            path = request.url.path
            for prefix, max_age in _CACHE_RULES:
                if path.startswith(prefix):
                    response.headers["Cache-Control"] = f"public, max-age={max_age}"
                    break
        return response

app.add_middleware(CacheControlMiddleware)


@app.on_event("shutdown")
async def shutdown():
    await llm_client.close()
    await close_pool()


async def _sync_document_templates():
    """启动时将 index.json 中的文书模板同步到数据库（UPSERT模式）"""
    import json
    from .core.database import get_db, get_system_db, _has_split_databases

    # 快速检查：模板已存在则跳过（打包的 system.db 已包含模板数据）
    # 但需要修复模板文件路径（开发环境和打包环境路径不同）
    try:
        async for conn, cur in get_system_db():
            await cur.execute("SELECT COUNT(*) as cnt FROM document_templates WHERE is_deleted = 0")
            row = await cur.fetchone()
            cnt = row["cnt"] if isinstance(row, dict) else row[0]
            if cnt > 0:
                # 检查并修复模板路径：开发环境路径 → 打包环境路径
                await cur.execute("SELECT id, template_file_path FROM document_templates WHERE is_deleted = 0 LIMIT 1")
                sample = await cur.fetchone()
                if sample:
                    sample_path = sample["template_file_path"] if isinstance(sample, dict) else sample[1]
                    if sample_path and not os.path.exists(sample_path):
                        # 路径无效，重写为打包目录
                        tpl_base = paths.resolve_resource_path("bff-api/templates")
                        logger.info(f"文书模板路径失效，重写为打包路径: {tpl_base}")
                        await cur.execute(
                            "SELECT id, template_file_path, template_name FROM document_templates WHERE is_deleted = 0"
                        )
                        fixed = 0
                        for tpl_row in await cur.fetchall():
                            r = dict(tpl_row)
                            old_path = r.get("template_file_path", "")
                            if old_path:
                                basename = os.path.basename(old_path)
                                new_path = os.path.join(tpl_base, basename)
                                await cur.execute(
                                    "UPDATE document_templates SET template_file_path = ? WHERE id = ?",
                                    (new_path, r["id"])
                                )
                                fixed += 1
                        await conn.commit()
                        logger.info(f"文书模板路径修复: {fixed} 个")
                # 补录 schema_TEXT（placeholder_mapping）：打包时可能未写入该字段
                await cur.execute(
                    "SELECT COUNT(*) as cnt FROM document_templates WHERE is_deleted = 0 AND (schema_TEXT IS NULL OR schema_TEXT = '')"
                )
                null_row = await cur.fetchone()
                null_cnt = null_row["cnt"] if isinstance(null_row, dict) else null_row[0]
                if null_cnt > 0:
                    base_dir = settings.TEMPLATE_BASE_DIR
                    if not os.path.isabs(base_dir):
                        base_dir = paths.resolve_resource_path(base_dir)
                    idx_path = os.path.join(base_dir, "index.json")
                    if os.path.exists(idx_path):
                        with open(idx_path, "r", encoding="utf-8") as f:
                            idx_templates = json.load(f)
                        backfilled = 0
                        for t in idx_templates:
                            pm = t.get("placeholder_mapping", {})
                            if not pm:
                                continue
                            schema_json = json.dumps(pm, ensure_ascii=False)
                            tc = t.get("id", "")
                            await cur.execute(
                                "UPDATE document_templates SET schema_TEXT = ? WHERE template_code = ? AND (schema_TEXT IS NULL OR schema_TEXT = '')",
                                (schema_json, tc),
                            )
                            if cur.rowcount > 0:
                                backfilled += 1
                        if backfilled:
                            await conn.commit()
                            logger.info(f"文书模板 schema_TEXT 补录: {backfilled} 个")
                logger.info(f"文书模板已存在（{cnt} 个），跳过同步")
                return
            break
    except Exception:
        pass  # 表可能不存在，继续执行同步逻辑

    base = settings.TEMPLATE_BASE_DIR
    if not os.path.isabs(base):
        base = paths.resolve_resource_path(base)

    index_path = os.path.join(base, "index.json")
    logger.info(f"文书模板同步: 读取索引文件 {index_path}")
    if not os.path.exists(index_path):
        logger.warning(f"模板索引文件不存在，跳过同步: {index_path}")
        return

    with open(index_path, "r", encoding="utf-8") as f:
        templates = json.load(f)

    if not templates:
        logger.info("模板索引为空，跳过同步")
        return

    # document_templates 属于 system.db
    _db_gen = get_system_db() if _has_split_databases() else get_db()
    async for conn, cur in _db_gen:
        try:
            synced = 0
            updated = 0
            for t in templates:
                template_code = t.get("id", "")
                if not template_code:
                    continue

                docx_filename = t.get("files", {}).get("docx", "")
                template_file_path = os.path.join(base, docx_filename) if docx_filename else ""
                # 模板文件路径（明文 .docx）
                if template_file_path and not os.path.exists(template_file_path):
                    pass  # 路径不存在时保留原值，启动后路径修复逻辑会处理

                placeholder_mapping = t.get("placeholder_mapping", {})
                schema_json = json.dumps(placeholder_mapping, ensure_ascii=False) if placeholder_mapping else None

                template_type = template_code.replace("L-", "").split("-")[0] if "-" in template_code else ""
                category = t.get("category", "")

                await cur.execute(
                    "SELECT id FROM document_templates WHERE template_code = ? AND is_deleted = 0",
                    (template_code,),
                )
                existing_row = await cur.fetchone()

                if existing_row:
                    await cur.execute(
                        """UPDATE document_templates SET
                           template_name=?, template_category=?, template_type=?,
                           description=?, template_file_path=?, schema_TEXT=?,
                           layout_type=?, updated_at=datetime('now')
                           WHERE template_code=?""",
                        (t.get("name", ""), category, template_type,
                         f"文书模板：{t.get('name', '')}", template_file_path,
                         schema_json, t.get("layout", ""),
                         template_code),
                    )
                    updated += 1
                else:
                    await cur.execute(
                        """INSERT INTO document_templates
                           (template_code, template_name, template_category, template_type,
                            description, version, template_file_path, schema_TEXT,
                            layout_type, is_system, is_active, is_deleted, usage_count)
                           VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 1, 0, 0)""",
                        (template_code, t.get("name", ""), category, template_type,
                         f"文书模板：{t.get('name', '')}", "1.0",
                         template_file_path, schema_json, t.get("layout", "")),
                    )
                    synced += 1

            await conn.commit()
            logger.info(f"文书模板同步完成：新增 {synced} 个，更新 {updated} 个，共 {len(templates)} 个模板")
        except Exception as e:
            logger.error(f"文书模板同步出错: {e}")
        break


async def _ensure_tables_exist():
    """启动时确保关键表存在（兼容模式：单库或三库）"""
    if _has_split_databases():
        # 逐个初始化，某个数据库失败不影响其他数据库
        for db_name, ensure_fn in [
            ("system", _ensure_system_tables),
            ("user", _ensure_user_tables),
            ("runtime", _ensure_runtime_tables),
        ]:
            try:
                await ensure_fn()
            except Exception as e:
                logger.warning(f"{db_name}.db 表初始化失败（非致命）: {e}")
    else:
        await _ensure_tables_exist_legacy()


async def _ensure_system_tables():
    """启动时确保 system.db 表存在"""
    logger.info("检查 system.db 表结构...")
    async for conn, cur in get_system_db():
        # document_templates
        try:
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='document_templates'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS document_templates (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        template_code VARCHAR(100) NOT NULL UNIQUE,
                        template_name VARCHAR(200) NOT NULL,
                        template_category VARCHAR(50) NOT NULL,
                        template_type VARCHAR(10) NOT NULL,
                        description TEXT,
                        version VARCHAR(10) DEFAULT '1.0',
                        template_file_path VARCHAR(500) NOT NULL,
                        schema_TEXT TEXT,
                        prompt_content LONGTEXT,
                        docx_preview_url VARCHAR(500),
                        default_values TEXT,
                        validation_rules TEXT,
                        preview_image_path VARCHAR(500),
                        layout_type VARCHAR(20) DEFAULT 'standard',
                        is_system TINYINT DEFAULT 1,
                        is_active TINYINT DEFAULT 1,
                        is_deleted TINYINT DEFAULT 0,
                        usage_count INT DEFAULT 0,
                        created_by INT,
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_tpl_category ON document_templates(template_category)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_tpl_active ON document_templates(is_active, is_deleted)"
                )
                await conn.commit()
                logger.info("已创建 document_templates 表 (system.db)")
        except Exception as e:
            logger.error(f"创建 document_templates 表出错: {e}")

        # evidence_rules
        try:
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='evidence_rules'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS evidence_rules (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        domain VARCHAR(100) NOT NULL DEFAULT '',
                        case_type VARCHAR(200) NOT NULL DEFAULT '',
                        target_objects TEXT DEFAULT '',
                        key_data TEXT DEFAULT '',
                        compliance_req TEXT DEFAULT '',
                        is_deleted INTEGER DEFAULT 0,
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_ev_rules_domain ON evidence_rules(domain)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_ev_rules_case_type ON evidence_rules(case_type)"
                )
                await conn.commit()
                logger.info("已创建 evidence_rules 表 (system.db)")
        except Exception as e:
            logger.error(f"创建 evidence_rules 表出错: {e}")

        # evidence_domains
        try:
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='evidence_domains'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS evidence_domains (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        domain VARCHAR(100) NOT NULL DEFAULT '',
                        case_type VARCHAR(200) NOT NULL DEFAULT '',
                        rule_count INTEGER DEFAULT 0,
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await cur.execute(
                    "CREATE UNIQUE INDEX IF NOT EXISTS idx_ev_domains_unique ON evidence_domains(domain, case_type)"
                )
                await conn.commit()
                logger.info("已创建 evidence_domains 表 (system.db)")
        except Exception as e:
            logger.error(f"创建 evidence_domains 表出错: {e}")

        # 自动导入电子取证 CSV 数据（仅在 evidence_rules 为空时）
        try:
            await cur.execute("SELECT COUNT(*) FROM evidence_rules")
            count_row = await cur.fetchone()
            if count_row and count_row[0] == 0:
                await _import_evidence_csv(conn, cur)
        except Exception as e:
            logger.error(f"电子取证 CSV 导入检查出错: {e}")

        # kb_documents（v2.0 起从 user.db 迁移到 system.db）
        try:
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='kb_documents'"
            )
            if not await cur.fetchone():
                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 '',
                        chunk_strategy TEXT DEFAULT 'auto',
                        chunk_count INTEGER DEFAULT 0,
                        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 cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_kb_docs_category ON kb_documents(category)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_kb_docs_region ON kb_documents(region)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_kb_docs_status ON kb_documents(status)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_kb_docs_title ON kb_documents(title)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_kb_docs_file_path ON kb_documents(file_path)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_kb_docs_category_region ON kb_documents(category, region)"
                )
                await conn.commit()
                logger.info("已创建 kb_documents 表 (system.db)")
        except Exception as e:
            logger.error(f"创建 kb_documents 表出错: {e}")

        # FTS5 全文索引（system.db）
        try:
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='kb_documents_fts'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE VIRTUAL TABLE IF NOT EXISTS kb_documents_fts USING fts5(
                        title, content, category, region, anyou, file_path,
                        content='kb_documents', content_rowid='id'
                    )
                """)
                await cur.execute("""
                    CREATE TRIGGER IF NOT EXISTS kb_docs_ai AFTER INSERT ON kb_documents BEGIN
                        INSERT INTO kb_documents_fts(rowid, title, content, category, region, anyou, file_path)
                        VALUES (new.id, new.title, new.content, new.category, new.region, new.anyou, new.file_path);
                    END
                """)
                await cur.execute("""
                    CREATE TRIGGER IF NOT EXISTS kb_docs_ad AFTER DELETE ON kb_documents BEGIN
                        INSERT INTO kb_documents_fts(kb_documents_fts, rowid, title, content, category, region, anyou, file_path)
                        VALUES ('delete', old.id, old.title, old.content, old.category, old.region, old.anyou, old.file_path);
                    END
                """)
                await cur.execute("""
                    CREATE TRIGGER IF NOT EXISTS kb_docs_au AFTER UPDATE ON kb_documents BEGIN
                        INSERT INTO kb_documents_fts(kb_documents_fts, rowid, title, content, category, region, anyou, file_path)
                        VALUES ('delete', old.id, old.title, old.content, old.category, old.region, old.anyou, old.file_path);
                        INSERT INTO kb_documents_fts(rowid, title, content, category, region, anyou, file_path)
                        VALUES (new.id, new.title, new.content, new.category, new.region, new.anyou, new.file_path);
                    END
                """)
                await cur.execute("""
                    INSERT INTO kb_documents_fts(rowid, title, content, category, region, anyou, file_path)
                    SELECT id, title, content, category, region, anyou, file_path FROM kb_documents
                """)
                await conn.commit()
                logger.info("已创建 FTS5 全文索引 kb_documents_fts (system.db)")
        except Exception as e:
            logger.error(f"创建 FTS5 全文索引出错: {e}")

        break


async def _ensure_user_tables():
    """启动时确保 user.db 表存在"""
    logger.info("检查 user.db 表结构...")
    async for conn, cur in get_user_db():
        try:
            # users
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='users'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS users (
                        id TEXT PRIMARY KEY,
                        username TEXT NOT NULL UNIQUE,
                        password_hash TEXT NOT NULL,
                        real_name TEXT DEFAULT '',
                        email TEXT DEFAULT '',
                        phone TEXT DEFAULT '',
                        role TEXT DEFAULT 'user',
                        level TEXT DEFAULT '',
                        parent_id TEXT DEFAULT '',
                        group_id INTEGER DEFAULT NULL,
                        edition TEXT DEFAULT '',
                        department TEXT DEFAULT '',
                        badge_number TEXT DEFAULT '',
                        org_phone TEXT DEFAULT '',
                        org_address TEXT DEFAULT '',
                        unit_contact_phone TEXT DEFAULT '',
                        unit_contact_address TEXT DEFAULT '',
                        is_active INTEGER DEFAULT 1,
                        is_protected INTEGER DEFAULT 0,
                        enabled_menus TEXT DEFAULT '',
                        max_accounts INTEGER DEFAULT NULL,
                        account_expires_at TEXT DEFAULT NULL,
                        is_reviewer INTEGER DEFAULT 0,
                        responsible_areas TEXT DEFAULT '',
                        last_login_at TEXT DEFAULT NULL,
                        login_attempts INTEGER DEFAULT 0,
                        locked_until TEXT DEFAULT NULL,
                        is_deleted TINYINT DEFAULT 0,
                        enforcement_region TEXT DEFAULT '',
                        enforcement_region_path TEXT DEFAULT '',
                        org_name TEXT DEFAULT '',
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await conn.commit()
                logger.info("已创建 users 表 (user.db)")

            # group_menus
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='group_menus'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS group_menus (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        group_id INTEGER NOT NULL,
                        menu_code VARCHAR(50) NOT NULL,
                        menu_name VARCHAR(100) NOT NULL,
                        parent_code VARCHAR(50) DEFAULT NULL,
                        icon VARCHAR(50) DEFAULT '',
                        route_path VARCHAR(200) DEFAULT '',
                        sort_order INTEGER DEFAULT 0,
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await conn.commit()
                logger.info("已创建 group_menus 表 (user.db)")

            # inquiry_strategies
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='inquiry_strategies'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS inquiry_strategies (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        case_id VARCHAR(64) NOT NULL,
                        user_id INT NOT NULL,
                        industry_category VARCHAR(50) DEFAULT '',
                        industry_subcategory VARCHAR(100) DEFAULT '',
                        case_type VARCHAR(50) DEFAULT 'general',
                        is_auto_recommended TINYINT DEFAULT 0,
                        case_description TEXT,
                        supplementary_content TEXT,
                        attachment_ids TEXT DEFAULT '[]',
                        strategy_content LONGTEXT,
                        strategy_version INT DEFAULT 1,
                        generation_prompt TEXT,
                        ai_model_used VARCHAR(100) DEFAULT '',
                        generation_time FLOAT DEFAULT 0,
                        token_usage TEXT DEFAULT '{}',
                        strategy_config TEXT DEFAULT '{}',
                        status VARCHAR(20) DEFAULT 'generated',
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_inq_strat_case ON inquiry_strategies(case_id)"
                )
                await conn.commit()
                logger.info("已创建 inquiry_strategies 表 (user.db)")

            # case_related_cache
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='case_related_cache'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS case_related_cache (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        case_id VARCHAR(64) NOT NULL UNIQUE,
                        related_cases TEXT DEFAULT '[]',
                        similar_penalty_cases TEXT DEFAULT '[]',
                        match_snapshot TEXT DEFAULT '{}',
                        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_rel_cache_case ON case_related_cache(case_id)"
                )
                await conn.commit()
                logger.info("已创建 case_related_cache 表 (user.db)")

            # case_causes_detail
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='case_causes_detail'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS case_causes_detail (
                        id VARCHAR(64) PRIMARY KEY,
                        case_id VARCHAR(64) NOT NULL,
                        cause_name VARCHAR(200) NOT NULL,
                        cause_code VARCHAR(20),
                        level1_code VARCHAR(10),
                        level1_name VARCHAR(50),
                        level2_code VARCHAR(10),
                        level2_name VARCHAR(100),
                        cause_source VARCHAR(20) DEFAULT 'user_custom',
                        ai_confidence VARCHAR(10),
                        sort_order INTEGER DEFAULT 0,
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_ccd_case ON case_causes_detail(case_id)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_ccd_case_sort ON case_causes_detail(case_id, sort_order)"
                )
                await conn.commit()
                logger.info("已创建 case_causes_detail 表 (user.db)")

            # case_cause_laws
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='case_cause_laws'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS case_cause_laws (
                        id VARCHAR(64) PRIMARY KEY,
                        cause_detail_id VARCHAR(64),
                        case_id VARCHAR(64) NOT NULL,
                        law_type VARCHAR(20) NOT NULL,
                        law_name VARCHAR(200),
                        article_number VARCHAR(50),
                        article_full_ref VARCHAR(200),
                        content_summary TEXT,
                        content_detail TEXT,
                        source VARCHAR(20) DEFAULT 'manual',
                        source_id VARCHAR(64),
                        sort_order INTEGER DEFAULT 0,
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_ccl_case ON case_cause_laws(case_id)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_ccl_cause_detail ON case_cause_laws(cause_detail_id)"
                )
                await conn.commit()
                logger.info("已创建 case_cause_laws 表 (user.db)")

            # knowledge_view_history
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='knowledge_view_history'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS knowledge_view_history (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        user_id TEXT NOT NULL DEFAULT '1',
                        point_id TEXT NOT NULL,
                        collection TEXT DEFAULT 'markdown_docs',
                        title TEXT DEFAULT '',
                        doc_path TEXT DEFAULT '',
                        view_count INTEGER DEFAULT 1,
                        viewed_at TEXT DEFAULT (datetime('now')),
                        UNIQUE(user_id, point_id)
                    )
                """)
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_kvhist_user ON knowledge_view_history(user_id)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_kvhist_viewed ON knowledge_view_history(viewed_at)"
                )
                await conn.commit()
                logger.info("已创建 knowledge_view_history 表 (user.db)")

            # evidence_checklists
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='evidence_checklists'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS evidence_checklists (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        user_id VARCHAR(36) NOT NULL DEFAULT '1',
                        title VARCHAR(200) NOT NULL DEFAULT '',
                        domain VARCHAR(100) DEFAULT '',
                        case_type VARCHAR(200) DEFAULT '',
                        status VARCHAR(20) DEFAULT 'draft',
                        notes TEXT DEFAULT '',
                        is_deleted INTEGER DEFAULT 0,
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_ev_cl_user ON evidence_checklists(user_id)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_ev_cl_status ON evidence_checklists(status)"
                )
                await conn.commit()
                logger.info("已创建 evidence_checklists 表 (user.db)")

            # evidence_checklist_items
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='evidence_checklist_items'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS evidence_checklist_items (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        checklist_id INTEGER NOT NULL,
                        rule_id INTEGER DEFAULT NULL,
                        item_type VARCHAR(20) NOT NULL DEFAULT 'custom',
                        content TEXT NOT NULL DEFAULT '',
                        sort_order INTEGER DEFAULT 0,
                        is_checked INTEGER DEFAULT 0,
                        user_note TEXT DEFAULT '',
                        is_deleted INTEGER DEFAULT 0,
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_ev_items_cl ON evidence_checklist_items(checklist_id)"
                )
                await conn.commit()
                logger.info("已创建 evidence_checklist_items 表 (user.db)")

            # cases（案件管理核心表，从 MySQL 迁移到 SQLite）
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='cases'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS cases (
                        id TEXT PRIMARY KEY,
                        case_number TEXT NOT NULL,
                        case_type TEXT DEFAULT '',
                        title TEXT NOT NULL,
                        cause TEXT DEFAULT '',
                        cause_code TEXT DEFAULT '',
                        cause_level1_name TEXT DEFAULT '',
                        cause_level2_name TEXT DEFAULT '',
                        cause_count INTEGER DEFAULT 0,
                        cause_confirmed INTEGER DEFAULT 0,
                        cause_confirmed_at TEXT DEFAULT NULL,
                        causes_merged TEXT DEFAULT '',
                        violation_laws_summary TEXT DEFAULT '',
                        penalty_laws_summary TEXT DEFAULT '',
                        discretion_advice TEXT DEFAULT '{}',
                        source TEXT DEFAULT '',
                        priority TEXT DEFAULT 'normal',
                        filing_date TEXT DEFAULT NULL,
                        party_name TEXT DEFAULT '',
                        party_type TEXT DEFAULT '',
                        credit_code TEXT DEFAULT '',
                        legal_representative TEXT DEFAULT '',
                        party_id_type TEXT DEFAULT '',
                        party_id_number TEXT DEFAULT '',
                        phone TEXT DEFAULT '',
                        address TEXT DEFAULT '',
                        illegal_fact_summary TEXT DEFAULT '',
                        involved_amount REAL DEFAULT NULL,
                        main_officer TEXT DEFAULT '{}',
                        assistant_officer TEXT DEFAULT '{}',
                        remark TEXT DEFAULT '',
                        description TEXT DEFAULT '',
                        info_pool TEXT DEFAULT '{}',
                        deerflow_thread_id TEXT DEFAULT NULL,
                        document_count INTEGER DEFAULT 0,
                        thread_message_count INTEGER DEFAULT 0,
                        is_deleted INTEGER DEFAULT 0,
                        deleted_at TEXT DEFAULT NULL,
                        status TEXT DEFAULT 'in_progress',
                        party_info TEXT DEFAULT '{}',
                        evidence TEXT DEFAULT '{}',
                        legal_basis TEXT DEFAULT '',
                        assigned_to TEXT DEFAULT NULL,
                        department_id TEXT DEFAULT NULL,
                        created_by TEXT NOT NULL,
                        group_id INTEGER DEFAULT NULL,
                        enforcement_region TEXT DEFAULT '',
                        transferred_from TEXT DEFAULT NULL,
                        transferred_at TEXT DEFAULT NULL,
                        transfer_reason TEXT DEFAULT '',
                        created_at TEXT DEFAULT (datetime('now')),
                        updated_at TEXT DEFAULT (datetime('now'))
                    )
                """)
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_cases_number ON cases(case_number)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_cases_created_by ON cases(created_by)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_cases_status ON cases(status)"
                )
                await conn.commit()
                logger.info("已创建 cases 表 (user.db)")

            # 迁移：为已有 cases 表补充列（每次启动都执行，已存在则跳过）
            for col, col_def in [
                ("transferred_from", "TEXT DEFAULT NULL"),
                ("transferred_at", "TEXT DEFAULT NULL"),
                ("transfer_reason", "TEXT DEFAULT ''"),
                ("discretion_advice", "TEXT DEFAULT '{}'"),
            ]:
                try:
                    await cur.execute(f"ALTER TABLE cases ADD COLUMN {col} {col_def}")
                except Exception:
                    pass  # 列已存在则跳过
            await conn.commit()

            # user_memories（用户记忆/个人中心数据）
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='user_memories'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS user_memories (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        user_id TEXT NOT NULL,
                        memory_type TEXT DEFAULT 'user',
                        target_id TEXT DEFAULT NULL,
                        content TEXT DEFAULT '',
                        content_summary TEXT DEFAULT '',
                        is_archived INTEGER DEFAULT 0,
                        created_at TEXT DEFAULT (datetime('now')),
                        updated_at TEXT DEFAULT (datetime('now'))
                    )
                """)
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_um_user_id ON user_memories(user_id)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_um_memory_type ON user_memories(memory_type)"
                )
                await conn.commit()
                logger.info("已创建 user_memories 表 (user.db)")

            # case_logs（案件操作日志）
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='case_logs'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS case_logs (
                        id TEXT PRIMARY KEY,
                        case_id TEXT NOT NULL,
                        event_type TEXT NOT NULL,
                        event_data TEXT DEFAULT NULL,
                        operator_id TEXT NOT NULL,
                        operator_name TEXT DEFAULT '',
                        created_at TEXT DEFAULT (datetime('now')),
                        resource_id TEXT DEFAULT NULL,
                        resource_type TEXT DEFAULT NULL,
                        attachment_path TEXT DEFAULT NULL
                    )
                """)
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_case_logs_case_id ON case_logs(case_id)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_case_logs_event_type ON case_logs(event_type)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_case_logs_operator_id ON case_logs(operator_id)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_case_logs_created_at ON case_logs(created_at)"
                )
                await conn.commit()
                logger.info("已创建 case_logs 表 (user.db)")

            # kb_md_documents（MD文档导入管理）
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='kb_md_documents'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS kb_md_documents (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        source_doc_id INTEGER DEFAULT 0,
                        doc_name TEXT NOT NULL,
                        category TEXT DEFAULT '',
                        description TEXT DEFAULT '',
                        content TEXT DEFAULT '',
                        csv_content TEXT DEFAULT '',
                        file_type TEXT DEFAULT '',
                        region TEXT DEFAULT '',
                        issuer TEXT DEFAULT '',
                        effective_date TEXT DEFAULT '',
                        doc_version TEXT DEFAULT '',
                        import_status TEXT DEFAULT 'pending',
                        wiki_page_id TEXT DEFAULT '',
                        qdrant_collection TEXT DEFAULT '',
                        qdrant_point_ids TEXT DEFAULT '[]',
                        graphiti_imported INTEGER DEFAULT 0,
                        chunk_strategy TEXT DEFAULT 'auto',
                        chunk_count INTEGER DEFAULT 0,
                        contributor_id INTEGER DEFAULT NULL,
                        contributor_name TEXT DEFAULT '',
                        source_upload_time DATETIME DEFAULT NULL,
                        remark TEXT DEFAULT '',
                        deleted_at DATETIME DEFAULT NULL,
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await cur.execute("CREATE INDEX IF NOT EXISTS idx_kb_md_cat ON kb_md_documents(category)")
                await cur.execute("CREATE INDEX IF NOT EXISTS idx_kb_md_status ON kb_md_documents(import_status)")
                await conn.commit()
                logger.info("已创建 kb_md_documents 表 (user.db)")

            # welcome_messages（欢迎消息）
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='welcome_messages'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS welcome_messages (
                        id TEXT PRIMARY KEY,
                        title TEXT NOT NULL,
                        content TEXT DEFAULT '',
                        status INTEGER DEFAULT 0,
                        created_by TEXT DEFAULT '',
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await conn.commit()
                logger.info("已创建 welcome_messages 表 (user.db)")

            # case_documents（案件文书生成记录）
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='case_documents'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS case_documents (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        case_id TEXT NOT NULL,
                        template_id TEXT NOT NULL,
                        template_name TEXT DEFAULT '',
                        document_name TEXT DEFAULT '',
                        version INTEGER DEFAULT 1,
                        parent_id INTEGER DEFAULT NULL,
                        status TEXT DEFAULT 'draft',
                        content_md_path TEXT DEFAULT '',
                        content_docx_path TEXT DEFAULT '',
                        basic_info TEXT DEFAULT '{}',
                        generated_content TEXT DEFAULT '{}',
                        generation_method TEXT DEFAULT 'ai_chat',
                        generation_prompt TEXT DEFAULT '',
                        created_by TEXT NOT NULL,
                        is_deleted INTEGER DEFAULT 0,
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await cur.execute("CREATE INDEX IF NOT EXISTS idx_cd_case ON case_documents(case_id)")
                await cur.execute("CREATE INDEX IF NOT EXISTS idx_cd_template ON case_documents(template_id)")
                await conn.commit()
                logger.info("已创建 case_documents 表 (user.db)")

            # case_threads（案件会话线程表，简版暂未启用但需预建防止查询报错）
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='case_threads'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS case_threads (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        case_id TEXT NOT NULL,
                        deerflow_thread_id TEXT,
                        thread_id TEXT,
                        agent_id TEXT,
                        thread_title TEXT DEFAULT '',
                        thread_purpose TEXT DEFAULT '',
                        is_active INTEGER DEFAULT 1,
                        message_count INTEGER DEFAULT 0,
                        last_message_at TEXT,
                        context_injected INTEGER DEFAULT 0,
                        created_by TEXT DEFAULT '',
                        created_at TEXT DEFAULT (datetime('now')),
                        updated_at TEXT DEFAULT (datetime('now'))
                    )
                """)
                await cur.execute("CREATE INDEX IF NOT EXISTS idx_ct_case_id ON case_threads(case_id)")
                await conn.commit()
                logger.info("已创建 case_threads 表 (user.db)")

            # api_call_logs（API调用日志；中间件使用 get_db()，写入 user.db）
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='api_call_logs'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS api_call_logs (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        user_id TEXT DEFAULT NULL,
                        endpoint TEXT DEFAULT '',
                        method TEXT DEFAULT '',
                        status_code INTEGER DEFAULT 0,
                        duration_ms INTEGER DEFAULT 0,
                        ip_address TEXT DEFAULT '',
                        user_agent TEXT DEFAULT '',
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await cur.execute("CREATE INDEX IF NOT EXISTS idx_acl_endpoint ON api_call_logs(endpoint)")
                await cur.execute("CREATE INDEX IF NOT EXISTS idx_acl_created ON api_call_logs(created_at)")
                await conn.commit()
                logger.info("已创建 api_call_logs 表 (user.db)")

            # law_enforcement_officers（执法人员列表，V1.6.8+）
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='law_enforcement_officers'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS law_enforcement_officers (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        user_id TEXT NOT NULL,
                        name TEXT NOT NULL,
                        badge_number TEXT NOT NULL,
                        sort_order INTEGER DEFAULT 0,
                        is_active INTEGER DEFAULT 1,
                        created_at TEXT DEFAULT (datetime('now')),
                        updated_at TEXT DEFAULT (datetime('now'))
                    )
                """)
                await cur.execute("CREATE INDEX IF NOT EXISTS idx_officers_user ON law_enforcement_officers(user_id, is_active)")
                await cur.execute("CREATE INDEX IF NOT EXISTS idx_officers_name ON law_enforcement_officers(name)")
                await conn.commit()
                logger.info("已创建 law_enforcement_officers 表 (user.db)")

        except Exception as e:
            logger.error(f"确保 user.db 表存在时出错: {e}")
        break


async def _ensure_runtime_tables():
    """启动时确保 runtime.db 表存在（运行时数据：激活、密码、日志等）"""
    logger.info("检查 runtime.db 表结构...")
    async for conn, cur in get_runtime_db():
        try:
            # system_configs（敏感配置：license_info, trial_used, unlock_password_hash, salt 等）
            await cur.execute(
                """CREATE TABLE IF NOT EXISTS system_configs (
                    config_key TEXT PRIMARY KEY,
                    config_value TEXT,
                    config_type TEXT DEFAULT 'string',
                    category TEXT DEFAULT 'general',
                    description TEXT,
                    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )"""
            )
            await conn.commit()
            logger.info("已创建 system_configs 表 (runtime.db)")

            # knowledge_search_logs
            await cur.execute(
                """CREATE TABLE IF NOT EXISTS knowledge_search_logs (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    query TEXT,
                    results_count INTEGER DEFAULT 0,
                    search_time_ms INTEGER DEFAULT 0,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )"""
            )
            await conn.commit()
            logger.info("已创建 knowledge_search_logs 表 (runtime.db)")

            # knowledge_sync_logs
            await cur.execute(
                """CREATE TABLE IF NOT EXISTS knowledge_sync_logs (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    sync_type TEXT,
                    source_file TEXT,
                    status TEXT,
                    new_nodes_count INTEGER DEFAULT 0,
                    updated_nodes_count INTEGER DEFAULT 0,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )"""
            )
            await conn.commit()
            logger.info("已创建 knowledge_sync_logs 表 (runtime.db)")

            # raw_documents
            await cur.execute(
                """CREATE TABLE IF NOT EXISTS raw_documents (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    title TEXT,
                    file_name TEXT,
                    file_type TEXT,
                    status TEXT DEFAULT 'pending',
                    upload_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    deleted_at TIMESTAMP
                )"""
            )
            await conn.commit()
            logger.info("已创建 raw_documents 表 (runtime.db)")

        except Exception as e:
            logger.error(f"确保 runtime.db 表存在时出错: {e}")
        break


async def _ensure_tables_exist_legacy():
    """启动时确保关键表存在（旧单库兼容模式）"""
    from .core.database import get_db

    async for conn, cur in get_db():
        try:
            # 检查 users 表（单机版用户认证）
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='users'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS users (
                        id TEXT PRIMARY KEY,
                        username TEXT NOT NULL UNIQUE,
                        password_hash TEXT NOT NULL,
                        real_name TEXT DEFAULT '',
                        email TEXT DEFAULT '',
                        phone TEXT DEFAULT '',
                        role TEXT DEFAULT 'user',
                        level TEXT DEFAULT '',
                        parent_id TEXT DEFAULT '',
                        group_id INTEGER DEFAULT NULL,
                        edition TEXT DEFAULT '',
                        department TEXT DEFAULT '',
                        badge_number TEXT DEFAULT '',
                        org_phone TEXT DEFAULT '',
                        org_address TEXT DEFAULT '',
                        unit_contact_phone TEXT DEFAULT '',
                        unit_contact_address TEXT DEFAULT '',
                        is_active INTEGER DEFAULT 1,
                        is_protected INTEGER DEFAULT 0,
                        enabled_menus TEXT DEFAULT '',
                        max_accounts INTEGER DEFAULT NULL,
                        account_expires_at TEXT DEFAULT NULL,
                        is_reviewer INTEGER DEFAULT 0,
                        responsible_areas TEXT DEFAULT '',
                        last_login_at TEXT DEFAULT NULL,
                        login_attempts INTEGER DEFAULT 0,
                        locked_until TEXT DEFAULT NULL,
                        is_deleted TINYINT DEFAULT 0,
                        enforcement_region TEXT DEFAULT '',
                        enforcement_region_path TEXT DEFAULT '',
                        org_name TEXT DEFAULT '',
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await conn.commit()
                logger.info("已创建 users 表")

            # 确保 group_menus 表存在
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='group_menus'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS group_menus (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        group_id INTEGER NOT NULL,
                        menu_code VARCHAR(50) NOT NULL,
                        menu_name VARCHAR(100) NOT NULL,
                        parent_code VARCHAR(50) DEFAULT NULL,
                        icon VARCHAR(50) DEFAULT '',
                        route_path VARCHAR(200) DEFAULT '',
                        sort_order INTEGER DEFAULT 0,
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await conn.commit()
                logger.info("已创建 group_menus 表")

            # 检查 inquiry_strategies 表
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='inquiry_strategies'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS inquiry_strategies (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        case_id VARCHAR(64) NOT NULL,
                        user_id INT NOT NULL,
                        industry_category VARCHAR(50) DEFAULT '',
                        industry_subcategory VARCHAR(100) DEFAULT '',
                        case_type VARCHAR(50) DEFAULT 'general',
                        is_auto_recommended TINYINT DEFAULT 0,
                        case_description TEXT,
                        supplementary_content TEXT,
                        attachment_ids TEXT DEFAULT '[]',
                        strategy_content LONGTEXT,
                        strategy_version INT DEFAULT 1,
                        generation_prompt TEXT,
                        ai_model_used VARCHAR(100) DEFAULT '',
                        generation_time FLOAT DEFAULT 0,
                        token_usage TEXT DEFAULT '{}',
                        strategy_config TEXT DEFAULT '{}',
                        status VARCHAR(20) DEFAULT 'generated',
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_inq_strat_case ON inquiry_strategies(case_id)"
                )
                await conn.commit()
                logger.info("已创建 inquiry_strategies 表")

            # 检查 document_templates 表
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='document_templates'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS document_templates (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        template_code VARCHAR(100) NOT NULL UNIQUE,
                        template_name VARCHAR(200) NOT NULL,
                        template_category VARCHAR(50) NOT NULL,
                        template_type VARCHAR(10) NOT NULL,
                        description TEXT,
                        version VARCHAR(10) DEFAULT '1.0',
                        template_file_path VARCHAR(500) NOT NULL,
                        schema_TEXT TEXT,
                        prompt_content LONGTEXT,
                        docx_preview_url VARCHAR(500),
                        default_values TEXT,
                        validation_rules TEXT,
                        preview_image_path VARCHAR(500),
                        layout_type VARCHAR(20) DEFAULT 'standard',
                        is_system TINYINT DEFAULT 1,
                        is_active TINYINT DEFAULT 1,
                        is_deleted TINYINT DEFAULT 0,
                        usage_count INT DEFAULT 0,
                        created_by INT,
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_tpl_category ON document_templates(template_category)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_tpl_active ON document_templates(is_active, is_deleted)"
                )
                await conn.commit()
                logger.info("已创建 document_templates 表")

            # 检查 case_related_cache 表（关联/类案推荐缓存）
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='case_related_cache'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS case_related_cache (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        case_id VARCHAR(64) NOT NULL UNIQUE,
                        related_cases TEXT DEFAULT '[]',
                        similar_penalty_cases TEXT DEFAULT '[]',
                        match_snapshot TEXT DEFAULT '{}',
                        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_rel_cache_case ON case_related_cache(case_id)"
                )
                await conn.commit()
                logger.info("已创建 case_related_cache 表")

            # 检查 case_causes_detail 表（案件关联的多案由）
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='case_causes_detail'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS case_causes_detail (
                        id VARCHAR(64) PRIMARY KEY,
                        case_id VARCHAR(64) NOT NULL,
                        cause_name VARCHAR(200) NOT NULL,
                        cause_code VARCHAR(20),
                        level1_code VARCHAR(10),
                        level1_name VARCHAR(50),
                        level2_code VARCHAR(10),
                        level2_name VARCHAR(100),
                        cause_source VARCHAR(20) DEFAULT 'user_custom',
                        ai_confidence VARCHAR(10),
                        sort_order INTEGER DEFAULT 0,
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_ccd_case ON case_causes_detail(case_id)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_ccd_case_sort ON case_causes_detail(case_id, sort_order)"
                )
                await conn.commit()
                logger.info("已创建 case_causes_detail 表")

            # 检查 case_cause_laws 表（案由关联的法条）
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='case_cause_laws'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS case_cause_laws (
                        id VARCHAR(64) PRIMARY KEY,
                        cause_detail_id VARCHAR(64),
                        case_id VARCHAR(64) NOT NULL,
                        law_type VARCHAR(20) NOT NULL,
                        law_name VARCHAR(200),
                        article_number VARCHAR(50),
                        article_full_ref VARCHAR(200),
                        content_summary TEXT,
                        content_detail TEXT,
                        source VARCHAR(20) DEFAULT 'manual',
                        source_id VARCHAR(64),
                        sort_order INTEGER DEFAULT 0,
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_ccl_case ON case_cause_laws(case_id)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_ccl_cause_detail ON case_cause_laws(cause_detail_id)"
                )
                await conn.commit()
                logger.info("已创建 case_cause_laws 表")

            # 检查 kb_documents 表（知识库文档数据库化）
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='kb_documents'"
            )
            if not await cur.fetchone():
                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 '',
                        chunk_strategy TEXT DEFAULT 'auto',
                        chunk_count INTEGER DEFAULT 0,
                        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 cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_kb_docs_category ON kb_documents(category)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_kb_docs_region ON kb_documents(region)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_kb_docs_status ON kb_documents(status)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_kb_docs_title ON kb_documents(title)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_kb_docs_file_path ON kb_documents(file_path)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_kb_docs_category_region ON kb_documents(category, region)"
                )
                await conn.commit()
                logger.info("已创建 kb_documents 表")

            # FTS5 全文索引（加速知识库搜索）
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='kb_documents_fts'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE VIRTUAL TABLE IF NOT EXISTS kb_documents_fts USING fts5(
                        title, content, category, region, anyou, file_path,
                        content='kb_documents', content_rowid='id'
                    )
                """)
                # 触发器：INSERT 时同步到 FTS
                await cur.execute("""
                    CREATE TRIGGER IF NOT EXISTS kb_docs_ai AFTER INSERT ON kb_documents BEGIN
                        INSERT INTO kb_documents_fts(rowid, title, content, category, region, anyou, file_path)
                        VALUES (new.id, new.title, new.content, new.category, new.region, new.anyou, new.file_path);
                    END
                """)
                # 触发器：DELETE 时同步删除
                await cur.execute("""
                    CREATE TRIGGER IF NOT EXISTS kb_docs_ad AFTER DELETE ON kb_documents BEGIN
                        INSERT INTO kb_documents_fts(kb_documents_fts, rowid, title, content, category, region, anyou, file_path)
                        VALUES ('delete', old.id, old.title, old.content, old.category, old.region, old.anyou, old.file_path);
                    END
                """)
                # 触发器：UPDATE 时同步更新
                await cur.execute("""
                    CREATE TRIGGER IF NOT EXISTS kb_docs_au AFTER UPDATE ON kb_documents BEGIN
                        INSERT INTO kb_documents_fts(kb_documents_fts, rowid, title, content, category, region, anyou, file_path)
                        VALUES ('delete', old.id, old.title, old.content, old.category, old.region, old.anyou, old.file_path);
                        INSERT INTO kb_documents_fts(rowid, title, content, category, region, anyou, file_path)
                        VALUES (new.id, new.title, new.content, new.category, new.region, new.anyou, new.file_path);
                    END
                """)
                # 初始化：将已有数据写入 FTS
                await cur.execute("""
                    INSERT INTO kb_documents_fts(rowid, title, content, category, region, anyou, file_path)
                    SELECT id, title, content, category, region, anyou, file_path FROM kb_documents
                """)
                await conn.commit()
                logger.info("已创建 FTS5 全文索引 kb_documents_fts")

            # 检查 knowledge_view_history 表（文档浏览历史）
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='knowledge_view_history'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS knowledge_view_history (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        user_id TEXT NOT NULL DEFAULT '1',
                        point_id TEXT NOT NULL,
                        collection TEXT DEFAULT 'markdown_docs',
                        title TEXT DEFAULT '',
                        doc_path TEXT DEFAULT '',
                        view_count INTEGER DEFAULT 1,
                        viewed_at TEXT DEFAULT (datetime('now')),
                        UNIQUE(user_id, point_id)
                    )
                """)
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_kvhist_user ON knowledge_view_history(user_id)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_kvhist_viewed ON knowledge_view_history(viewed_at)"
                )
                await conn.commit()
                logger.info("已创建 knowledge_view_history 表")

            # ── 电子取证清单相关表 ──

            # evidence_rules: 基础取证规则库
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='evidence_rules'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS evidence_rules (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        domain VARCHAR(100) NOT NULL DEFAULT '',
                        case_type VARCHAR(200) NOT NULL DEFAULT '',
                        target_objects TEXT DEFAULT '',
                        key_data TEXT DEFAULT '',
                        compliance_req TEXT DEFAULT '',
                        is_deleted INTEGER DEFAULT 0,
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_ev_rules_domain ON evidence_rules(domain)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_ev_rules_case_type ON evidence_rules(case_type)"
                )
                await conn.commit()
                logger.info("已创建 evidence_rules 表")

            # evidence_checklists: 用户取证清单
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='evidence_checklists'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS evidence_checklists (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        user_id VARCHAR(36) NOT NULL DEFAULT '1',
                        title VARCHAR(200) NOT NULL DEFAULT '',
                        domain VARCHAR(100) DEFAULT '',
                        case_type VARCHAR(200) DEFAULT '',
                        status VARCHAR(20) DEFAULT 'draft',
                        notes TEXT DEFAULT '',
                        is_deleted INTEGER DEFAULT 0,
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_ev_cl_user ON evidence_checklists(user_id)"
                )
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_ev_cl_status ON evidence_checklists(status)"
                )
                await conn.commit()
                logger.info("已创建 evidence_checklists 表")

            # evidence_checklist_items: 清单明细
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='evidence_checklist_items'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS evidence_checklist_items (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        checklist_id INTEGER NOT NULL,
                        rule_id INTEGER DEFAULT NULL,
                        item_type VARCHAR(20) NOT NULL DEFAULT 'custom',
                        content TEXT NOT NULL DEFAULT '',
                        sort_order INTEGER DEFAULT 0,
                        is_checked INTEGER DEFAULT 0,
                        user_note TEXT DEFAULT '',
                        is_deleted INTEGER DEFAULT 0,
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
                        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await cur.execute(
                    "CREATE INDEX IF NOT EXISTS idx_ev_items_cl ON evidence_checklist_items(checklist_id)"
                )
                await conn.commit()
                logger.info("已创建 evidence_checklist_items 表")

            # evidence_domains: 领域枚举
            await cur.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='evidence_domains'"
            )
            if not await cur.fetchone():
                await cur.execute("""
                    CREATE TABLE IF NOT EXISTS evidence_domains (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        domain VARCHAR(100) NOT NULL DEFAULT '',
                        case_type VARCHAR(200) NOT NULL DEFAULT '',
                        rule_count INTEGER DEFAULT 0,
                        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
                    )
                """)
                await cur.execute(
                    "CREATE UNIQUE INDEX IF NOT EXISTS idx_ev_domains_unique ON evidence_domains(domain, case_type)"
                )
                await conn.commit()
                logger.info("已创建 evidence_domains 表")

            # 自动导入 CSV 数据（仅在 evidence_rules 为空时）
            await cur.execute("SELECT COUNT(*) FROM evidence_rules")
            count_row = await cur.fetchone()
            if count_row and count_row[0] == 0:
                await _import_evidence_csv(conn, cur)

        except Exception as e:
            logger.error(f"确保表存在时出错: {e}")
        break


async def _import_evidence_csv(conn, cur):
    """从电子取证清单.csv导入基础规则数据"""
    import csv

    csv_paths = [
        os.path.join(os.getcwd(), "单机版", "电子取证清单.csv"),
        os.path.join(os.path.dirname(__file__), "..", "..", "单机版", "电子取证清单.csv"),
        os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "单机版", "电子取证清单.csv"),
        os.path.join(paths.get_exe_dir(), "电子取证清单.csv"),
    ]

    csv_path = None
    for p in csv_paths:
        if os.path.exists(p):
            csv_path = p
            break

    if not csv_path:
        logger.warning("电子取证清单.csv 未找到，跳过导入")
        return

    try:
        with open(csv_path, "r", encoding="utf-8") as f:
            reader = csv.reader(f)
            header = next(reader)  # 跳过表头
            rows = list(reader)

        if not rows:
            logger.info("电子取证清单.csv 为空，跳过导入")
            return

        imported = 0
        for row in rows:
            if len(row) < 5:
                continue
            domain, case_type, target_objects, key_data, compliance_req = (
                row[0].strip(), row[1].strip(), row[2].strip(), row[3].strip(), row[4].strip()
            )
            if not domain or not case_type:
                continue
            await cur.execute(
                """INSERT INTO evidence_rules (domain, case_type, target_objects, key_data, compliance_req)
                   VALUES (?, ?, ?, ?, ?)""",
                (domain, case_type, target_objects, key_data, compliance_req),
            )
            imported += 1

        # 聚合生成 evidence_domains
        await cur.execute("DELETE FROM evidence_domains")
        await cur.execute("""
            INSERT INTO evidence_domains (domain, case_type, rule_count)
            SELECT domain, case_type, COUNT(*) as cnt
            FROM evidence_rules
            WHERE is_deleted = 0
            GROUP BY domain, case_type
        """)

        await conn.commit()
        logger.info(f"电子取证清单 CSV 导入完成：{imported} 条规则")
    except Exception as e:
        logger.error(f"电子取证清单 CSV 导入失败: {e}")


async def _cleanup_kb_documents():
    """清理 kb_documents 表：去重 + 数据库内部修复。
    注：v2.0 起 kb_documents 位于 system.db（拆分模式）或 zfzs_standalone.db（单库兼容）。
    """
    from .core.database import get_db as _get_db_fallback, get_system_db as _get_system_db, _has_split_databases

    print("[KB-Cleanup] 开始清理 kb_documents 表")

    db_gen = _get_system_db() if _has_split_databases() else _get_db_fallback()
    async for conn, cur in db_gen:
        try:
            # Step 0: 清理反斜杠路径（Windows 历史遗留，统一为正斜杠）
            await cur.execute("DELETE FROM kb_documents WHERE file_path LIKE '%\\%'")
            bs_deleted = cur.rowcount
            if bs_deleted > 0:
                print(f"[KB-Cleanup] 清理反斜杠路径记录 {bs_deleted} 条")
            await conn.commit()

            # Step 1: 去重 — 按 file_path 保留最后一条（id 最大的）
            await cur.execute("""
                DELETE FROM kb_documents WHERE id NOT IN (
                    SELECT MAX(id) FROM kb_documents GROUP BY file_path
                )
            """)
            deleted = cur.rowcount
            if deleted > 0:
                print(f"[KB-Cleanup] 去重完成，删除 {deleted} 条重复记录")
            await conn.commit()

            # Step 1b: 添加唯一索引，防止未来再次出现重复
            await cur.execute("CREATE UNIQUE INDEX IF NOT EXISTS uk_kb_docs_file_path ON kb_documents(file_path)")
            await conn.commit()

            # Step 2: 统计最终结果
            await cur.execute("SELECT category, COUNT(*) as cnt FROM kb_documents GROUP BY category")
            final_stats = {row['category']: row['cnt'] for row in await cur.fetchall()}

            total = sum(final_stats.values())
            print(f"[KB-Cleanup] 完成: 去重 {deleted} 条, 最终 {total} 条")
            for cat, cnt in sorted(final_stats.items()):
                print(f"  {cat}: {cnt}")
        except Exception as e:
            print(f"[KB-Cleanup] 清理失败: {e}")
            import traceback
            traceback.print_exc()
        break


@app.on_event("startup")
async def startup_init():
    """启动时初始化——数据库连接池 + 建表（阻塞直到就绪，避免前端请求 500）"""
    global _backend_ready
    try:
        # 1. 确保数据库文件存在
        from .core.database import _resolve_system_db_path, _resolve_user_db_path, _resolve_runtime_db_path
        for db_label, db_path in [
            ("system.db", _resolve_system_db_path()),
            ("user.db", _resolve_user_db_path()),
            ("runtime.db", _resolve_runtime_db_path()),
        ]:
            logger.info(f"检查 {db_label}: {db_path} (存在={os.path.exists(db_path)})")
            if not os.path.exists(db_path):
                os.makedirs(os.path.dirname(db_path), exist_ok=True)
                import sqlite3 as _sqlite3
                conn = _sqlite3.connect(db_path)
                conn.close()
                logger.info(f"首次启动：已创建空 {db_label} ({db_path})")
            # 确保数据库文件可写（打包解压后可能被设为只读）
            if os.path.exists(db_path):
                import stat
                try:
                    os.chmod(db_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH)
                except Exception:
                    pass

        # 2. 预热数据库连接池（必须在接收请求前完成）
        from .core.database import get_system_pool, get_user_pool, get_runtime_pool
        import app.core.database as db_module
        if _has_split_databases():
            for db_name, get_pool_fn, pool_attr in [
                ("system", get_system_pool, "_system_pool"),
                ("user", get_user_pool, "_user_pool"),
                ("runtime", get_runtime_pool, "_runtime_pool"),
            ]:
                try:
                    await get_pool_fn()
                    logger.info(f"{db_name}.db 连接池预热完成")
                except Exception as e:
                    logger.warning(f"{db_name}.db 连接池初始化失败（非致命）: {e}")
                    setattr(db_module, pool_attr, None)
        else:
            from .core.database import get_pool
            await get_pool()
            logger.info("数据库连接池预热完成")

        # 3. 确保关键表存在
        await _ensure_tables_exist()

        _backend_ready = True
        logger.info("后端初始化完成，开始接收请求")

    except Exception as e:
        logger.error(f"启动初始化失败: {e}", exc_info=True)
        _backend_ready = True  # 即使失败也标记就绪，避免永远阻塞

    # 4. 非关键任务放到后台（不影响前端请求）
    asyncio.get_event_loop().create_task(_background_init())


_backend_ready = False


async def _background_init():
    """后台非关键任务（模板同步、缓存清理等，不影响前端请求）"""
    try:
        # 1. 并行执行数据初始化任务（互不依赖）
        async def _init_discretion():
            from .services.discretion_rule_service import ensure_table_and_data
            if _has_split_databases():
                async for conn, cur in get_system_db():
                    await ensure_table_and_data(conn, cur)
                    break
            else:
                from .core.database import get_db as _get_db_discretion
                async for conn, cur in _get_db_discretion():
                    await ensure_table_and_data(conn, cur)
                    break

        async def _clear_cache():
            if _has_split_databases():
                async for conn, cur in get_user_db():
                    await cur.execute("DELETE FROM case_related_cache")
                    await conn.commit()
                    break
            else:
                from .core.database import get_db as _get_db_cache
                async for conn, cur in _get_db_cache():
                    await cur.execute("DELETE FROM case_related_cache")
                    await conn.commit()
                    break

        async def _backfill_kb_content():
            """补录 kb_documents 中 content 为空的记录（从文件读取）"""
            if not _has_split_databases():
                return
            from .core.crypto import read_text_file
            zhishiku_base = paths.resolve_resource_path("data/zhishiku")
            if not os.path.isdir(zhishiku_base):
                return
            async for conn, cur in get_system_db():
                await cur.execute(
                    "SELECT id, file_path FROM kb_documents WHERE (content IS NULL OR content = '') AND file_path IS NOT NULL AND file_path != ''"
                )
                rows = await cur.fetchall()
                if not rows:
                    return
                filled = 0
                for row in rows:
                    r = dict(row) if not isinstance(row, dict) else row
                    fp = r.get("file_path", "")
                    if not fp:
                        continue
                    full_path = os.path.join(zhishiku_base, fp)
                    if not os.path.exists(full_path):
                        # 尝试 .enc 版本
                        full_path_enc = full_path + ".enc"
                        if os.path.exists(full_path_enc):
                            full_path = full_path_enc
                        else:
                            continue
                    try:
                        content = read_text_file(full_path)
                        if content and content.strip():
                            await cur.execute(
                                "UPDATE kb_documents SET content = %s, import_status = 'done' WHERE id = %s",
                                (content, r["id"])
                            )
                            filled += 1
                    except Exception:
                        pass
                if filled:
                    await conn.commit()
                logger.info(f"知识库内容补录: {filled}/{len(rows)} 条")
                break

        tasks = [
            ("文书模板同步", _sync_document_templates()),
            ("裁量规则初始化", _init_discretion()),
            ("清空推荐缓存", _clear_cache()),
            ("知识库文档清理", _cleanup_kb_documents()),
            ("知识库内容补录", _backfill_kb_content()),
        ]
        results = await asyncio.gather(
            *[coro for _, coro in tasks],
            return_exceptions=True,
        )
        for (name, _), result in zip(tasks, results):
            if isinstance(result, Exception):
                logger.warning(f"{name}失败（非致命）: {result}")

        logger.info("后台初始化全部完成")
    except Exception as e:
        logger.error(f"后台初始化异常: {e}", exc_info=True)


@app.get("/health")
async def health_check():
    return {"status": "ok", "service": "bff-api"}


@app.get("/ready")
async def ready_check():
    """后端完全就绪（数据库初始化完成）后返回 200，否则 503"""
    if _backend_ready:
        return {"ready": True}
    from starlette.responses import JSONResponse
    return JSONResponse({"ready": False}, status_code=503)


# ── 授权与秘钥路由（单机版 v1.2.0）──
try:
    from .api.license import router as license_router
    app.include_router(license_router, prefix="/api", tags=["License"])
    logger.info("License 路由注册成功")
except Exception as e:
    logger.error(f"License 路由加载失败: {e}")


# ── 授权检查中间件：试用到期/未激活时限制 API 访问 ──
_LICENSE_ALLOWED_PREFIXES = ("/api/license", "/health", "/docs", "/redoc", "/openapi.json",
                             "/api/user/status", "/api/user/login", "/api/user/register",
                             "/api/user/reset", "/api/user/logout")

class LicenseCheckMiddleware(BaseHTTPMiddleware):
    """授权检查中间件：试用到期/未激活时，GET 放行（只读），写操作返回 403"""

    # 只读模式下允许写入的路径（认证、授权相关）
    _READONLY_ALLOWED_WRITE = ("/api/user/login", "/api/user/register", "/api/user/reset",
                               "/api/user/logout", "/api/user/status", "/api/license")

    async def dispatch(self, request: Request, call_next):
        path = request.url.path

        # 允许的路径直接放行
        if any(path.startswith(p) for p in _LICENSE_ALLOWED_PREFIXES):
            return await call_next(request)

        # 非 API 请求（前端静态文件）放行
        if not path.startswith("/api/"):
            return await call_next(request)

        # 检查授权状态（仅在非开发模式下生效）
        if not settings.MOCK_LOGIN:
            try:
                from .core.license import check_trial_status
                status = await check_trial_status()
                if not status.get("can_use", False):
                    reason = status.get("reason", "unknown")

                    # 未激活 → 返回 403（前端跳转激活页）
                    if reason == "not_activated":
                        return JSONResponse(
                            {
                                "detail": status.get("message", "系统尚未激活"),
                                "reason": reason,
                                "code": "LICENSE_REQUIRED",
                            },
                            status_code=403,
                        )

                    # 已过期 → 只读模式：GET 放行，写操作拒绝
                    if request.method not in ("GET", "OPTIONS", "HEAD"):
                        # 登录/注册/授权相关接口仍允许写入
                        if not any(path.startswith(p) for p in self._READONLY_ALLOWED_WRITE):
                            return JSONResponse(
                                {
                                    "detail": "产品授权已过期，当前为只读模式。请联系客服续费。",
                                    "reason": reason,
                                    "code": "READONLY_MODE",
                                },
                                status_code=403,
                            )
            except Exception:
                pass  # 授权检查异常时不阻断（降级放行）

        return await call_next(request)


app.add_middleware(LicenseCheckMiddleware)


# 挂载文件目录（可写，EXE 模式下在 EXE 目录下）
_writable_root = paths.get_exe_dir()

# 挂载测试文件目录
files_dir = os.path.join(_writable_root, "files")
if os.path.exists(files_dir):
    app.mount("/files", StaticFiles(directory=files_dir), name="files")

# 挂载上传文件目录
uploads_dir = os.path.join(_writable_root, "uploads")
if os.path.exists(uploads_dir):
    app.mount("/uploads", StaticFiles(directory=uploads_dir), name="uploads")
else:
    os.makedirs(uploads_dir, exist_ok=True)
    app.mount("/uploads", StaticFiles(directory=uploads_dir), name="uploads")

# 挂载知识库目录 (zhishiku) — 可写，用户可增删
zhishiku_path = getattr(settings, 'ZHISHIKU_PATH', None) or os.path.join(_writable_root, "data", "zhishiku")
if not os.path.isabs(zhishiku_path):
    zhishiku_path = os.path.join(_writable_root, zhishiku_path)
if os.path.exists(zhishiku_path):
    app.mount("/zhishiku", StaticFiles(directory=zhishiku_path), name="zhishiku")


# ── Admin routers（单机版：仅保留配置类路由，权限已开放给所有用户）──
# 以下6个纯管理功能已注销（用户端有对应路由或单机版不需要）：
#   auth(用户端登录替代), cases/causes/threads(用户端已有), audit_logs(不需要)
try:
    from .api.admin import ai_management as admin_ai_management
    from .api.admin import feature_flags as admin_feature_flags
    # from .api.admin import system_config as admin_system_config  # 简版移除：文件已删除
    from .api.admin import knowledge as admin_knowledge
    from .api.admin import knowledge_base as admin_knowledge_base
    from .api.admin import knowledge_lib as admin_knowledge_lib
    from .api.admin import kb_md_documents as admin_kb_md_documents
    from .api.admin import document_templates as admin_document_templates
    from .api.admin import welcome_message as admin_welcome_message
    from .api.admin import quick_prompts as admin_quick_prompts
    from .api.admin import system_update as admin_system_update
    from .api.admin import user_backup as admin_user_backup
    from .api.admin import password as admin_password

    # app.include_router(admin_ai_management.router, prefix="/api/admin/ai-management", tags=["Admin-AI-Management"])  # 简版移除
    # app.include_router(admin_feature_flags.router, prefix="/api/admin/feature-flags", tags=["Admin-Feature-Flags"])  # 简版移除
    # app.include_router(admin_system_config.router, prefix="/api/admin/system-config", tags=["Admin-System-Config"])  # 简版移除
    app.include_router(admin_knowledge.router, prefix="/api/admin/knowledge", tags=["Admin-Knowledge"])
    app.include_router(admin_knowledge_base.router, prefix="/api/admin/knowledge-base", tags=["Admin-Knowledge-Base"])
    app.include_router(admin_knowledge_lib.router, prefix="/api/admin", tags=["Admin-KnowledgeLib"])
    app.include_router(admin_kb_md_documents.router, prefix="/api/admin", tags=["Admin-KbMdDocuments"])
    app.include_router(admin_document_templates.router, prefix="/api/admin/document-templates", tags=["Admin-Document-Templates"])
    app.include_router(admin_welcome_message.router, prefix="/api/admin/welcome-message", tags=["Admin-Welcome-Message"])
    app.include_router(admin_system_update.router, prefix="/api/admin", tags=["Admin-SystemUpdate"])
    app.include_router(admin_user_backup.router, prefix="/api/admin", tags=["Admin-UserBackup"])
    app.include_router(admin_password.router, tags=["Admin-Password"])
    # app.include_router(admin_quick_prompts.router, prefix="/api/admin/quick-prompts", tags=["Admin-QuickPrompts"])  # 简版移除

    from .api.admin import config_paths as admin_config_paths
    app.include_router(admin_config_paths.router, prefix="/api/admin", tags=["Admin-ConfigPaths"])
except Exception as e:
    logger.error(f"Admin routers 加载失败: {e}")


# ── 知识库解封接口 ──
@app.post("/api/admin/unlock-knowledge-base")
async def unlock_knowledge_base(request: Request):
    """解封知识库（需要管理员密码验证）"""
    try:
        body = await request.json()
    except Exception:
        return JSONResponse({"detail": "请求格式错误"}, status_code=400)

    admin_password = body.get("admin_password", "")
    if not admin_password:
        return JSONResponse({"detail": "请输入管理员密码"}, status_code=400)

    # 验证用户密码（单机版：任意活跃用户均可解封）
    try:
        from .api.user.auth import _verify_password
        from .core.database import get_db
        async for conn, cur in get_db():
            await cur.execute("SELECT password_hash FROM users WHERE is_active=1 LIMIT 1")
            row = await cur.fetchone()
            if not row:
                return JSONResponse({"detail": "未找到用户账户"}, status_code=404)
            stored_hash = row["password_hash"] if isinstance(row, dict) else row[0]
            if not _verify_password(admin_password, stored_hash):
                return JSONResponse({"detail": "密码错误"}, status_code=403)
            break
    except Exception as e:
        logger.error(f"验证管理员密码失败: {e}")
        return JSONResponse({"detail": "验证失败，请重试"}, status_code=500)

    # 解封
    if not _is_kb_locked():
        return JSONResponse({"detail": "知识库未被锁定，无需解封"}, status_code=200)

    _unlock_knowledge_base()
    logger.info("[解封] 知识库已由管理员解封")
    return JSONResponse({"detail": "知识库已成功解封"}, status_code=200)


# ── User routers ──
try:
    from .api.user import auth as user_auth
    # from .api.user import threads as user_threads  # 简版移除：文件已删除
    from .api.user import cases as user_cases
    from .api.user import causes as user_causes
    from .api.user import knowledge as user_knowledge
    from .api.user import knowledge_enhanced as user_knowledge_enhanced
    # from .api.user import knowledge_chat as user_knowledge_chat  # 简版移除：文件已删除
    from .api.user import knowledge_base as user_knowledge_base
    from .api.user import md_search as user_md_search
    from .api.user import knowledge_favorites as user_knowledge_favorites
    from .api.user import document_generation as user_doc_gen
    from .api.user import document_templates as user_document_templates
    from .api.user import agents as user_agents
    from .api.user import memories as user_memories
    from .api.user import inquiry_strategy as user_inquiry_strategy
    from .api.user import discretion_rules as user_discretion_rules
    from .api.user import evidence as user_evidence
    from .api.user import welcome_message as user_welcome_message
    from .api.user import quick_prompts as user_quick_prompts
    from .api.user import features as user_features
    from .api.user import system_config as user_system_config
    from .api.user import chat_capabilities as user_chat_capabilities
    from .api.user import officers as user_officers
    from .api.user import ai_config as user_ai_config

    app.include_router(user_auth.router, prefix="/api/user/auth", tags=["User-Auth"])
    # 兼容前端 /api/user/profile 路径
    app.include_router(user_auth.router, prefix="/api/user", tags=["User-Profile"])
    # app.include_router(user_threads.router, prefix="/api/user/threads", tags=["User-Threads"])  # 简版移除：案件会话
    app.include_router(user_cases.router, prefix="/api/user/cases", tags=["User-Cases"])
    app.include_router(user_causes.router, prefix="/api/user/causes", tags=["User-Causes"])
    app.include_router(user_knowledge.router, prefix="/api/user/knowledge", tags=["User-Knowledge"])
    app.include_router(user_knowledge_enhanced.router, prefix="/api/user/knowledge/enhanced", tags=["User-Knowledge-Enhanced"])
    # app.include_router(user_knowledge_chat.router, prefix="/api/user/knowledge/chat", tags=["User-Knowledge-Chat"])  # 简版移除：知识库AI对话
    app.include_router(user_knowledge_base.router, prefix="/api/user/knowledge-base", tags=["User-Knowledge-Base"])
    app.include_router(user_md_search.router, prefix="/api/user/knowledge", tags=["User-MdSearch"])
    app.include_router(user_knowledge_favorites.router, prefix="/api/user/knowledge", tags=["User-Knowledge-Favorites"])
    app.include_router(user_doc_gen.router, prefix="/api/user/doc-gen", tags=["User-DocumentGeneration"])
    app.include_router(user_document_templates.router, prefix="/api/user/document-templates", tags=["User-Document-Templates"])
    app.include_router(user_officers.router, prefix="/api/user", tags=["User-Officers"])
    # app.include_router(user_agents.router, prefix="/api/user/agents", tags=["User-Agents"])  # 简版移除：AI Agent
    # app.include_router(user_memories.router, prefix="/api/user/memories", tags=["User-Memories"])  # 简版移除：记忆管理
    app.include_router(user_inquiry_strategy.router, prefix="/api/user", tags=["User-InquiryStrategy"])
    app.include_router(user_discretion_rules.router, prefix="/api/user", tags=["User-DiscretionRules"])
    app.include_router(user_evidence.router, prefix="/api/user/evidence", tags=["User-Evidence"])
    # app.include_router(user_welcome_message.router, prefix="/api/user/welcome-message", tags=["User-Welcome-Message"])  # 简版移除
    # app.include_router(user_quick_prompts.router, prefix="/api/user/quick-prompts", tags=["User-QuickPrompts"])  # 简版移除
    app.include_router(user_features.router, prefix="/api/user/features", tags=["User-Features"])
    # app.include_router(user_chat_capabilities.router, prefix="/api/user/chat/capabilities", tags=["User-Chat-Capabilities"])  # 简版移除：对话能力
    # app.include_router(user_ai_config.router, prefix="/api/user/ai", tags=["User-AI-Config"])  # 简版移除：AI配置
    # app.include_router(user_system_config.router, prefix="/api/user/system-config", tags=["User-System-Config"])  # 简版移除：系统配置
except Exception as e:
    logger.error(f"User routers 加载失败: {e}")


# ── 单机版：挂载前端静态文件（SPA 模式）──
# 开发模式：项目根目录/web/dist
# EXE 模式：_MEIPASS/web/dist
# 使用 catch-all 路由替代 app.mount("/", StaticFiles(...))，兼容性更好
_frontend_dir = paths.resolve_resource_path("web/dist")
# 开发模式兼容：web/dist 在项目根目录（bff-api 的上一级），不在 bff-api/ 下
if not os.path.exists(_frontend_dir) and not paths.is_frozen():
    _project_root = os.path.dirname(paths.get_resource_dir())
    _candidate = os.path.join(_project_root, "web", "dist")
    if os.path.exists(_candidate):
        _frontend_dir = _candidate
# EXE 模式兼容：web/dist 直接在 EXE 目录下（不在 bff-api/ 内）
if not os.path.exists(_frontend_dir) and paths.is_frozen():
    _candidate = os.path.join(paths.get_exe_dir(), "web", "dist")
    if os.path.exists(_candidate):
        _frontend_dir = _candidate
logger.info(f"前端静态目录: {_frontend_dir}, 存在: {os.path.exists(_frontend_dir)}")

if os.path.exists(_frontend_dir):
    from starlette.responses import FileResponse
    from starlette.middleware.base import BaseHTTPMiddleware as _BaseHTTPMiddleware
    logger.info(f"✅ 前端 SPA 中间件已注册，目录: {_frontend_dir}")

    # SPA 中间件需要放行的路径前缀（后端路由，不走前端 fallback）
    _SPA_PASSTHROUGH = ("/api/", "/health", "/ready", "/docs", "/redoc", "/openapi.json",
                        "/files/", "/uploads/", "/zhishiku/")

    _LOADING_HTML = """<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>综合执法智能辅助系统</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body { font-family: "Microsoft YaHei", sans-serif; background: #f5f7fa; display: flex; justify-content: center; align-items: center; height: 100vh; }
  .box { text-align: center; }
  .title { font-size: 22px; font-weight: bold; color: #1a5276; margin-bottom: 24px; }
  .spinner { width: 40px; height: 40px; border: 4px solid #e0e0e0; border-top-color: #1a5276; border-radius: 50%; animation: spin 0.8s linear infinite; margin: 0 auto 16px; }
  @keyframes spin { to { transform: rotate(360deg); } }
  .msg { color: #666; font-size: 14px; }
</style>
</head>
<body>
<div class="box">
  <div class="title">综合执法智能辅助系统</div>
  <div class="spinner"></div>
  <div class="msg">正在初始化服务，请稍候...</div>
</div>
<script>
(function poll() {
  fetch('/ready').then(function(r) {
    if (r.ok) { location.reload(); }
    else { setTimeout(poll, 1500); }
  }).catch(function() { setTimeout(poll, 2000); });
})();
</script>
</body>
</html>"""

    class SPAMiddleware(_BaseHTTPMiddleware):
        """SPA 中间件：后端路由放行，始终返回前端静态文件（前端自行处理加载状态）"""
        async def dispatch(self, request, call_next):
            # 后端路由直接放行
            if any(request.url.path.startswith(p) for p in _SPA_PASSTHROUGH):
                return await call_next(request)
            # 非 GET 请求放行
            if request.method != "GET":
                return await call_next(request)
            # 尝试返回静态文件
            path = request.url.path.lstrip("/")
            if path:
                file_path = os.path.normpath(os.path.join(_frontend_dir, path))
                if file_path.startswith(os.path.normpath(_frontend_dir)) and os.path.isfile(file_path):
                    return FileResponse(file_path)
            # SPA fallback：返回 index.html
            index_path = os.path.join(_frontend_dir, "index.html")
            if os.path.exists(index_path):
                return FileResponse(index_path, media_type="text/html")
            return JSONResponse({"detail": "Frontend not built"}, status_code=404)

    app.add_middleware(SPAMiddleware)
else:
    logger.warning(f"前端目录不存在: {_frontend_dir}")
# force reload
