import uuid
import json
import os
import io
import time
import zipfile
import datetime
import logging
import httpx
import aiohttp
import re
import base64
from fastapi import APIRouter, Depends, Query, Request, UploadFile, File, Form
from fastapi.responses import StreamingResponse, FileResponse
from fastapi import HTTPException
from pydantic import BaseModel
from typing import Optional, List
from ...core.database import get_db, get_db_write, get_system_db, SQLHelper, _has_split_databases
from ...core.permission import require_login
from ...core.config import settings
# from ...services.deerflow_service import get_deerflow_service, DeerFlowService  # 简版移除：AI对话
from ...services.info_pool_service import info_pool_service
# case_memory_service removed — memory endpoints return stub responses
from ...utils.desensitization import desensitize_case
from ...utils.request_utils import safe_json
# from .threads import _check_sensitive_words  # 简版移除：敏感词检查（AI对话）


def _clean_content_detail(text: str) -> str:
    """去掉法条内容中的 Markdown 标题前缀（### 、## 等）"""
    if not text:
        return text
    return re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE)
# OnlyOffice 并发会话管理（原 my_documents 模块已移除，内联定义）
import time as _time
MAX_CONCURRENT_EDITS = 20
EDITOR_IDLE_TIMEOUT = 1800  # 30 minutes
_active_edit_sessions: dict = {}

def _get_session_key(doc_id: str, user_id: str) -> str:
    return f"{doc_id}:{user_id}"

def _get_active_edit_count() -> int:
    now = _time.time()
    expired = [k for k, v in _active_edit_sessions.items() if now - v >= EDITOR_IDLE_TIMEOUT]
    for k in expired:
        del _active_edit_sessions[k]
    return len(_active_edit_sessions)

def _record_edit_session(doc_id: str, user_id: str):
    _active_edit_sessions[_get_session_key(doc_id, user_id)] = _time.time()

def _clear_edit_session(doc_id: str, user_id: str):
    _active_edit_sessions.pop(_get_session_key(doc_id, user_id), None)

logger = logging.getLogger(__name__)

router = APIRouter()


async def get_system_db_dep():
    """依赖注入：system.db（知识库处罚案例等只读参考数据）"""
    async for conn, cur in (get_system_db() if _has_split_databases() else get_db()):
        yield conn, cur


VALID_SOURCES = {"complaint", "inspection", "transfer", "report", "other"}
VALID_CASE_TYPES = {
    "food_drug", "advertising", "price", "product_quality",
    "intellectual_property", "other",
}
VALID_PARTY_TYPES = {"individual", "enterprise", "individual_business", "other"}
VALID_ID_TYPES = {"id_card", "business_license", "other"}
VALID_PRIORITIES = {"normal", "urgent", "major"}
VALID_STATUSES = {"draft", "pending", "in_progress", "completed", "closed"}
VALID_VIEWS = {"mine", "involved"}

CASE_CREATE_REQUIRED = {"title", "cause", "source", "filing_date",
                        "party_name", "party_type", "party_id_type",
                        "main_officer"}

CASE_EDITABLE_FIELDS = {
    "case_number", "title", "cause", "source", "filing_date", "priority",
    "party_name", "party_type", "credit_code", "legal_representative",
    "party_id_type", "party_id_number", "phone", "address", "party_info",
    "illegal_fact_summary", "involved_amount",
    "main_officer", "assistant_officer", "remark", "status",
    "description", "legal_basis", "discretion_advice",
}

CASE_DESENSITIZE_FIELDS = {"party_id_number", "phone", "credit_code"}


async def _check_case_access(case_id: str, user_id: str, db, permission: str = "read") -> dict:
    conn, cur = db
    await cur.execute("SELECT * FROM cases WHERE id = %s AND is_deleted = 0", (case_id,))
    case = await cur.fetchone()
    if not case:
        return None

    case_dict = dict(case)
    created_by = str(case_dict.get("created_by", ""))

    is_creator = created_by == str(user_id)

    is_officer = False
    for officer_field in ("main_officer", "assistant_officer"):
        officer_data = case_dict.get(officer_field)
        if officer_data:
            if isinstance(officer_data, str):
                try:
                    officer_data = json.loads(officer_data)
                except (json.JSONDecodeError, TypeError):
                    officer_data = None
            if isinstance(officer_data, dict):
                officer_user_id = officer_data.get("user_id")
                if officer_user_id and str(officer_user_id) == str(user_id):
                    is_officer = True
                    break

    await cur.execute("SELECT role FROM users WHERE id = %s", (user_id,))
    user_row = await cur.fetchone()
    is_admin = user_row and dict(user_row).get("role") in ("system_admin", "admin", "org_admin")

    has_access = is_creator or is_officer or is_admin

    if not has_access:
        return None

    if permission == "delete" and not is_admin:
        return None

    return case_dict


def _build_permissions(case_data: dict, user_id: str, user_role: str) -> dict:
    is_creator = str(case_data.get("created_by", "")) == str(user_id)

    is_officer = False
    for officer_field in ("main_officer", "assistant_officer"):
        officer_data = case_data.get(officer_field)
        if officer_data:
            if isinstance(officer_data, str):
                try:
                    officer_data = json.loads(officer_data)
                except (json.JSONDecodeError, TypeError):
                    officer_data = None
            if isinstance(officer_data, dict):
                if str(officer_data.get("user_id", "")) == str(user_id):
                    is_officer = True
                    break

    is_admin = user_role in ("system_admin", "admin", "org_admin")
    has_access = is_creator or is_officer or is_admin

    return {
        "can_edit": has_access,
        "can_delete": is_admin,
        "can_chat": has_access,
        "can_create_document": has_access,
        "can_transfer": has_access,
        "is_admin": is_admin,
    }


async def _log_event(case_id: str, event_type: str, operator_id: str, db,
                     event_data: dict = None, resource_id: str = None,
                     resource_type: str = None, operator_name: str = ""):
    import uuid as _uuid
    conn, cur = db
    event_data_json = json.dumps(event_data, ensure_ascii=False, default=str) if event_data else None
    await cur.execute(
        "INSERT INTO case_logs (id, case_id, event_type, event_data, operator_id, operator_name, "
        "resource_id, resource_type, created_at) "
        "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, NOW())",
        (str(_uuid.uuid4()), case_id, event_type, event_data_json, operator_id, operator_name,
         resource_id, resource_type),
    )


def _decrypt_key(enc_key: str) -> str:
    """统一使用 config.py 的解密方法"""
    from ...core.config import _decrypt_value
    return _decrypt_value(enc_key)


async def _resolve_model(db) -> dict | None:
    conn, cur = db
    await cur.execute(
        "SELECT config_value FROM system_configs WHERE config_key = 'model_selection_source'"
    )
    row = await cur.fetchone()
    source = dict(row)["config_value"] if row else "config_yaml"

    if source != "bff_api":
        return None

    await cur.execute(
        """SELECT sc.*, sc.config_json, m.model_name, m.api_base, m.api_key,
           m.provider_name, m.supports_thinking, m.supports_vision
           FROM ai_scene_model_configs sc
           JOIN ai_models m ON sc.model_id = m.id
           WHERE sc.scene_key = 'case_intelligent_entry' AND m.enabled = 1
           ORDER BY sc.priority ASC LIMIT 1""",
    )
    row = await cur.fetchone()
    if not row:
        return None

    model = dict(row)
    api_key_enc = model.get("api_key", "")
    model["api_key_decrypted"] = _decrypt_key(api_key_enc)

    config_raw = model.get("config_json")
    params = {}
    if config_raw:
        if isinstance(config_raw, str):
            try:
                params = json.loads(config_raw)
            except json.JSONDecodeError:
                params = {}
        elif isinstance(config_raw, dict):
            params = config_raw

    return {
        "api_base": (model.get("api_base") or "").rstrip("/"),
        "api_key": model.get("api_key_decrypted", ""),
        "model_name": model.get("model_name", ""),
        "params": params,
        "supports_vision": bool(model.get("supports_vision", 0)),
    }


async def _get_chat_model_config(db) -> dict | None:
    """获取案件分析用的聊天模型配置（优先 case_intelligent_entry，fallback 到 chat_model）"""
    # 1. 尝试 BFF API 动态配置
    cfg = await _resolve_model(db)
    if cfg:
        return cfg

    # 2. fallback 到 system_configs 的 ai_scene_chat_model
    try:
        from app.core.config import get_ai_scene_config
        scene_cfg = await get_ai_scene_config("chat_model", db)
        if scene_cfg and scene_cfg.get("api_base") and scene_cfg.get("model"):
            api_key = scene_cfg.get("api_key_decrypted") or scene_cfg.get("api_key", "")
            return {
                "api_base": scene_cfg["api_base"].rstrip("/"),
                "api_key": api_key,
                "model_name": scene_cfg["model"],
                "supports_vision": False,
            }
    except Exception:
        pass

    # 3. fallback 到 settings 默认值
    from app.core.config import settings
    if settings.LLM_API_BASE and settings.LLM_MODEL:
        return {
            "api_base": settings.LLM_API_BASE.rstrip("/"),
            "api_key": settings.SILICONFLOW_API_KEY or settings.LLM_API_KEY or "",
            "model_name": settings.LLM_MODEL,
            "supports_vision": False,
        }
    return None


async def _get_vision_model_config(db) -> dict | None:
    """获取视觉模型配置"""
    try:
        from app.core.config import get_ai_scene_config
        scene_cfg = await get_ai_scene_config("vision_model", db)
        if scene_cfg and scene_cfg.get("api_base") and scene_cfg.get("model"):
            api_key = scene_cfg.get("api_key_decrypted") or scene_cfg.get("api_key", "")
            return {
                "api_base": scene_cfg["api_base"].rstrip("/"),
                "api_key": api_key,
                "model_name": scene_cfg["model"],
                "supports_vision": True,
            }
    except Exception:
        pass

    # fallback 到 settings 默认值
    from app.core.config import settings
    if settings.LLM_API_BASE and settings.AI_SCENE_VISION_MODEL:
        return {
            "api_base": settings.LLM_API_BASE.rstrip("/"),
            "api_key": settings.SILICONFLOW_API_KEY or settings.LLM_API_KEY or "",
            "model_name": settings.AI_SCENE_VISION_MODEL,
            "supports_vision": True,
        }
    return None


async def _call_llm_api(config: dict, prompt: str, image_parts: list[dict] | None = None) -> str:
    """直接调用 LLM /chat/completions API（绕过 ReAct agent，避免 tools 绑定）"""
    api_base = config.get("api_base", "").rstrip("/")
    api_key = config.get("api_key", "")
    model_name = config.get("model_name", "")

    if not api_base or not model_name:
        raise ValueError("模型配置不完整：缺少 api_base 或 model_name")

    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    }

    if image_parts:
        # Vision 格式
        content = [{"type": "text", "text": prompt}]
        for img in image_parts:
            content.append({
                "type": "image_url",
                "image_url": {"url": f"data:{img['mime_type']};base64,{img['base64_data']}"},
            })
        messages = [{"role": "user", "content": content}]
    else:
        messages = [{"role": "user", "content": prompt}]

    payload = {
        "model": model_name,
        "messages": messages,
        "temperature": 0.3,
        "max_tokens": 4000,
    }

    async with httpx.AsyncClient(timeout=120.0) as client:
        resp = await client.post(f"{api_base}/chat/completions", headers=headers, json=payload)
        if resp.status_code != 200:
            error_text = resp.text[:500]
            raise RuntimeError(f"LLM API 返回错误 HTTP {resp.status_code}: {error_text}")

        data = resp.json()
        choices = data.get("choices", [])
        if not choices:
            raise RuntimeError("LLM API 未返回 choices")

        return choices[0].get("message", {}).get("content", "")


async def _extract_text_from_file(file: UploadFile) -> str:
    try:
        content_bytes = await file.read()
        filename = file.filename.lower()

        if filename.endswith(('.txt', '.md')):
            return content_bytes.decode('utf-8', errors='replace')
        elif filename.endswith('.pdf'):
            return await _extract_pdf_text(content_bytes)
        elif filename.endswith(('.doc', '.docx')):
            return await _extract_doc_text(content_bytes, filename)
        elif filename.endswith(('.xls', '.xlsx')):
            return await _extract_excel_text(content_bytes, filename)
        elif filename.endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp')):
            return await _extract_image_text(content_bytes)
        else:
            return content_bytes.decode('utf-8', errors='replace')
    except Exception as e:
        return f"文件解析错误: {str(e)}"


async def _extract_pdf_text(content_bytes: bytes) -> str:
    try:
        import pdfplumber
        pdf_file = io.BytesIO(content_bytes)
        text = ""
        with pdfplumber.open(pdf_file) as pdf:
            for page in pdf.pages:
                text += page.extract_text() or ""
        return text
    except ImportError:
        return "PDF解析库未安装"
    except Exception as e:
        return f"PDF解析失败: {str(e)}"


async def _extract_doc_text(content_bytes: bytes, filename: str) -> str:
    try:
        if filename.endswith('.docx'):
            from docx import Document
            doc_file = io.BytesIO(content_bytes)
            doc = Document(doc_file)
            text = "\n".join([para.text for para in doc.paragraphs])
            return text
        else:
            return "Word文档解析功能待实现"
    except ImportError:
        return "Word解析库未安装"
    except Exception as e:
        return f"Word解析失败: {str(e)}"


async def _extract_excel_text(content_bytes: bytes, filename: str) -> str:
    try:
        from openpyxl import load_workbook
        wb = load_workbook(io.BytesIO(content_bytes), read_only=True, data_only=True)
        parts = []
        for sheet_name in wb.sheetnames:
            ws = wb[sheet_name]
            rows = []
            for row in ws.iter_rows(values_only=True):
                cells = [str(c) if c is not None else "" for c in row]
                if any(cells):
                    rows.append("\t".join(cells))
            if rows:
                parts.append(f"=== 工作表: {sheet_name} ===\n" + "\n".join(rows))
        wb.close()
        return "\n\n".join(parts) if parts else "Excel文件为空"
    except ImportError:
        return "Excel解析库未安装"
    except Exception as e:
        return f"Excel解析失败: {str(e)}"


async def _extract_image_text(content_bytes: bytes) -> str:
    """图片文字识别 — 需要视觉模型支持"""
    return "图片识别需要配置视觉模型，请前往 系统配置-AI配置 中配置 vision_model"


def _get_image_mime_type(filename: str) -> str:
    ext = filename.lower().rsplit(".", 1)[-1] if "." in filename else ""
    mime_map = {
        "jpg": "image/jpeg", "jpeg": "image/jpeg",
        "png": "image/png", "gif": "image/gif", "webp": "image/webp",
    }
    return mime_map.get(ext, "image/jpeg")


def _build_vision_content(prompt: str, image_parts: list[dict]) -> list:
    content = [{"type": "text", "text": prompt}]
    for img in image_parts:
        content.append({
            "type": "image_url",
            "image_url": {"url": f"data:{img['mime_type']};base64,{img['base64_data']}"},
        })
    return content


def _build_case_analysis_prompt(text: str) -> str:
    return f"""你是一位专业的行政执法助手。请从以下案件材料中精准提取信息，自动填充案件表单。

## 材料内容
---
{text}
---

## 提取要求
请严格按照以下字段定义提取信息，返回JSON对象。字段名和枚举值必须与定义完全一致，不要编造材料中不存在的信息。

### 基本信息
- title (string): 案件标题，格式如"关于XXX涉嫌XXX案"
- cause (string): 案由，格式如"涉嫌XXX违法行为"
- source (string): 案件来源，只能取以下枚举值之一：
  - "complaint" = 投诉举报
  - "inspection" = 巡查发现
  - "transfer" = 部门移送
  - "report" = 上级交办
  - "other" = 其他
- type (string): 案件类型，只能取以下枚举值之一：
  - "food_drug" = 食品药品
  - "advertising" = 广告
  - "price" = 价格
  - "product_quality" = 产品质量
  - "intellectual_property" = 知识产权
  - "other" = 其他
- filing_date (string|null): 立案日期，格式YYYY-MM-DD，无法确定则null
- priority (string): 优先级，只能取："normal"(普通)、"urgent"(紧急)、"major"(重大)

### 当事人信息
- party_name (string): 当事人名称（个人姓名或企业名称）
- party_type (string): 当事人类型，只能取以下枚举值之一：
  - "individual" = 个人
  - "enterprise" = 企业
  - "individual_business" = 个体工商户
  - "other" = 其他
- party_id_type (string): 证件类型，只能取："id_card"(身份证)、"business_license"(营业执照)、"other"(其他)
- party_id_number (string|null): 证件号码
- credit_code (string|null): 统一社会信用代码（仅企业/个体工商户）
- legal_representative (string|null): 法定代表人（仅企业/个体工商户）
- phone (string|null): 联系电话
- address (string|null): 住所地址

### 执法人员
- main_officer_name (string|null): 主办人员姓名
- main_officer_badge (string|null): 主办人员执法证号
- assistant_officer_name (string|null): 协办人员姓名
- assistant_officer_badge (string|null): 协办人员执法证号

### 违法事实
- illegal_fact_summary (string|null): 违法事实摘要，2000字以内
- involved_amount (number|null): 涉案金额（纯数字，单位元）
- remark (string|null): 备注

### 分析说明
- analysis_summary (string): 对材料的简要分析说明，包括提取了哪些关键信息、哪些信息缺失

## 输出格式
只返回JSON对象，不要markdown代码块标记，不要任何解释性文字。示例：
{{"title":"关于XX超市涉嫌销售过期食品案","cause":"涉嫌销售过期食品","source":"complaint","type":"food_drug","filing_date":null,"priority":"normal","party_name":"XX超市","party_type":"enterprise","party_id_type":"business_license","party_id_number":"91110000XXXXXXXXXX","credit_code":"91110000XXXXXXXXXX","legal_representative":"张三","phone":"13800138000","address":"XX市XX区XX路XX号","main_officer_name":null,"main_officer_badge":null,"assistant_officer_name":null,"assistant_officer_badge":null,"illegal_fact_summary":"当事人XX超市销售超过保质期的食品...","involved_amount":500.00,"remark":null,"analysis_summary":"从投诉材料中提取了当事人信息和违法事实，执法人员信息缺失"}}"""


def _parse_ai_response(text: str) -> dict:
    text = text.strip()
    if text.startswith("```"):
        lines = text.split("\n")
        text = "\n".join(lines[1:]) if len(lines) > 1 else text
        if text.endswith("```"):
            text = text[:-3].strip()

    try:
        return json.loads(text)
    except json.JSONDecodeError:
        pass

    json_match = re.search(r'\{[\s\S]*\}', text)
    if json_match:
        try:
            return json.loads(json_match.group(0))
        except json.JSONDecodeError:
            pass

    return {"error": "无法解析AI返回结果", "_raw": text[:500]}


@router.post("")
async def create_case(
    request: Request,
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    conn, cur = db
    body = await safe_json(request)

    missing = CASE_CREATE_REQUIRED - set(body.keys())
    if missing:
        return {"error": "字段校验失败", "detail": f"缺少必填字段: {', '.join(missing)}"}

    if body.get("source") not in VALID_SOURCES:
        return {"error": "字段校验失败", "detail": f"source必须是: {', '.join(VALID_SOURCES)}"}
    if body.get("party_type") not in VALID_PARTY_TYPES:
        return {"error": "字段校验失败", "detail": f"party_type必须是: {', '.join(VALID_PARTY_TYPES)}"}
    if body.get("party_id_type") not in VALID_ID_TYPES:
        return {"error": "字段校验失败", "detail": f"party_id_type必须是: {', '.join(VALID_ID_TYPES)}"}

    case_id = str(uuid.uuid4())
    user_id = user.get("userId", "")
    user_group_id = body.get("group_id")

    # 处理案号字段
    region_prefix = body.get("region_prefix", "")
    case_year = body.get("case_year", str(datetime.date.today().year))
    case_sequence = body.get("case_sequence", "001")

    case_number = body.get("case_number") or f"{region_prefix}〔{case_year}〕{case_sequence}号"

    # 查询创建者的执法区域信息
    enforcement_region = None
    await cur.execute(
        "SELECT level, enforcement_region, enforcement_region_path FROM users WHERE id = %s",
        (user_id,),
    )
    creator_row = await cur.fetchone()
    if creator_row:
        cr = dict(creator_row)
        creator_level = cr.get("level", "")
        region_path = cr.get("enforcement_region_path", "")
        # 省级用户：取省级区域；市级及以下：取市级区域
        if region_path:
            path_parts = region_path.split("/")
            if creator_level == "province" and len(path_parts) >= 1:
                enforcement_region = path_parts[0]
            elif len(path_parts) >= 2:
                enforcement_region = path_parts[1]
            else:
                enforcement_region = region_path

    main_officer = body.get("main_officer", {})
    if isinstance(main_officer, (dict, list)):
        if isinstance(main_officer, list):
            # 列表格式：取第一个元素作为主办人
            main_officer = main_officer[0] if main_officer else {}
        # 如果缺少 user_id，根据姓名从 users 表查找补充
        if isinstance(main_officer, dict) and not main_officer.get("user_id") and main_officer.get("name"):
            await cur.execute(
                "SELECT id FROM users WHERE real_name = %s AND is_active = 1 LIMIT 1",
                (main_officer["name"],),
            )
            officer_row = await cur.fetchone()
            if officer_row:
                main_officer["user_id"] = str(officer_row["id"])
        main_officer = json.dumps(main_officer, ensure_ascii=False)
    assistant_officer = body.get("assistant_officer")
    if assistant_officer and isinstance(assistant_officer, (dict, list)):
        if isinstance(assistant_officer, list):
            # 列表格式：取第一个元素作为协办人
            assistant_officer = assistant_officer[0] if assistant_officer else {}
        # 如果缺少 user_id，根据姓名从 users 表查找补充
        if isinstance(assistant_officer, dict) and not assistant_officer.get("user_id") and assistant_officer.get("name"):
            await cur.execute(
                "SELECT id FROM users WHERE real_name = %s AND is_active = 1 LIMIT 1",
                (assistant_officer["name"],),
            )
            officer_row = await cur.fetchone()
            if officer_row:
                assistant_officer["user_id"] = str(officer_row["id"])
        assistant_officer = json.dumps(assistant_officer, ensure_ascii=False)

    # 构建 party_info JSON
    party_info = {
        "name": body.get("party_name"),
        "type": body.get("party_type"),
        "id_type": body.get("party_id_type"),
        "id_number": body.get("party_id_number"),
        "credit_code": body.get("credit_code"),
        "legal_representative": body.get("legal_representative"),
        "phone": body.get("phone"),
        "address": body.get("address"),
    }
    # 添加扩展字段（如果提供了）
    party_info_input = body.get("party_info", {})
    if isinstance(party_info_input, dict):
        party_info.update(party_info_input)
    party_info_json = json.dumps(party_info, ensure_ascii=False)

    try:
        await cur.execute(
            "INSERT INTO cases (id, case_number, title, cause, source, case_type, status, priority, "
            "filing_date, party_name, party_type, credit_code, legal_representative, "
            "party_id_type, party_id_number, phone, address, illegal_fact_summary, "
            "involved_amount, main_officer, assistant_officer, remark, description, "
            "party_info, enforcement_region, created_by, group_id) "
            "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, "
            "%s, %s, %s, %s, %s, "
            "%s, %s, %s, %s, %s, "
            "%s, %s, %s, %s, %s, %s, %s, %s, %s)",
            (
                case_id,
                case_number,
                body["title"],
                body["cause"],
                body["source"],
                None,
                body.get("status", "in_progress"),
                body.get("priority", "normal"),
                body.get("filing_date"),
                body["party_name"],
                body["party_type"],
                body.get("credit_code"),
                body.get("legal_representative"),
                body.get("party_id_type"),
                body.get("party_id_number"),
                body.get("phone"),
                body.get("address"),
                body.get("illegal_fact_summary"),
                body.get("involved_amount"),
                main_officer,
                assistant_officer,
                body.get("remark"),
                body.get("description"),
                party_info_json,
                enforcement_region,
                user_id,
                user_group_id,
            ),
        )
    except Exception as e:
        logger.error(f"Failed to create case: {e}")
        return {"error": "案件创建失败", "detail": str(e)}

    await cur.execute("SELECT * FROM cases WHERE id = %s", (case_id,))
    case_row = await cur.fetchone()
    case_data = dict(case_row) if case_row else {}

    # 多案由支持：批量写入 case_causes_detail
    causes_input = body.get("causes", [])
    if isinstance(causes_input, list) and len(causes_input) > 0:
        for idx, cause_item in enumerate(causes_input):
            cause_name = (cause_item.get("cause_name") or "").strip()
            if not cause_name:
                continue
            await cur.execute(
                "INSERT INTO case_causes_detail "
                "(case_id, cause_name, cause_code, level1_code, level1_name, level2_code, level2_name, "
                "cause_source, ai_confidence, sort_order) "
                "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
                (
                    case_id,
                    cause_name,
                    cause_item.get("cause_code"),
                    cause_item.get("level1_code"),
                    cause_item.get("level1_name"),
                    cause_item.get("level2_code"),
                    cause_item.get("level2_name"),
                    cause_item.get("cause_source", "user_custom"),
                    cause_item.get("ai_confidence"),
                    idx,
                ),
            )
        # 聚合更新 cases 表的案由字段
        await _update_case_cause_summary(case_id, cur)
        # 重新读取更新后的案件数据
        await cur.execute("SELECT * FROM cases WHERE id = %s", (case_id,))
        case_row = await cur.fetchone()
        case_data = dict(case_row) if case_row else {}

    pool = await info_pool_service.initialize(case_id, case_data, db)

    # 简版移除：案件会话创建（deerflow_service）
    # deerflow_service = get_deerflow_service()
    # thread_info = await deerflow_service.get_or_create_case_thread(case_id, user_id, db)
    thread_info = None

    await _log_event(
        case_id, "case_created", user_id, db,
        event_data={"case_number": case_number, "title": body["title"]},
        operator_name=user.get("username", ""),
    )

    await conn.commit()

    # 类案缓存不在创建时预计算（会阻塞事件循环）
    # 用户查看案件详情时，GET /related 接口会按需计算并缓存
    # 前端已支持 computing 状态展示和自动轮询

    return {
        "success": True,
        "data": {
            "case": desensitize_case(case_data),
            "basic_info_pool": pool,
            "deerflow_thread": thread_info,
        },
    }


@router.get("")
async def list_cases(
    request: Request,
    view: str = Query("mine"),
    keyword: str = Query(""),
    status: str = Query(""),
    case_type: str = Query(""),
    page: int = Query(1, ge=1),
    pageSize: int = Query(12, ge=1, le=50),
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    conn, cur = db
    user_id = user.get("userId", "")

    conditions = ["c.is_deleted = 0"]
    params: list = []

    if view == "mine":
        conditions.append("c.created_by = %s")
        params.append(user_id)
    elif view == "involved":
        conditions.append(
            "(c.created_by = %s OR "
            "JSON_EXTRACT(c.main_officer, '$.user_id') = %s OR "
            "JSON_EXTRACT(c.assistant_officer, '$.user_id') = %s)"
        )
        params.extend([user_id, user_id, user_id])

    if status and status in VALID_STATUSES:
        conditions.append("c.status = %s")
        params.append(status)

    if case_type and case_type in VALID_CASE_TYPES:
        conditions.append("c.case_type = %s")
        params.append(case_type)

    if keyword:
        conditions.append("(c.title LIKE %s OR c.party_name LIKE %s)")
        params.extend([f"%{keyword}%", f"%{keyword}%"])

    where = f"WHERE {' AND '.join(conditions)}"

    await cur.execute(f"SELECT COUNT(*) as total FROM cases c {where}", params)
    total = (await cur.fetchone())["total"]

    offset = (page - 1) * pageSize
    await cur.execute(
        f"SELECT c.id, c.case_number, c.title, c.cause, c.source, c.case_type, "
        f"c.status, c.priority, c.party_name, c.party_type, c.filing_date, "
        f"c.document_count, c.thread_message_count, c.created_at, c.updated_at "
        f"FROM cases c {where} ORDER BY c.updated_at DESC LIMIT %s OFFSET %s",
        params + [pageSize, offset],
    )
    rows = [dict(row) for row in (await cur.fetchall())]

    # 为每个案件附加删除权限
    user_role = user.get("role", "")
    is_admin = user_role in ("system_admin", "admin", "org_admin")
    for row in rows:
        row["permissions"] = {"can_delete": is_admin}

    total_pages = (total + pageSize - 1) // pageSize if total > 0 else 0

    return {
        "success": True,
        "data": {
            "cases": rows,
            "pagination": {
                "page": page,
                "pageSize": pageSize,
                "total": total,
                "totalPages": total_pages,
            },
        },
    }


@router.get("/{case_id}")
async def get_case(
    case_id: str,
    request: Request,
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    conn, cur = db
    user_id = user.get("userId", "")

    case_data = await _check_case_access(case_id, user_id, db, "read")
    if not case_data:
        return {"error": "案件不存在或无权访问", "detail": "case not found or access denied"}

    # 查询创建者信息（用于补充单位名称和执法人员信息）
    created_by = case_data.get("created_by")
    creator_info = None
    if created_by:
        # 优先从 user_memories（个人中心）获取执法单位信息
        try:
            await cur.execute(
                "SELECT content FROM user_memories WHERE user_id = %s AND memory_type = 'user_profile'",
                (created_by,),
            )
            profile_row = await cur.fetchone()
            if profile_row and dict(profile_row).get("content"):
                saved = json.loads(dict(profile_row)["content"])
                basic_info = saved.get("basic_info", {})
                if basic_info.get("org_name"):
                    case_data["org_name"] = basic_info["org_name"]
                if basic_info.get("org_phone"):
                    case_data["unit_contact_phone"] = basic_info["org_phone"]
                if basic_info.get("org_address"):
                    case_data["unit_contact_address"] = basic_info["org_address"]
        except Exception:
            pass

        # 兜底：从 users 表获取
        await cur.execute(
            "SELECT id, real_name, badge_number, org_name, unit_contact_phone, unit_contact_address FROM users WHERE id = %s",
            (created_by,),
        )
        creator_row = await cur.fetchone()
        if creator_row:
            creator_info = dict(creator_row)
            if creator_info.get("org_name") and not case_data.get("org_name"):
                case_data["org_name"] = creator_info["org_name"]
            if creator_info.get("unit_contact_phone") and not case_data.get("unit_contact_phone"):
                case_data["unit_contact_phone"] = creator_info["unit_contact_phone"]
            if creator_info.get("unit_contact_address") and not case_data.get("unit_contact_address"):
                case_data["unit_contact_address"] = creator_info["unit_contact_address"]

    # 如果 main_officer 为空或没有 name，自动填充创建者信息
    main_officer = case_data.get("main_officer")
    if main_officer:
        if isinstance(main_officer, str):
            try:
                main_officer = json.loads(main_officer)
            except (json.JSONDecodeError, TypeError):
                main_officer = {}
        if isinstance(main_officer, dict) and not main_officer.get("name") and creator_info:
            case_data["main_officer"] = json.dumps({
                "user_id": creator_info.get("id"),
                "name": creator_info.get("real_name", ""),
                "badge_number": creator_info.get("badge_number", ""),
            }, ensure_ascii=False)

    pool = await info_pool_service.get(case_id, db)

    await cur.execute(
        "SELECT id, template_id, template_name, document_name, version, status, "
        "generation_method, created_by, created_at, updated_at "
        "FROM case_documents WHERE case_id = %s AND is_deleted = 0 "
        "ORDER BY updated_at DESC",
        (case_id,),
    )
    documents = [dict(row) for row in (await cur.fetchall())]

    try:
        await cur.execute(
            "SELECT deerflow_thread_id, message_count, last_message_at, context_injected "
            "FROM case_threads WHERE case_id = %s",
            (case_id,),
        )
        thread_row = await cur.fetchone()
    except Exception:
        thread_row = None
    thread_stats = None
    if thread_row:
        tr = dict(thread_row)
        thread_stats = {
            "thread_id": tr["deerflow_thread_id"],
            "message_count": tr.get("message_count", 0),
            "last_message_at": str(tr["last_message_at"]) if tr.get("last_message_at") else None,
        }

    await cur.execute("SELECT role FROM users WHERE id = %s", (user_id,))
    user_role_row = await cur.fetchone()
    user_role = dict(user_role_row).get("role", "") if user_role_row else ""

    permissions = _build_permissions(case_data, user_id, user_role)

    # ===== V2: 查询多案由+结构化法条 =====
    await cur.execute(
        """SELECT id, cause_name, cause_code, level1_code, level1_name,
                  level2_code, level2_name, cause_source, ai_confidence, sort_order
           FROM case_causes_detail
           WHERE case_id = %s
           ORDER BY sort_order ASC, id ASC""",
        (case_id,),
    )
    cause_details = [dict(row) for row in (await cur.fetchall())]

    # 为每个案由查询关联的法条
    for cause in cause_details:
        cause_id = cause["id"]
        await cur.execute(
            """SELECT id, law_type, law_name, article_number, article_full_ref,
                      content_summary, content_detail, source, source_id, sort_order
               FROM case_cause_laws
               WHERE cause_detail_id = %s
               ORDER BY law_type, sort_order ASC, id ASC""",
            (cause_id,),
        )
        laws = [dict(row) for row in (await cur.fetchall())]
        cause["violation_laws"] = [l for l in laws if l["law_type"] == "violation"]
        cause["penalty_laws"] = [l for l in laws if l["law_type"] == "penalty"]

    # 查询用户区域信息（从 enforcement_region_path 解析省/市）
    user_region = {"province": "", "city": ""}
    await cur.execute(
        "SELECT enforcement_region_path, level FROM users WHERE id = %s LIMIT 1",
        (user_id,),
    )
    user_region_row = await cur.fetchone()
    if user_region_row:
        urr = dict(user_region_row)
        region_path = urr.get("enforcement_region_path", "") or ""
        if region_path:
            path_parts = region_path.strip("/").split("/")
            if len(path_parts) >= 1:
                user_region["province"] = path_parts[0]
            if len(path_parts) >= 2:
                user_region["city"] = path_parts[1]

    return {
        "success": True,
        "data": {
            "case": desensitize_case(case_data),
            "basic_info_pool": pool,
            "documents": documents,
            "thread_stats": thread_stats,
            "permissions": permissions,
            "cause_details": cause_details,
            "user_region": user_region,
        },
    }


@router.put("/{case_id}")
async def update_case(
    case_id: str,
    request: Request,
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    conn, cur = db
    user_id = user.get("userId", "")
    body = await safe_json(request)

    case_data = await _check_case_access(case_id, user_id, db, "write")
    if not case_data:
        return {"error": "案件不存在或无编辑权限", "detail": "case not found or no write permission"}

    updates = []
    values = []
    updated_fields = []

    # 处理案号字段
    if "case_number" in body:
        updates.append("case_number = %s")
        values.append(body["case_number"])
        updated_fields.append("case_number")
    elif "region_abbr" in body or "unit_abbr" in body or "doc_abbr" in body or "case_year" in body or "case_sequence" in body:
        region_abbr = body.get("region_abbr", "")
        unit_abbr = body.get("unit_abbr", "")
        doc_abbr = body.get("doc_abbr", "")
        case_year = body.get("case_year", "")
        case_sequence = body.get("case_sequence", "")

        # 如果没有传年份或序号，尝试从现有案号中解析
        if not case_year or not case_sequence:
            existing_case_number = case_data.get("case_number", "")
            import re
            match = re.search(r'〔(\d{4})〕(\d+)号', existing_case_number)
            if match:
                if not case_year:
                    case_year = match.group(1)
                if not case_sequence:
                    case_sequence = match.group(2)

        prefix = f"{region_abbr}{unit_abbr}{doc_abbr}"
        case_number = f"{prefix}〔{case_year}〕{case_sequence}号"

        updates.append("case_number = %s")
        values.append(case_number)
        updated_fields.append("case_number")
    elif "region_prefix" in body or "case_year" in body or "case_sequence" in body:
        # 兼容旧字段
        region_prefix = body.get("region_prefix", "")
        case_year = body.get("case_year", "")
        case_sequence = body.get("case_sequence", "")

        # 如果没有传年份或序号，尝试从现有案号中解析
        if not case_year or not case_sequence:
            existing_case_number = case_data.get("case_number", "")
            import re
            match = re.search(r'〔(\d{4})〕(\d+)号', existing_case_number)
            if match:
                if not case_year:
                    case_year = match.group(1)
                if not case_sequence:
                    case_sequence = match.group(2)

        case_number = f"{region_prefix}〔{case_year}〕{case_sequence}号"

        updates.append("case_number = %s")
        values.append(case_number)
        updated_fields.append("case_number")

    for key, value in body.items():
        if key in CASE_EDITABLE_FIELDS and key != "status" and key not in ("case_number",):
            if key in ("main_officer", "assistant_officer") and isinstance(value, dict):
                # 如果缺少 user_id，根据姓名从 users 表查找补充
                if not value.get("user_id") and value.get("name"):
                    await cur.execute(
                        "SELECT id FROM users WHERE real_name = %s AND is_active = 1 LIMIT 1",
                        (value["name"],),
                    )
                    officer_row = await cur.fetchone()
                    if officer_row:
                        value["user_id"] = str(officer_row["id"])
                value = json.dumps(value, ensure_ascii=False)
            elif key == "party_info" and isinstance(value, dict):
                value = json.dumps(value, ensure_ascii=False)
            elif key == "discretion_advice" and isinstance(value, dict):
                value = json.dumps(value, ensure_ascii=False)
            db_key = key
            updates.append(f"{db_key} = %s")
            values.append(value)
            updated_fields.append(key)

    if "status" in body and body["status"] in VALID_STATUSES:
        updates.append("status = %s")
        values.append(body["status"])
        updated_fields.append("status")

    if not updates:
        return {"error": "没有可更新的字段", "detail": "no fields to update"}

    updates.append("updated_at = NOW()")
    values.append(case_id)

    await cur.execute(
        f"UPDATE cases SET {', '.join(updates)} WHERE id = %s",
        values,
    )

    # 多案由支持：更新 case_causes_detail 表
    causes_input = body.get("causes")
    if isinstance(causes_input, list):
        # 先删除旧的案由记录
        await cur.execute("DELETE FROM case_causes_detail WHERE case_id = %s", (case_id,))
        # 批量插入新的案由
        for idx, cause_item in enumerate(causes_input):
            cause_name = (cause_item.get("cause_name") or "").strip()
            if not cause_name:
                continue
            await cur.execute(
                "INSERT INTO case_causes_detail "
                "(case_id, cause_name, cause_code, level1_code, level1_name, level2_code, level2_name, "
                "cause_source, ai_confidence, sort_order) "
                "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
                (
                    case_id,
                    cause_name,
                    cause_item.get("cause_code"),
                    cause_item.get("level1_code"),
                    cause_item.get("level1_name"),
                    cause_item.get("level2_code"),
                    cause_item.get("level2_name"),
                    cause_item.get("cause_source", "user_custom"),
                    cause_item.get("ai_confidence"),
                    idx,
                ),
            )
        # 聚合更新 cases 表的案由字段
        await _update_case_cause_summary(case_id, cur)
        updated_fields.append("causes")

    # 检测关键字段变更，自动刷新关联/类案缓存
    _KEY_FIELDS_FOR_RELATED = {"party_name", "legal_representative", "cause", "causes", "party_info"}
    if _KEY_FIELDS_FOR_RELATED & set(updated_fields):
        # 异步刷新缓存（不阻塞响应）
        import asyncio
        asyncio.create_task(_async_refresh_related_cache(case_id))

    pool_synced = await info_pool_service.sync_update(case_id, dict(body), db)

    context_updated = False
    # 简版移除：上下文变更通知（deerflow_service）
    # if pool_synced:
    #     deerflow_service = get_deerflow_service()
    #     notify_result = await deerflow_service.notify_context_change(case_id, db)
    #     context_updated = notify_result.get("notified", False)

    await _log_event(
        case_id, "case_updated", user_id, db,
        event_data={"updated_fields": updated_fields},
        operator_name=user.get("username", ""),
    )

    await conn.commit()

    await cur.execute("SELECT * FROM cases WHERE id = %s", (case_id,))
    updated_row = await cur.fetchone()
    updated_case = dict(updated_row) if updated_row else case_data

    return {
        "success": True,
        "data": {
            "case": desensitize_case(updated_case),
            "updated_fields": updated_fields,
            "pool_synced": pool_synced,
            "context_updated": context_updated,
        },
    }


@router.delete("/{case_id}")
async def delete_case(
    case_id: str,
    request: Request,
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    conn, cur = db
    user_id = user.get("userId", "")

    case_data = await _check_case_access(case_id, user_id, db, "delete")
    if not case_data:
        await cur.execute("SELECT role FROM users WHERE id = %s", (user_id,))
        user_row = await cur.fetchone()
        is_admin = user_row and dict(user_row).get("role") in ("system_admin", "admin", "org_admin")
        if is_admin:
            return {"error": "案件不存在", "detail": "case not found"}
        return {"error": "删除案件需联系管理员", "detail": "only admin can delete cases"}

    await cur.execute(
        "UPDATE cases SET is_deleted = 1, deleted_at = NOW(), updated_at = NOW() WHERE id = %s",
        (case_id,),
    )

    await _log_event(
        case_id, "case_updated", user_id, db,
        event_data={"action": "delete"},
        operator_name=user.get("username", ""),
    )

    await conn.commit()
    return {"success": True, "message": "案件已删除"}


# ── 简版移除：案件会话相关端点（threads/messages/cancel）──
# 如需恢复，取消下方注释并重新引入 deerflow_service 和 _check_sensitive_words
# @router.get("/{case_id}/thread")
# @router.post("/{case_id}/thread/messages")
# @router.get("/{case_id}/thread/messages")
# @router.delete("/{case_id}/thread/messages")
# @router.post("/{case_id}/thread/cancel")


@router.get("/{case_id}/documents")
async def get_case_documents(
    case_id: str,
    request: Request,
    templateId: str = Query(""),
    status: str = Query(""),
    includeHistory: bool = Query(False),
    page: int = Query(1, ge=1),
    pageSize: int = Query(20, ge=1, le=100),
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    user_id = user.get("userId", "")
    case_data = await _check_case_access(case_id, user_id, db, "read")
    if not case_data:
        return {"error": "案件不存在或无权访问", "detail": "case not found or access denied"}

    conn, cur = db
    conditions = ["case_id = %s", "is_deleted = 0"]
    params: list = [case_id]

    if templateId:
        conditions.append("template_id = %s")
        params.append(templateId)

    if status and status in ("draft", "review", "finalized"):
        conditions.append("status = %s")
        params.append(status)

    if not includeHistory:
        conditions.append(
            "id IN (SELECT MAX(id) FROM case_documents WHERE case_id = %s AND is_deleted = 0 GROUP BY template_id)"
        )
        params.append(case_id)

    where = f"WHERE {' AND '.join(conditions)}"

    await cur.execute(f"SELECT COUNT(*) as total FROM case_documents {where}", params)
    total = (await cur.fetchone())["total"]

    offset = (page - 1) * pageSize
    await cur.execute(
        f"SELECT id, template_id, template_name, document_name, version, status, "
        f"generation_method, created_by, created_at, updated_at "
        f"FROM case_documents {where} ORDER BY updated_at DESC LIMIT %s OFFSET %s",
        params + [pageSize, offset],
    )
    documents = [dict(row) for row in (await cur.fetchall())]

    await cur.execute(
        "SELECT status, COUNT(*) as count FROM case_documents "
        "WHERE case_id = %s AND is_deleted = 0 GROUP BY status",
        (case_id,),
    )
    by_status = {row["status"]: row["count"] for row in (await cur.fetchall())}

    await cur.execute(
        "SELECT template_name, COUNT(*) as count FROM case_documents "
        "WHERE case_id = %s AND is_deleted = 0 GROUP BY template_name",
        (case_id,),
    )
    by_template = {row["template_name"]: row["count"] for row in (await cur.fetchall())}

    return {
        "success": True,
        "data": {
            "documents": documents,
            "pagination": {
                "page": page,
                "pageSize": pageSize,
                "total": total,
                "totalPages": (total + pageSize - 1) // pageSize if total > 0 else 0,
            },
            "statistics": {
                "total_documents": total,
                "by_status": by_status,
                "by_template": by_template,
            },
        },
    }


@router.get("/{case_id}/logs")
async def get_case_logs(
    case_id: str,
    request: Request,
    event_type: str = Query(""),
    page: int = Query(1, ge=1),
    pageSize: int = Query(20, ge=1, le=100),
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    user_id = user.get("userId", "")
    case_data = await _check_case_access(case_id, user_id, db, "read")
    if not case_data:
        return {"error": "案件不存在或无权访问", "detail": "case not found or access denied"}

    conn, cur = db
    conditions = ["case_id = %s"]
    params: list = [case_id]

    if event_type:
        conditions.append("event_type = %s")
        params.append(event_type)

    where = f"WHERE {' AND '.join(conditions)}"

    await cur.execute(f"SELECT COUNT(*) as total FROM case_logs {where}", params)
    total = (await cur.fetchone())["total"]

    offset = (page - 1) * pageSize
    await cur.execute(
        f"SELECT id, case_id, event_type, event_data, operator_id, operator_name, "
        f"resource_id, resource_type, created_at "
        f"FROM case_logs {where} ORDER BY created_at DESC LIMIT %s OFFSET %s",
        params + [pageSize, offset],
    )
    logs = [dict(row) for row in (await cur.fetchall())]

    return {
        "success": True,
        "data": {
            "logs": logs,
            "pagination": {
                "page": page,
                "pageSize": pageSize,
                "total": total,
            },
        },
    }


@router.post("/{case_id}/archive")
async def archive_case(
    case_id: str,
    request: Request,
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    conn, cur = db
    user_id = user.get("userId", "")

    await cur.execute("SELECT status FROM cases WHERE id = %s AND created_by = %s AND is_deleted = 0", (case_id, user_id))
    row = await cur.fetchone()
    if not row:
        return {"error": "案件不存在", "detail": "case not found"}

    await cur.execute(
        "UPDATE cases SET status = 'completed', updated_at = NOW() WHERE id = %s",
        (case_id,),
    )

    await _log_event(
        case_id, "status_changed", user_id, db,
        event_data={"previous_status": row["status"], "new_status": "completed"},
        operator_name=user.get("username", ""),
    )

    await conn.commit()
    return {"success": True, "message": "案件已归档"}


@router.post("/{case_id}/unarchive")
async def unarchive_case(
    case_id: str,
    request: Request,
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    conn, cur = db
    user_id = user.get("userId", "")

    await cur.execute("SELECT status FROM cases WHERE id = %s AND created_by = %s AND is_deleted = 0", (case_id, user_id))
    row = await cur.fetchone()
    if not row:
        return {"error": "案件不存在", "detail": "case not found"}

    await cur.execute(
        "UPDATE cases SET status = 'in_progress', updated_at = NOW() WHERE id = %s",
        (case_id,),
    )

    await _log_event(
        case_id, "status_changed", user_id, db,
        event_data={"previous_status": row["status"], "new_status": "in_progress"},
        operator_name=user.get("username", ""),
    )

    await conn.commit()
    return {"success": True, "message": "案件已取消归档"}


@router.post("/{case_id}/documents")
async def create_case_document(
    case_id: str,
    request: Request,
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    """创建文书（零AI版：直接用案件数据填充模板）"""
    from .document_generation import (
        _fetch_template_and_placeholders, _fetch_case_with_causes,
        _build_case_render_context, _build_user_doc_path,
        generate_document as docxtpl_generate,
        PLACEHOLDER_FIELD_DEF,
        _get_system_cur, _release_system_conn,
    )
    import shutil
    import time as _time

    conn, cur = db
    user_id = user.get("userId", "")
    case_data = await _check_case_access(case_id, user_id, db, "write")
    if not case_data:
        return {"error": "案件不存在或无创建文书权限", "detail": "no write permission"}

    body = await safe_json(request)
    template_id = body.get("template_id")
    if not template_id:
        return {"error": "请选择文书模板", "detail": "template_id required"}

    field_overrides = body.get("field_overrides")

    # document_templates 在 system.db 中，需要获取 sys_cur
    _sys_conn, sys_cur, _sys_pool = await _get_system_cur()
    try:
        template, _ = await _fetch_template_and_placeholders(cur, template_id, sys_cur=sys_cur)
    finally:
        await _release_system_conn(_sys_conn, _sys_pool)
    if not template:
        return {"error": "模板不存在", "detail": "template not found"}

    template_file_path = template.get("template_file_path", "")
    if not template_file_path or not os.path.exists(template_file_path):
        return {"error": "模板文件不存在", "detail": f"template docx not found: {template_file_path}"}

    case_row, causes = await _fetch_case_with_causes(cur, case_id)
    if not case_row:
        return {"error": "案件不存在", "detail": "case not found"}

    render_context = _build_case_render_context(case_row, causes)

    if field_overrides and isinstance(field_overrides, dict):
        for ph_key, value in field_overrides.items():
            if value is not None and str(value).strip():
                render_context[ph_key] = str(value).strip()

    try:
        temp_output = docxtpl_generate(actual_path, render_context)
    except Exception as e:
        return {"error": "文档渲染失败", "detail": str(e)}

    upload_dir = await get_upload_dir(db)
    docx_filename = f"{template['template_name']}.docx"
    final_path = _build_user_doc_path(upload_dir, int(user_id) if str(user_id).isdigit() else 1, "cases", docx_filename)

    shutil.move(temp_output, final_path)
    file_size = os.path.getsize(final_path)

    latest_version = 1
    await cur.execute(
        "SELECT COALESCE(MAX(version), 0) as max_ver FROM case_documents "
        "WHERE case_id = %s AND template_id = %s AND is_deleted = 0",
        (case_id, template["id"]),
    )
    ver_row = await cur.fetchone()
    if ver_row:
        latest_version = ver_row["max_ver"] + 1

    template_name = template["template_name"]
    document_name = f"{template_name}-v{latest_version}"

    await cur.execute(
        "INSERT INTO case_documents (case_id, template_id, template_name, document_name, "
        "version, status, content_docx_path, basic_info, generated_content, generation_method, created_by) "
        "VALUES (%s, %s, %s, %s, %s, 'draft', %s, %s, %s, 'direct_fill', %s)",
        (
            case_id, template["id"], template_name, document_name,
            latest_version, final_path,
            json.dumps(render_context, ensure_ascii=False, default=str),
            json.dumps({"overrides": field_overrides or {}}, ensure_ascii=False),
            user_id,
        ),
    )
    doc_id = cur.lastrowid

    await cur.execute(
        "UPDATE cases SET document_count = COALESCE(document_count, 0) + 1, updated_at = NOW() WHERE id = %s",
        (case_id,),
    )

    result = {
        "success": True,
        "data": {
            "document_id": doc_id,
            "template_name": template_name,
            "document_name": document_name,
            "version": latest_version,
            "status": "draft",
            "generation_method": "direct_fill",
        },
    }

    if result.get("success"):
        doc_data = result.get("data", {})
        await _log_event(
            case_id, "document_generated", user_id, db,
            event_data={
                "document_id": doc_data.get("document_id"),
                "template_name": doc_data.get("template_name"),
                "version": doc_data.get("version"),
            },
            resource_id=str(doc_data.get("document_id", "")),
            resource_type="document",
            operator_name=user.get("username", ""),
        )

    return result


@router.get("/{case_id}/documents/{doc_id}/download")
async def download_case_document(
    case_id: str,
    doc_id: str,
    request: Request,
    format: str = Query("docx"),
    version: str = Query("latest"),
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    user_id = user.get("userId", "")
    case_data = await _check_case_access(case_id, user_id, db, "read")
    if not case_data:
        return {"error": "案件不存在或无权访问", "detail": "access denied"}

    conn, cur = db

    if version == "latest" or not version.isdigit():
        await cur.execute(
            "SELECT id, document_name, content_md_path, content_docx_path, version "
            "FROM case_documents WHERE case_id = %s AND id = %s AND is_deleted = 0 "
            "ORDER BY version DESC LIMIT 1",
            (case_id, doc_id),
        )
    else:
        await cur.execute(
            "SELECT id, document_name, content_md_path, content_docx_path, version "
            "FROM case_documents WHERE case_id = %s AND id = %s AND version = %s AND is_deleted = 0",
            (case_id, doc_id, int(version)),
        )

    doc_row = await cur.fetchone()
    if not doc_row:
        return {"error": "文书不存在", "detail": "document not found"}

    doc = dict(doc_row)

    if format == "pdf":
        docx_path = doc.get("content_docx_path", "")
        if not docx_path or not os.path.exists(docx_path):
            return {"error": "DOCX文件不存在，无法转换为PDF", "detail": "docx file not found"}

        from ...core.config import settings
        onlyoffice_url = getattr(settings, "ONLYOFFICE_URL", "http://localhost:3025")
        import httpx
        async with httpx.AsyncClient(timeout=60.0) as client:
            try:
                convert_res = await client.post(
                    f"{onlyoffice_url}/ConvertService.ashx",
                    json={
                        "async": False,
                        "filetype": "docx",
                        "key": str(uuid.uuid4()),
                        "outputtype": "pdf",
                        "url": docx_path,
                    },
                    timeout=60.0,
                )
                convert_data = convert_res.json()
                pdf_url = convert_data.get("FileUrl")
                if pdf_url:
                    pdf_res = await client.get(pdf_url, timeout=60.0)
                    from fastapi.responses import Response
                    return Response(
                        content=pdf_res.content,
                        media_type="application/pdf",
                        headers={
                            "Content-Disposition": f'attachment; filename="{doc["document_name"]}.pdf"'
                        },
                    )
            except Exception as e:
                logger.warning(f"PDF conversion failed: {e}")

        return {"error": "PDF转换失败", "detail": "conversion service unavailable"}

    if format == "md":
        md_path = doc.get("content_md_path", "")
        if not md_path or not os.path.exists(md_path):
            return {"error": "Markdown文件不存在", "detail": "md file not found"}
        return FileResponse(
            md_path,
            media_type="text/markdown",
            filename=f"{doc['document_name']}.md",
        )

    docx_path = doc.get("content_docx_path", "")
    if not docx_path or not os.path.exists(docx_path):
        return {"error": "DOCX文件不存在", "detail": "docx file not found"}

    await _log_event(
        case_id, "document_downloaded", user_id, db,
        event_data={"document_id": doc_id, "version": doc["version"], "format": format},
        resource_id=doc_id,
        resource_type="document",
        operator_name=user.get("username", ""),
    )

    return FileResponse(
        docx_path,
        media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        filename=f"{doc['document_name']}.docx",
    )


@router.get("/{case_id}/documents/{doc_id}/versions")
async def get_document_versions(
    case_id: str,
    doc_id: str,
    request: Request,
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    user_id = user.get("userId", "")
    case_data = await _check_case_access(case_id, user_id, db, "read")
    if not case_data:
        return {"error": "案件不存在或无权访问", "detail": "access denied"}

    conn, cur = db

    await cur.execute(
        "SELECT template_id FROM case_documents WHERE id = %s AND case_id = %s AND is_deleted = 0 LIMIT 1",
        (doc_id, case_id),
    )
    doc_row = await cur.fetchone()
    if not doc_row:
        return {"error": "文书不存在", "detail": "document not found"}

    template_id = doc_row["template_id"]

    await cur.execute(
        "SELECT id, template_id, template_name, document_name, version, status, "
        "generation_method, created_by, created_at, updated_at "
        "FROM case_documents WHERE case_id = %s AND template_id = %s AND is_deleted = 0 "
        "ORDER BY version ASC",
        (case_id, template_id),
    )
    versions = [dict(row) for row in (await cur.fetchall())]

    return {
        "success": True,
        "data": {
            "versions": versions,
            "total": len(versions),
        },
    }


@router.post("/{case_id}/documents/{doc_id}/edit")
async def edit_case_document(
    case_id: str,
    doc_id: str,
    request: Request,
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    user_id = user.get("userId", "")
    case_data = await _check_case_access(case_id, user_id, db, "write")
    if not case_data:
        return {"error": "案件不存在或无编辑权限", "detail": "no write permission"}

    conn, cur = db
    body = await safe_json(request)
    edit_mode = body.get("edit_mode", "create_new")

    await cur.execute(
        "SELECT id, template_id, template_name, document_name, version, content_md_path, status "
        "FROM case_documents WHERE id = %s AND case_id = %s AND is_deleted = 0",
        (doc_id, case_id),
    )
    doc_row = await cur.fetchone()
    if not doc_row:
        return {"error": "文书不存在", "detail": "document not found"}

    doc = dict(doc_row)

    # 简版移除：文书编辑会话创建（deerflow_service）
    # deerflow_service = get_deerflow_service()
    # thread_info = await deerflow_service.get_or_create_case_thread(case_id, user_id, db)
    # if "error" in thread_info:
    #     return thread_info

    await _log_event(
        case_id, "document_ai_edited", user_id, db,
        event_data={"document_id": doc_id, "version": doc["version"], "edit_mode": edit_mode},
        resource_id=doc_id,
        resource_type="document",
        operator_name=user.get("username", ""),
    )

    return {
        "success": True,
        "data": {
            "thread_id": thread_info["thread_id"],
            "document_id": doc_id,
            "document_name": doc["document_name"],
            "current_version": doc["version"],
            "edit_mode": edit_mode,
            "status": doc["status"],
        },
    }


@router.post("/{case_id}/documents/{doc_id}/finalize")
async def finalize_case_document(
    case_id: str,
    doc_id: str,
    request: Request,
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    user_id = user.get("userId", "")
    case_data = await _check_case_access(case_id, user_id, db, "write")
    if not case_data:
        return {"error": "案件不存在或无操作权限", "detail": "no write permission"}

    conn, cur = db

    await cur.execute(
        "UPDATE case_documents SET status = 'finalized', updated_at = NOW() "
        "WHERE id = %s AND case_id = %s AND is_deleted = 0",
        (doc_id, case_id),
    )

    await _log_event(
        case_id, "document_finalized", user_id, db,
        event_data={"document_id": doc_id},
        resource_id=doc_id,
        resource_type="document",
        operator_name=user.get("username", ""),
    )

    return {"success": True, "message": "文书已定稿"}


@router.delete("/{case_id}/documents/{doc_id}")
async def delete_case_document(
    case_id: str,
    doc_id: str,
    request: Request,
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    user_id = user.get("userId", "")
    case_data = await _check_case_access(case_id, user_id, db, "write")
    if not case_data:
        return {"error": "案件不存在或无删除权限", "detail": "no delete permission"}

    conn, cur = db

    try:
        await cur.execute(
            "UPDATE case_documents SET is_deleted = 1, updated_at = NOW() "
            "WHERE id = %s AND case_id = %s AND is_deleted = 0",
            (doc_id, case_id),
        )

        await cur.execute(
            f"UPDATE cases SET document_count = {SQLHelper.greatest('COALESCE(document_count, 0) - 1', '0')}, updated_at = NOW() WHERE id = %s",
            (case_id,),
        )

        await _log_event(
            case_id, "case_updated", user_id, db,
            event_data={"action": "delete_document", "document_id": doc_id},
            operator_name=user.get("username", ""),
        )

        await conn.commit()
        return {"success": True, "message": "文书已删除"}
    except Exception as e:
        logger.error(f"删除文书失败: doc_id={doc_id}, case_id={case_id}, error={e}", exc_info=True)
        return {"error": f"删除文书失败: {str(e)}"}


@router.post("/{case_id}/documents/batch-download")
async def batch_download_documents(
    case_id: str,
    request: Request,
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    user_id = user.get("userId", "")
    case_data = await _check_case_access(case_id, user_id, db, "read")
    if not case_data:
        return {"error": "案件不存在或无权访问", "detail": "access denied"}

    body = await safe_json(request)
    document_ids = body.get("document_ids", [])
    if not document_ids:
        return {"error": "请选择要下载的文书", "detail": "no document_ids"}

    conn, cur = db
    placeholders = ", ".join(["%s"] * len(document_ids))
    await cur.execute(
        f"SELECT id, document_name, content_docx_path FROM case_documents "
        f"WHERE case_id = %s AND id IN ({placeholders}) AND is_deleted = 0",
        [case_id] + document_ids,
    )
    docs = [dict(row) for row in (await cur.fetchall())]

    if not docs:
        return {"error": "未找到可下载的文书", "detail": "no documents found"}

    zip_buffer = io.BytesIO()
    with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
        for doc in docs:
            docx_path = doc.get("content_docx_path", "")
            if docx_path and os.path.exists(docx_path):
                zf.write(docx_path, f"{doc['document_name']}.docx")
            else:
                md_path = docx_path.replace(".docx", ".md") if docx_path else ""
                content_md = doc.get("content_md_path", "")
                if content_md and os.path.exists(content_md):
                    zf.write(content_md, f"{doc['document_name']}.md")

    zip_buffer.seek(0)

    await _log_event(
        case_id, "document_downloaded", user_id, db,
        event_data={"action": "batch_download", "document_ids": document_ids, "count": len(docs)},
        operator_name=user.get("username", ""),
    )

    return StreamingResponse(
        zip_buffer,
        media_type="application/zip",
        headers={
            "Content-Disposition": f"attachment; filename=case_{case_id}_documents.zip"
        },
    )


@router.post("/{case_id}/export")
async def export_case(
    case_id: str,
    request: Request,
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    user_id = user.get("userId", "")
    case_data = await _check_case_access(case_id, user_id, db, "read")
    if not case_data:
        return {"error": "案件不存在或无权访问", "detail": "access denied"}

    conn, cur = db

    pool = await info_pool_service.get(case_id, db)

    await cur.execute(
        "SELECT id, template_id, template_name, document_name, version, status, "
        "generation_method, created_at FROM case_documents "
        "WHERE case_id = %s AND is_deleted = 0 ORDER BY created_at ASC",
        (case_id,),
    )
    documents = [dict(row) for row in (await cur.fetchall())]

    await cur.execute(
        "SELECT event_type, event_data, operator_name, created_at FROM case_logs "
        "WHERE case_id = %s ORDER BY created_at ASC",
        (case_id,),
    )
    logs = [dict(row) for row in (await cur.fetchall())]

    try:
        await cur.execute(
            "SELECT deerflow_thread_id, message_count, context_injected FROM case_threads WHERE case_id = %s",
            (case_id,),
        )
        thread_row = await cur.fetchone()
    except Exception:
        thread_row = None
    thread_info = dict(thread_row) if thread_row else None

    export_data = {
        "export_time": datetime.datetime.now().isoformat(),
        "case": desensitize_case(case_data),
        "basic_info_pool": pool,
        "documents": documents,
        "thread_info": thread_info,
        "logs": logs,
    }

    export_json = json.dumps(export_data, ensure_ascii=False, indent=2, default=str)

    zip_buffer = io.BytesIO()
    with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
        zf.writestr("case_data.json", export_json)

        for doc in documents:
            doc_id = doc["id"]
            await cur.execute(
                "SELECT content_md_path, content_docx_path FROM case_documents WHERE id = %s",
                (doc_id,),
            )
            doc_file_row = await cur.fetchone()
            if doc_file_row:
                dfr = dict(doc_file_row)
                md_path = dfr.get("content_md_path", "")
                docx_path = dfr.get("content_docx_path", "")
                if md_path and os.path.exists(md_path):
                    zf.write(md_path, f"documents/{doc['document_name']}.md")
                if docx_path and os.path.exists(docx_path):
                    zf.write(docx_path, f"documents/{doc['document_name']}.docx")

    zip_buffer.seek(0)

    await _log_event(
        case_id, "case_updated", user_id, db,
        event_data={"action": "export_case"},
        operator_name=user.get("username", ""),
    )

    return StreamingResponse(
        zip_buffer,
        media_type="application/zip",
        headers={
            "Content-Disposition": f"attachment; filename=case_{case_id}_export.zip"
        },
    )


@router.get("/{case_id}/memory")
async def get_case_memory(
    case_id: str,
    request: Request,
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    user_id = user.get("userId", "")
    case_data = await _check_case_access(case_id, user_id, db, "read")
    if not case_data:
        return {"error": "案件不存在或无权访问", "detail": "access denied"}

    memory = None  # case_memory_service removed

    if not memory:
        memory = {}

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


@router.put("/{case_id}/memory")
async def update_case_memory(
    case_id: str,
    request: Request,
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    user_id = user.get("userId", "")
    case_data = await _check_case_access(case_id, user_id, db, "write")
    if not case_data:
        return {"error": "案件不存在或无编辑权限", "detail": "no write permission"}

    body = await safe_json(request)
    content = body.get("content", "")
    content_summary = body.get("content_summary", "")

    if not content:
        return {"error": "记忆内容不能为空", "detail": "content required"}

    result = {"success": True, "message": "记忆功能暂未启用"}  # case_memory_service removed

    return result


@router.post("/{case_id}/memory/refresh")
async def refresh_case_memory(
    case_id: str,
    request: Request,
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    user_id = user.get("userId", "")
    case_data = await _check_case_access(case_id, user_id, db, "write")
    if not case_data:
        return {"error": "案件不存在或无操作权限", "detail": "no write permission"}

    result = {"success": True, "message": "记忆功能暂未启用"}  # case_memory_service removed

    return result


@router.post("/{case_id}/memory/clear")
async def clear_case_memory_endpoint(
    case_id: str,
    request: Request,
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    user_id = user.get("userId", "")
    case_data = await _check_case_access(case_id, user_id, db, "write")
    if not case_data:
        return {"error": "案件不存在或无操作权限", "detail": "no write permission"}

    result = {"success": True, "message": "记忆功能暂未启用"}  # case_memory_service removed
    return result


@router.post("/{case_id}/transfer")
async def transfer_case(
    case_id: str,
    request: Request,
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    user_id = user.get("userId", "")
    case_data = await _check_case_access(case_id, user_id, db, "write")
    if not case_data:
        return {"error": "案件不存在或无操作权限", "detail": "no write permission"}

    body = await safe_json(request)
    new_officer_id = body.get("new_officer_id")
    new_officer_name = body.get("new_officer_name")
    new_officer_badge = body.get("new_officer_badge", "")
    transfer_reason = body.get("transfer_reason", "")

    if not new_officer_id or not new_officer_name:
        return {"error": "缺少转交目标人员信息", "detail": "new_officer_id and new_officer_name required"}

    conn, cur = db

    old_main_officer = case_data.get("main_officer")
    if isinstance(old_main_officer, str):
        try:
            old_main_officer = json.loads(old_main_officer)
        except (json.JSONDecodeError, TypeError):
            old_main_officer = {}

    new_main_officer = {
        "user_id": new_officer_id,
        "name": new_officer_name,
        "badge_number": new_officer_badge,
    }

    new_assistant = old_main_officer if old_main_officer else None

    await cur.execute(
        "UPDATE cases SET main_officer = %s, assistant_officer = %s, "
        "transferred_from = %s, transferred_at = NOW(), transfer_reason = %s, "
        "updated_at = NOW() WHERE id = %s",
        (
            json.dumps(new_main_officer, ensure_ascii=False),
            json.dumps(new_assistant, ensure_ascii=False) if new_assistant else None,
            str(user_id),
            transfer_reason,
            case_id,
        ),
    )

    await cur.execute(
        "INSERT INTO case_logs (case_id, event_type, event_data, operator_id, operator_name, created_at) "
        "VALUES (%s, 'case_transferred', %s, %s, %s, NOW())",
        (
            case_id,
            json.dumps(
                {
                    "from_officer": old_main_officer,
                    "to_officer": new_main_officer,
                    "reason": transfer_reason,
                },
                ensure_ascii=False,
            ),
            str(user_id),
            user.get("username", ""),
        ),
    )

    try:
        pass  # case_memory_service removed
    except Exception as e:
        logger.warning(f"案件转交后更新记忆失败: {e}")

    return {
        "success": True,
        "message": "案件转交成功",
        "data": {
            "case_id": case_id,
            "new_main_officer": new_main_officer,
            "new_assistant_officer": new_assistant,
        },
    }


def _extract_keywords(text: str) -> list:
    """jieba 分词提取关键词，去除停用词，保留 ≥2 字的词。"""
    import jieba
    words = jieba.lcut(text)
    STOP_WORDS = {'涉嫌', '违法', '违反', '非法', '的', '了', '在', '和', '与', '对', '被', '已'}
    keywords = []
    for w in words:
        w = w.strip()
        if len(w) >= 2 and w not in STOP_WORDS:
            keywords.append(w)
    if not keywords and len(text) >= 2:
        keywords = [text]
    return keywords


def _search_penalty_cases_by_anyou(cause: str, limit: int = 20) -> list:
    """按案由模糊匹配知识库中的处罚案例（category='处罚案例'）。"""
    try:
        from .md_search import _scan_all_md_files
        all_docs = _scan_all_md_files()
    except Exception:
        return []

    penalty_docs = [d for d in all_docs if d.get('category') == '处罚案例']
    if not penalty_docs:
        return []

    keywords = _extract_keywords(cause)
    if not keywords:
        return []

    keywords_lower = [kw.lower() for kw in keywords]
    anyou_hits = {}

    for doc in penalty_docs:
        rel_path = doc['rel_path']
        anyou = doc.get('anyou', '')

        # ---- 第一轮：anyou 元数据关键词匹配 ----
        if anyou:
            anyou_text = ' '.join(str(a) for a in anyou).lower() if isinstance(anyou, list) else str(anyou).lower()
            hit_count = sum(1 for kw in keywords_lower if kw in anyou_text)
            if hit_count > 0:
                anyou_val = anyou if isinstance(anyou, list) else ([str(anyou)] if anyou else [])
                anyou_hits[rel_path] = ({
                    'id': rel_path.replace('/', '__').replace('\\', '__'),
                    'rel_path': rel_path,
                    'title': doc.get('title', doc['filename']),
                    'filename': doc['filename'],
                    'anyou': anyou_val,
                    'region': doc.get('region', ''),
                    'match_reason': f'案由匹配: {cause}',
                    'match_type': 'anyou',
                }, hit_count)
                continue

    anyou_sorted = sorted(anyou_hits.values(), key=lambda x: x[1], reverse=True)
    return [r for r, _ in anyou_sorted[:limit]]


async def _search_penalty_cases_by_anyou_db(cause: str, db, limit: int = 20) -> list:
    """按案由模糊匹配知识库处罚案例（数据库版，替代文件系统扫描）。
    注意：kb_documents 表在 system.db 中，此处自动获取正确的数据库连接。"""
    from ...core.database import get_system_db, _has_split_databases

    # 拆分库模式：kb_documents 在 system.db 中，必须用 system.db 连接
    # 单库兼容模式：所有表在同一库中，使用传入的连接
    if _has_split_databases():
        async for _conn, cur in get_system_db():
            return await _do_penalty_anyou_search(cur, cause, limit)
    else:
        conn, cur = db
        return await _do_penalty_anyou_search(cur, cause, limit)


async def _do_penalty_anyou_search(cur, cause: str, limit: int) -> list:
    """执行 anyou 案由模糊匹配查询的核心逻辑（复用，不关心连接来源）。"""
    keywords = _extract_keywords(cause)
    if not keywords:
        return []

    # ---- 第一轮：anyou 字段匹配（SQL LIKE 过滤） ----
    like_conditions = " OR ".join(["LOWER(anyou) LIKE %s"] * len(keywords))
    like_params = [f"%{kw}%" for kw in keywords]

    await cur.execute(
        f"SELECT file_path, title, region, anyou FROM kb_documents "
        f"WHERE category = '处罚案例' AND status = '有效' "
        f"AND anyou IS NOT NULL AND anyou != '[]' "
        f"AND ({like_conditions}) "
        f"LIMIT %s",
        like_params + [limit * 2],
    )
    anyou_rows = await cur.fetchall()

    anyou_hits = {}
    for row in anyou_rows:
        row = dict(row)
        file_path = row["file_path"]
        anyou_raw = row.get("anyou", "[]")
        if isinstance(anyou_raw, str):
            try:
                anyou_list = json.loads(anyou_raw)
            except (json.JSONDecodeError, TypeError):
                anyou_list = []
        else:
            anyou_list = anyou_raw if isinstance(anyou_raw, list) else []

        anyou_text = ' '.join(str(a) for a in anyou_list).lower()
        hit_count = sum(1 for kw in keywords if kw in anyou_text)
        if hit_count > 0:
            anyou_hits[file_path] = ({
                'id': file_path.replace('/', '__').replace('\\', '__'),
                'rel_path': file_path,
                'title': row.get('title', ''),
                'filename': os.path.basename(file_path),
                'anyou': anyou_list,
                'region': row.get('region', ''),
                'match_reason': f'案由匹配: {cause}',
                'match_type': 'anyou',
            }, hit_count)

    anyou_sorted = sorted(anyou_hits.values(), key=lambda x: x[1], reverse=True)
    return [r for r, _ in anyou_sorted[:limit]]


async def _search_penalty_cases_by_multi_causes_db(causes: list, db, limit_per_cause: int = 20) -> dict:
    """多案由分组检索（数据库版）。"""
    by_cause = []
    merged = []
    seen_paths = set()

    for cause_item in causes:
        cause_name = cause_item.get("cause_name", "").strip()
        cause_index = cause_item.get("cause_index", 0)
        if not cause_name:
            continue

        cases_list = await _search_penalty_cases_by_anyou_db(cause_name, db, limit=limit_per_cause)
        by_cause.append({
            "cause_name": cause_name,
            "cause_index": cause_index,
            "cases": cases_list,
            "total": len(cases_list),
        })

        for c in cases_list:
            if c["rel_path"] not in seen_paths:
                merged.append(c)
                seen_paths.add(c["rel_path"])

    return {"by_cause": by_cause, "merged": merged, "merged_total": len(merged)}


def _search_penalty_cases_by_multi_causes(causes: list, limit_per_cause: int = 20) -> dict:
    """多案由分组检索：每个案由独立调用 _search_penalty_cases_by_anyou，返回分组+合并结果。
    causes: [{"cause_name": "xxx", "cause_index": 1}, ...]
    返回: {"by_cause": [...], "merged": [...], "merged_total": N}"""
    by_cause = []
    merged = []
    seen_paths = set()

    for cause_item in causes:
        cause_name = cause_item.get("cause_name", "").strip()
        cause_index = cause_item.get("cause_index", 0)
        if not cause_name:
            continue

        cases_list = _search_penalty_cases_by_anyou(cause_name, limit=limit_per_cause)
        by_cause.append({
            "cause_name": cause_name,
            "cause_index": cause_index,
            "cases": cases_list,
            "total": len(cases_list),
        })

        # 合并去重
        for c in cases_list:
            rp = c.get("rel_path", "")
            if rp not in seen_paths:
                seen_paths.add(rp)
                merged.append(c)

    return {
        "by_cause": by_cause,
        "merged": merged,
        "merged_total": len(merged),
    }


async def _compute_related_data(case_data: dict, causes_list: list = None, db=None):
    """纯计算函数：根据案件数据计算关联案件和类案推荐结果。
    causes_list: [{"cause_name": "xxx", "cause_index": 1}, ...] 可选，由调用方从 case_causes_detail 查询传入。
    返回 (related_cases, match_fields, similar_penalty_cases) 元组。"""
    party_name = (case_data.get("party_name") or "").strip()
    legal_rep = (case_data.get("legal_representative") or "").strip()
    cause = (case_data.get("cause") or "").strip()

    operator_name = ""
    party_info = case_data.get("party_info")
    if isinstance(party_info, str) and party_info:
        try:
            party_info = json.loads(party_info)
        except (json.JSONDecodeError, TypeError):
            party_info = {}
    if isinstance(party_info, dict):
        operator_name = (party_info.get("operator_name") or "").strip()

    # 构建匹配快照（用于后续判断是否需要刷新）
    snapshot = {
        "party_name": party_name,
        "legal_representative": legal_rep,
        "operator_name": operator_name,
        "cause": cause,
    }

    # 关联案件查询条件：基础条件 AND (关键字段任一匹配)
    base_conditions = ["c.is_deleted = 0", "c.id != %s"]
    params: list = [case_data["id"]]
    match_conditions = []

    match_fields = []
    if party_name:
        match_conditions.append("c.party_name = %s")
        params.append(party_name)
        match_fields.append("当事人名称")
    if legal_rep:
        match_conditions.append("c.legal_representative = %s")
        params.append(legal_rep)
        match_fields.append("法定代表人")
    if operator_name:
        match_conditions.append("(JSON_EXTRACT(c.party_info, '$.operator_name') = %s)")
        params.append(operator_name)
        match_fields.append("经营者姓名")

    if match_conditions:
        where = f"WHERE {' AND '.join(base_conditions)} AND ({' OR '.join(match_conditions)})"
    else:
        where = f"WHERE {' AND '.join(base_conditions)} AND 1 = 0"

    # 类案推荐：支持多案由分组检索
    similar_penalty_cases = []
    similar_penalty_by_cause = []

    try:
        if causes_list and len(causes_list) > 0:
            # 多案由分组检索
            if db:
                multi_result = await _search_penalty_cases_by_multi_causes_db(causes_list, db, limit_per_cause=20)
            else:
                multi_result = _search_penalty_cases_by_multi_causes(causes_list, limit_per_cause=20)
            similar_penalty_cases = multi_result["merged"]
            similar_penalty_by_cause = multi_result["by_cause"]
        elif cause:
            # 单案由兜底
            if db:
                similar_penalty_cases = await _search_penalty_cases_by_anyou_db(cause, db)
            else:
                similar_penalty_cases = _search_penalty_cases_by_anyou(cause)
    except Exception as e:
        # 知识库表不存在或无数据时，类案推荐返回空列表，不影响关联案件查询
        import logging
        logger = logging.getLogger(__name__)
        logger.warning(f"类案推荐查询异常（可能知识库未导入数据）: {e}")

    return {
        "sql_where": where,
        "sql_params": params,
        "match_fields": match_fields,
        "snapshot": snapshot,
        "similar_penalty_cases": similar_penalty_cases,
        "similar_penalty_by_cause": similar_penalty_by_cause,
    }


async def _refresh_related_cache(case_id: str, db):
    """计算关联/类案推荐结果，写入 case_related_cache 表。
    在 update_case 保存成功后调用。"""
    conn, cur = db
    try:
        await cur.execute("SELECT * FROM cases WHERE id = %s AND is_deleted = 0", (case_id,))
        case_row = await cur.fetchone()
        if not case_row:
            return
        case_data = dict(case_row)

        # 查询多案由列表
        causes_list = []
        try:
            await cur.execute(
                "SELECT cause_name, sort_order FROM case_causes_detail WHERE case_id = %s ORDER BY sort_order",
                (case_id,)
            )
            causes_rows = await cur.fetchall()
            causes_list = [{"cause_name": r["cause_name"], "cause_index": r["sort_order"]} for r in causes_rows]
        except Exception:
            pass

        computed = await _compute_related_data(case_data, causes_list=causes_list, db=db)

        # 查询关联案件
        await cur.execute(
            f"SELECT c.id, c.case_number, c.title, c.cause, c.party_name, "
            f"c.legal_representative, c.party_info, c.status, c.filing_date, c.created_at, c.updated_at "
            f"FROM cases c {computed['sql_where']} ORDER BY c.updated_at DESC LIMIT 50",
            computed["sql_params"],
        )
        rows = [dict(row) for row in (await cur.fetchall())]

        # 补充 match_reasons
        snapshot = computed["snapshot"]
        for row in rows:
            row["match_reasons"] = []
            if snapshot["party_name"] and row.get("party_name") == snapshot["party_name"]:
                row["match_reasons"].append(f"当事人名称: {snapshot['party_name']}")
            if snapshot["legal_representative"] and row.get("legal_representative") == snapshot["legal_representative"]:
                row["match_reasons"].append(f"法定代表人: {snapshot['legal_representative']}")
            if snapshot["operator_name"]:
                pi = row.get("party_info")
                if isinstance(pi, str) and pi:
                    try:
                        pi = json.loads(pi)
                    except Exception:
                        pi = {}
                if isinstance(pi, dict) and pi.get("operator_name") == snapshot["operator_name"]:
                    row["match_reasons"].append(f"经营者姓名: {snapshot['operator_name']}")

        # UPSERT 到缓存表（含多案由分组数据）
        await cur.execute("""
            INSERT INTO case_related_cache (case_id, related_cases, similar_penalty_cases, match_snapshot, updated_at)
            VALUES (%s, %s, %s, %s, NOW())
            ON CONFLICT(case_id) DO UPDATE SET
                related_cases = EXCLUDED.related_cases,
                similar_penalty_cases = EXCLUDED.similar_penalty_cases,
                match_snapshot = EXCLUDED.match_snapshot,
                updated_at = NOW()
        """, (
            case_id,
            json.dumps(rows, ensure_ascii=False),
            json.dumps({
                "merged": computed["similar_penalty_cases"],
                "by_cause": computed.get("similar_penalty_by_cause", []),
            }, ensure_ascii=False),
            json.dumps(computed["snapshot"], ensure_ascii=False),
        ))
    except Exception as e:
        import logging
        logging.getLogger(__name__).warning(f"刷新关联/类案缓存失败 case_id={case_id}: {e}")


# 正在计算类案推荐的 case_id 集合（内存级，进程重启后自动清空）
_computing_related: set = set()


async def _async_refresh_related_cache(case_id: str):
    """异步刷新缓存（独立数据库连接，不阻塞请求响应）。"""
    _computing_related.add(case_id)
    try:
        async for conn, cur in get_system_db():
            await _refresh_related_cache(case_id, (conn, cur))
            break
    except Exception as e:
        import logging
        logging.getLogger(__name__).warning(f"异步刷新关联/类案缓存失败 case_id={case_id}: {e}")
    finally:
        _computing_related.discard(case_id)


@router.get("/{case_id}/related")
async def get_related_cases(
    case_id: str,
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    """读取关联/类案推荐：优先从缓存表读取，无缓存则实时计算并缓存。"""
    conn, cur = db
    user_id = user.get("userId", "")

    case_data = await _check_case_access(case_id, user_id, db, "read")
    if not case_data:
        return {"error": "案件不存在或无权访问", "detail": "case not found or access denied"}

    # ===== 1. 尝试从缓存表读取 =====
    await cur.execute(
        "SELECT related_cases, similar_penalty_cases, match_snapshot FROM case_related_cache WHERE case_id = %s",
        (case_id,),
    )
    cache_row = await cur.fetchone()

    if cache_row:
        related_cases = json.loads(cache_row["related_cases"] or "[]")
        similar_raw = json.loads(cache_row["similar_penalty_cases"] or "[]")

        # 兼容新旧缓存格式：新格式 {"merged":[], "by_cause":[]}，旧格式 []
        if isinstance(similar_raw, dict):
            similar_penalty_cases = similar_raw.get("merged", [])
            similar_penalty_by_cause = similar_raw.get("by_cause", [])
        else:
            similar_penalty_cases = similar_raw
            similar_penalty_by_cause = []

        return {
            "success": True,
            "data": {
                "related_cases": related_cases,
                "total": len(related_cases),
                "match_fields": [label for k, label in {
                    "party_name": "当事人名称",
                    "legal_representative": "法定代表人",
                    "operator_name": "经营者姓名",
                }.items() if json.loads(cache_row["match_snapshot"] or "{}").get(k)],
                "similar_penalty_cases": similar_penalty_cases,
                "similar_penalty_total": len(similar_penalty_cases),
                "similar_penalty_by_cause": similar_penalty_by_cause,
            },
        }

    # ===== 2. 无缓存 → 检查是否正在后台计算 =====
    if case_id in _computing_related:
        return {
            "success": True,
            "computing": True,
            "data": {
                "related_cases": [],
                "total": 0,
                "match_fields": [],
                "similar_penalty_cases": [],
                "similar_penalty_total": 0,
                "similar_penalty_by_cause": [],
            },
        }

    # ===== 3. 无缓存且无后台任务 → 实时计算并写入缓存（兜底） =====
    # 查询多案由列表
    causes_list = []
    try:
        await cur.execute(
            "SELECT cause_name, sort_order FROM case_causes_detail WHERE case_id = %s ORDER BY sort_order",
            (case_id,)
        )
        causes_rows = await cur.fetchall()
        causes_list = [{"cause_name": r["cause_name"], "cause_index": r["sort_order"]} for r in causes_rows]
    except Exception:
        pass

    computed = await _compute_related_data(case_data, causes_list=causes_list, db=db)

    # 查询关联案件
    await cur.execute(
        f"SELECT c.id, c.case_number, c.title, c.cause, c.party_name, "
        f"c.legal_representative, c.party_info, c.status, c.filing_date, c.created_at, c.updated_at "
        f"FROM cases c {computed['sql_where']} ORDER BY c.updated_at DESC LIMIT 50",
        computed["sql_params"],
    )
    rows = [dict(row) for row in (await cur.fetchall())]

    snapshot = computed["snapshot"]
    for row in rows:
        row["match_reasons"] = []
        if snapshot["party_name"] and row.get("party_name") == snapshot["party_name"]:
            row["match_reasons"].append(f"当事人名称: {snapshot['party_name']}")
        if snapshot["legal_representative"] and row.get("legal_representative") == snapshot["legal_representative"]:
            row["match_reasons"].append(f"法定代表人: {snapshot['legal_representative']}")
        if snapshot["operator_name"]:
            pi = row.get("party_info")
            if isinstance(pi, str) and pi:
                try:
                    pi = json.loads(pi)
                except Exception:
                    pi = {}
            if isinstance(pi, dict) and pi.get("operator_name") == snapshot["operator_name"]:
                row["match_reasons"].append(f"经营者姓名: {snapshot['operator_name']}")

    similar_penalty_cases = computed["similar_penalty_cases"]
    similar_penalty_by_cause = computed.get("similar_penalty_by_cause", [])

    # 异步写入缓存（不阻塞响应）
    try:
        await cur.execute("""
            INSERT INTO case_related_cache (case_id, related_cases, similar_penalty_cases, match_snapshot, updated_at)
            VALUES (%s, %s, %s, %s, NOW())
            ON CONFLICT(case_id) DO UPDATE SET
                related_cases = EXCLUDED.related_cases,
                similar_penalty_cases = EXCLUDED.similar_penalty_cases,
                match_snapshot = EXCLUDED.match_snapshot,
                updated_at = NOW()
        """, (
            case_id,
            json.dumps(rows, ensure_ascii=False),
            json.dumps({
                "merged": similar_penalty_cases,
                "by_cause": similar_penalty_by_cause,
            }, ensure_ascii=False),
            json.dumps(snapshot, ensure_ascii=False),
        ))
    except Exception:
        pass  # 缓存写入失败不影响返回

    return {
        "success": True,
        "data": {
            "related_cases": rows,
            "total": len(rows),
            "match_fields": computed["match_fields"],
            "similar_penalty_cases": similar_penalty_cases,
            "similar_penalty_total": len(similar_penalty_cases),
            "similar_penalty_by_cause": similar_penalty_by_cause,
        },
    }


# ===== V2: 多案由CRUD API =====

class CauseDetailCreate(BaseModel):
    cause_name: str
    cause_code: Optional[str] = None
    level1_code: Optional[str] = None
    level1_name: Optional[str] = None
    level2_code: Optional[str] = None
    level2_name: Optional[str] = None
    cause_source: Optional[str] = "user_custom"
    ai_confidence: Optional[str] = None
    sort_order: Optional[int] = 0
    violation_laws: Optional[List[dict]] = []
    penalty_laws: Optional[List[dict]] = []


class CauseDetailUpdate(BaseModel):
    cause_name: Optional[str] = None
    cause_code: Optional[str] = None
    level1_code: Optional[str] = None
    level1_name: Optional[str] = None
    level2_code: Optional[str] = None
    level2_name: Optional[str] = None
    sort_order: Optional[int] = None
    violation_laws: Optional[List[dict]] = []
    penalty_laws: Optional[List[dict]] = []


@router.get("/{case_id}/causes")
async def get_case_causes(
    case_id: str,
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    """获取案件的多案由列表（含结构化法条）"""
    user_id = user.get("userId", "")
    case_data = await _check_case_access(case_id, user_id, db, "read")
    if not case_data:
        return {"error": "案件不存在或无权访问", "detail": "case not found or access denied"}

    conn, cur = db

    await cur.execute(
        """SELECT id, cause_name, cause_code, level1_code, level1_name,
                  level2_code, level2_name, cause_source, ai_confidence, sort_order
           FROM case_causes_detail
           WHERE case_id = %s
           ORDER BY sort_order ASC, id ASC""",
        (case_id,),
    )
    cause_details = [dict(row) for row in (await cur.fetchall())]

    for cause in cause_details:
        cause_id_inner = cause["id"]
        await cur.execute(
            """SELECT id, law_type, law_name, article_number, article_full_ref,
                      content_summary, content_detail, source, source_id, sort_order
               FROM case_cause_laws
               WHERE cause_detail_id = %s
               ORDER BY law_type, sort_order ASC, id ASC""",
            (cause_id_inner,),
        )
        laws = [dict(row) for row in (await cur.fetchall())]
        cause["violation_laws"] = [l for l in laws if l["law_type"] == "violation"]
        cause["penalty_laws"] = [l for l in laws if l["law_type"] == "penalty"]

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


@router.post("/{case_id}/causes")
async def create_case_cause(
    case_id: str,
    cause: CauseDetailCreate,
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    """为案件添加案由（含法条）"""
    user_id = user.get("userId", "")
    case_data = await _check_case_access(case_id, user_id, db, "write")
    if not case_data:
        return {"error": "案件不存在或无编辑权限", "detail": "case not found or no write permission"}

    conn, cur = db

    # 插入案由
    await cur.execute(
        """INSERT INTO case_causes_detail
           (case_id, cause_name, cause_code, level1_code, level1_name,
            level2_code, level2_name, cause_source, ai_confidence, sort_order)
           VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)""",
        (case_id, cause.cause_name, cause.cause_code, cause.level1_code,
         cause.level1_name, cause.level2_code, cause.level2_name,
         cause.cause_source, cause.ai_confidence, cause.sort_order),
    )
    cause_detail_id = cur.lastrowid

    # 插入违法法条
    for law in (cause.violation_laws or []):
        await cur.execute(
            """INSERT INTO case_cause_laws
               (cause_detail_id, case_id, law_type, law_name, article_number,
                article_full_ref, content_summary, content_detail, source, source_id, sort_order)
               VALUES (%s, %s, 'violation', %s, %s, %s, %s, %s, %s, %s, %s)""",
            (cause_detail_id, case_id, law.get("law_name", ""), law.get("article_number", ""),
             law.get("article_full_ref", ""), law.get("content_summary", ""),
             _clean_content_detail(law.get("content_detail", "")), law.get("source", "manual"),
             law.get("source_id", ""), law.get("sort_order", 0)),
        )

    # 插入处罚法条
    for law in (cause.penalty_laws or []):
        await cur.execute(
            """INSERT INTO case_cause_laws
               (cause_detail_id, case_id, law_type, law_name, article_number,
                article_full_ref, content_summary, content_detail, source, source_id, sort_order)
               VALUES (%s, %s, 'penalty', %s, %s, %s, %s, %s, %s, %s, %s)""",
            (cause_detail_id, case_id, law.get("law_name", ""), law.get("article_number", ""),
             law.get("article_full_ref", ""), law.get("content_summary", ""),
             _clean_content_detail(law.get("content_detail", "")), law.get("source", "manual"),
             law.get("source_id", ""), law.get("sort_order", 0)),
        )

    # 更新cases表聚合字段
    await _update_case_cause_summary(case_id, cur)

    return {
        "success": True,
        "message": "案由添加成功",
        "data": {"cause_detail_id": cause_detail_id},
    }


@router.put("/{case_id}/causes/{cause_detail_id}")
async def update_case_cause(
    case_id: str,
    cause_detail_id: str,
    cause: CauseDetailUpdate,
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    """更新案由（含法条）"""
    user_id = user.get("userId", "")
    case_data = await _check_case_access(case_id, user_id, db, "write")
    if not case_data:
        return {"error": "案件不存在或无编辑权限", "detail": "case not found or no write permission"}

    conn, cur = db

    # 更新案由
    updates = []
    values = []
    if cause.cause_name is not None:
        updates.append("cause_name = %s")
        values.append(cause.cause_name)
    if cause.cause_code is not None:
        updates.append("cause_code = %s")
        values.append(cause.cause_code)
    if cause.level1_code is not None:
        updates.append("level1_code = %s")
        values.append(cause.level1_code)
    if cause.level1_name is not None:
        updates.append("level1_name = %s")
        values.append(cause.level1_name)
    if cause.level2_code is not None:
        updates.append("level2_code = %s")
        values.append(cause.level2_code)
    if cause.level2_name is not None:
        updates.append("level2_name = %s")
        values.append(cause.level2_name)
    if cause.sort_order is not None:
        updates.append("sort_order = %s")
        values.append(cause.sort_order)

    if updates:
        values.extend([cause_detail_id, case_id])
        await cur.execute(
            f"UPDATE case_causes_detail SET {', '.join(updates)} WHERE id = %s AND case_id = %s",
            values,
        )

    # 如果有法条更新，先删除旧法条再插入新法条
    if cause.violation_laws is not None or cause.penalty_laws is not None:
        await cur.execute(
            "DELETE FROM case_cause_laws WHERE cause_detail_id = %s",
            (cause_detail_id,),
        )

        for law in (cause.violation_laws or []):
            await cur.execute(
                """INSERT INTO case_cause_laws
                   (cause_detail_id, case_id, law_type, law_name, article_number,
                    article_full_ref, content_summary, content_detail, source, source_id, sort_order)
                   VALUES (%s, %s, 'violation', %s, %s, %s, %s, %s, %s, %s, %s)""",
                (cause_detail_id, case_id, law.get("law_name", ""), law.get("article_number", ""),
                 law.get("article_full_ref", ""), law.get("content_summary", ""),
                 _clean_content_detail(law.get("content_detail", "")), law.get("source", "manual"),
                 law.get("source_id", ""), law.get("sort_order", 0)),
            )

        for law in (cause.penalty_laws or []):
            await cur.execute(
                """INSERT INTO case_cause_laws
                   (cause_detail_id, case_id, law_type, law_name, article_number,
                    article_full_ref, content_summary, content_detail, source, source_id, sort_order)
                   VALUES (%s, %s, 'penalty', %s, %s, %s, %s, %s, %s, %s, %s)""",
                (cause_detail_id, case_id, law.get("law_name", ""), law.get("article_number", ""),
                 law.get("article_full_ref", ""), law.get("content_summary", ""),
                 _clean_content_detail(law.get("content_detail", "")), law.get("source", "manual"),
                 law.get("source_id", ""), law.get("sort_order", 0)),
            )

    # 更新cases表聚合字段
    await _update_case_cause_summary(case_id, cur)

    return {
        "success": True,
        "message": "案由更新成功",
    }


@router.delete("/{case_id}/causes/{cause_detail_id}")
async def delete_case_cause(
    case_id: str,
    cause_detail_id: str,
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    """删除案由（级联删除法条）"""
    user_id = user.get("userId", "")
    case_data = await _check_case_access(case_id, user_id, db, "write")
    if not case_data:
        return {"error": "案件不存在或无编辑权限", "detail": "case not found or no write permission"}

    conn, cur = db

    # 删除法条（级联由数据库处理，但应用层也做双重保障）
    await cur.execute(
        "DELETE FROM case_cause_laws WHERE cause_detail_id = %s",
        (cause_detail_id,),
    )

    # 删除案由
    await cur.execute(
        "DELETE FROM case_causes_detail WHERE id = %s AND case_id = %s",
        (cause_detail_id, case_id),
    )

    # 更新cases表聚合字段
    await _update_case_cause_summary(case_id, cur)

    return {
        "success": True,
        "message": "案由删除成功",
    }


async def _update_case_cause_summary(case_id: str, cur):
    """更新cases表的案由聚合字段（供文书生成使用）"""
    await cur.execute(
        """SELECT cause_name FROM case_causes_detail
           WHERE case_id = %s ORDER BY sort_order ASC, id ASC""",
        (case_id,),
    )
    # 去重：保留首次出现的名称，保持原有顺序
    seen = set()
    cause_names = []
    for row in (await cur.fetchall()):
        name = row["cause_name"]
        if name and name not in seen:
            cause_names.append(name)
            seen.add(name)

    await cur.execute(
        """SELECT article_full_ref FROM case_cause_laws
           WHERE case_id = %s AND law_type = 'violation'
           ORDER BY sort_order ASC, id ASC""",
        (case_id,),
    )
    violation_refs = [row["article_full_ref"] for row in (await cur.fetchall())]

    await cur.execute(
        """SELECT article_full_ref FROM case_cause_laws
           WHERE case_id = %s AND law_type = 'penalty'
           ORDER BY sort_order ASC, id ASC""",
        (case_id,),
    )
    penalty_refs = [row["article_full_ref"] for row in (await cur.fetchall())]

    causes_merged = "、".join(cause_names) if cause_names else None
    violation_summary = "、".join(violation_refs) if violation_refs else None
    penalty_summary = "、".join(penalty_refs) if penalty_refs else None

    await cur.execute(
        """UPDATE cases SET
           cause_count = %s,
           cause_confirmed = %s,
           causes_merged = %s,
           violation_laws_summary = %s,
           penalty_laws_summary = %s,
           updated_at = NOW()
           WHERE id = %s""",
        (len(cause_names), 1 if cause_names else 0,
         causes_merged, violation_summary, penalty_summary, case_id),
    )


# ===== V2: 案由搜索API =====

@router.get("/causes/search")
async def search_case_causes(
    q: str = Query(..., description="搜索关键词"),
    limit: int = Query(10, ge=1, le=50, description="返回数量"),
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    """从case_causes标准库搜索案由"""
    conn, cur = db

    await cur.execute(
        """SELECT cause_code, cause_name, cause_name_short,
                  level1_code, level1_name, level2_code, level2_name,
                  violation_law, penalty_law, remark
           FROM case_causes
           WHERE cause_name LIKE %s AND is_enabled = 1
           ORDER BY cause_code ASC
           LIMIT %s""",
        (f"%{q}%", limit),
    )
    results = [dict(row) for row in (await cur.fetchall())]

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


# ===== 处罚裁量 API =====

@router.get("/{case_id}/discretion")
async def get_case_discretion(
    case_id: str,
    request: Request,
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    conn, cur = db
    await cur.execute(
        "SELECT discretion_advice FROM cases WHERE id = %s AND is_deleted = 0",
        (case_id,),
    )
    row = await cur.fetchone()
    if not row:
        raise HTTPException(status_code=404, detail="案件不存在")

    advice = row["discretion_advice"]
    if isinstance(advice, str):
        try:
            advice = json.loads(advice)
        except (json.JSONDecodeError, TypeError):
            advice = None

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


class DiscretionSaveBody(BaseModel):
    discretion_advice: dict


@router.put("/{case_id}/discretion")
async def save_case_discretion(
    case_id: str,
    body: DiscretionSaveBody,
    request: Request,
    user: dict = Depends(require_login),
    db=Depends(get_db_write),
):
    conn, cur = db
    await cur.execute(
        "SELECT id FROM cases WHERE id = %s AND is_deleted = 0",
        (case_id,),
    )
    if not await cur.fetchone():
        raise HTTPException(status_code=404, detail="案件不存在")

    advice_json = json.dumps(body.discretion_advice, ensure_ascii=False)
    await cur.execute(
        "UPDATE cases SET discretion_advice = %s, updated_at = NOW() WHERE id = %s",
        (advice_json, case_id),
    )
    await conn.commit()

    return {"success": True, "data": body.discretion_advice}


@router.delete("/{case_id}/discretion")
async def delete_case_discretion(
    case_id: str,
    request: Request,
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    conn, cur = db
    await cur.execute(
        "SELECT id FROM cases WHERE id = %s AND is_deleted = 0",
        (case_id,),
    )
    if not await cur.fetchone():
        raise HTTPException(status_code=404, detail="案件不存在")

    await cur.execute(
        "UPDATE cases SET discretion_advice = NULL, updated_at = NOW() WHERE id = %s",
        (case_id,),
    )
    await conn.commit()

    return {"success": True}


# ==================== 案件文书 OnlyOffice 编辑器接口 ====================

def _generate_case_doc_jwt(editor_config: dict) -> str:
    """生成 OnlyOffice JWT token"""
    import jwt as pyjwt
    _fallback = bytes([122,104,105,102,97,122,104,117,115,104,111,117,95,115,101,99,114,101,116,95,50,48,50,54]).decode('ascii')
    secret = settings.ONLYOFFICE_JWT_SECRET or _fallback
    payload = {}
    if "document" in editor_config:
        doc = {}
        src_doc = editor_config["document"]
        for key in ("key", "url", "permissions", "fileType", "title"):
            if key in src_doc:
                doc[key] = src_doc[key]
        payload["document"] = doc
    if "editorConfig" in editor_config:
        ec = {}
        src_ec = editor_config["editorConfig"]
        for key in ("callbackUrl", "mode", "lang", "user", "customization"):
            if key in src_ec:
                ec[key] = src_ec[key]
        payload["editorConfig"] = ec
    return pyjwt.encode(payload, secret, algorithm="HS256")


@router.get("/{case_id}/documents/{doc_id}/public-file")
@router.head("/{case_id}/documents/{doc_id}/public-file")
async def get_case_document_public_file(
    case_id: str,
    doc_id: int,
    request: Request,
    db=Depends(get_db),
):
    """公开文件下载接口（供 OnlyOffice Document Server 下载文件，无需认证）"""
    conn, cur = db

    await cur.execute(
        "SELECT document_name, content_docx_path FROM case_documents "
        "WHERE id = %s AND case_id = %s AND is_deleted = 0",
        (doc_id, case_id),
    )
    row = await cur.fetchone()
    if not row:
        from fastapi.responses import Response
        return Response(status_code=404) if request.method == "HEAD" else {"error": "文书不存在"}

    doc = dict(row)
    file_path = doc["content_docx_path"]
    if not file_path or not os.path.exists(file_path):
        from fastapi.responses import Response
        return Response(status_code=404) if request.method == "HEAD" else {"error": "文件不存在"}

    content_type = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
    file_size = os.path.getsize(file_path)
    safe_filename = doc["document_name"] or f"document_{doc_id}"
    if not safe_filename.lower().endswith('.docx'):
        safe_filename += '.docx'

    if request.method == "HEAD":
        from fastapi.responses import Response
        from urllib.parse import quote
        return Response(
            status_code=200,
            media_type=content_type,
            headers={
                'Content-Length': str(file_size),
                'Content-Disposition': f"attachment; filename=\"{quote(safe_filename)}\"; filename*=UTF-8''{quote(safe_filename)}",
                'Accept-Ranges': 'bytes',
                'Cache-Control': 'public, max-age=3600',
            },
        )

    return FileResponse(file_path, media_type=content_type, filename=safe_filename)


@router.get("/{case_id}/documents/{doc_id}/editor-config")
async def get_case_document_editor_config(
    case_id: str,
    doc_id: int,
    request: Request,
    mode: str = Query(default="view", description="打开模式: view 或 edit"),
    user: dict = Depends(require_login),
    db=Depends(get_db),
):
    """生成案件文书 OnlyOffice 编辑器配置（含并发控制）"""
    import time
    conn, cur = db
    user_id = user.get("userId", "")

    # 验证案件访问权限
    case_data = await _check_case_access(case_id, user_id, db, "read")
    if not case_data:
        return {"error": "案件不存在或无访问权限", "detail": "no access"}

    await cur.execute(
        "SELECT id, document_name, content_docx_path, template_id, template_name, version "
        "FROM case_documents WHERE id = %s AND case_id = %s AND is_deleted = 0",
        (doc_id, case_id),
    )
    row = await cur.fetchone()
    if not row:
        return {"error": "文书不存在", "detail": "document not found"}

    doc = dict(row)
    file_path = doc["content_docx_path"]
    if not file_path or not os.path.exists(file_path):
        return {"error": "文件不存在", "detail": "file not found on disk"}

    document_key = f"case-doc-{doc_id}-{int(time.time() / 3600)}"

    # 构建外部可访问的 server_url（与 my-documents 保持一致）
    # 检测顺序：环境变量 → X-Forwarded头(nginx代理) → request.base_url → localhost兜底
    server_url = getattr(settings, 'NEXT_PUBLIC_API_URL', None)
    if not server_url:
        # 尝试从nginx代理头获取真实外部地址（解决JWT URL不匹配问题）
        forwarded_host = request.headers.get("X-Forwarded-Host") or request.headers.get("x-forwarded-host")
        forwarded_proto = request.headers.get("X-Forwarded-Proto") or request.headers.get("x-forwarded-proto", "http")
        if forwarded_host:
            server_url = f"{forwarded_proto}://{forwarded_host}"
        else:
            try:
                server_url = str(request.base_url)
                if server_url.endswith('/'):
                    server_url = server_url[:-1]
            except:
                server_url = f"http://127.0.0.1:{settings.BFF_API_PORT}"

    file_url = f"{server_url}/api/user/cases/{case_id}/documents/{doc_id}/public-file"
    callback_url = f"{server_url}/api/user/cases/{case_id}/documents/{doc_id}/callback"

    file_name = doc["document_name"] or f"document_{doc_id}.docx"
    file_ext = "docx"
    document_type = "word"

    # 根据模式参数和并发情况决定实际模式
    actual_mode = mode
    notice = ""

    if mode == "edit":
        session_key = _get_session_key(doc_id, user_id)
        now = time.time()
        has_session = session_key in _active_edit_sessions and (now - _active_edit_sessions[session_key]) < EDITOR_IDLE_TIMEOUT

        if not has_session:
            active_count = _get_active_edit_count()
            if active_count >= MAX_CONCURRENT_EDITS:
                actual_mode = "view"
                notice = "并发编辑人数过多，请稍后再试。当前已切换为预览模式。"
                logger.info(f"[case-doc] Edit rejected user={user_id} doc={doc_id}: concurrent limit ({active_count})")
            else:
                _record_edit_session(doc_id, user_id)
                logger.info(f"[case-doc] Edit granted user={user_id} doc={doc_id}: active={active_count + 1}")
        else:
            _record_edit_session(doc_id, user_id)
            logger.info(f"[case-doc] Edit continued user={user_id} doc={doc_id}")

    config = {
        "onlyofficeUrl": "/onlyoffice",
        "documentType": document_type,
        "type": "desktop",
        "document": {
            "fileType": file_ext,
            "key": document_key,
            "url": file_url,
            "title": file_name,
            "permissions": {
                "comment": actual_mode == "edit",
                "download": True,
                "edit": actual_mode == "edit",
                "print": True,
                "review": actual_mode == "edit",
            },
        },
        "editorConfig": {
            "mode": actual_mode,
            "lang": "zh-CN",
            "callbackUrl": callback_url,
            "user": {
                "id": str(user_id),
                "name": user.get("username", "用户"),
            },
        },
    }

    if notice:
        config["_notice"] = notice

    if settings.ONLYOFFICE_JWT_ENABLED and settings.ONLYOFFICE_JWT_SECRET:
        try:
            token = _generate_case_doc_jwt(config)
            config["token"] = token
        except Exception as e:
            logger.error(f"JWT 编码错误: {e}")

    return config


@router.post("/{case_id}/documents/{doc_id}/callback")
async def case_document_callback(
    case_id: str,
    doc_id: int,
    request: Request,
    db=Depends(get_db),
):
    """OnlyOffice 回调处理：保存编辑后的文书"""
    conn, cur = db
    body = await safe_json(request)
    status = body.get("status")

    if status == 2:
        url = body.get("url")
        if url:
            try:
                async with aiohttp.ClientSession() as session:
                    async with session.get(url, timeout=aiohttp.ClientTimeout(total=60)) as resp:
                        if resp.status == 200:
                            file_data = await resp.read()
                            await cur.execute(
                                "SELECT content_docx_path FROM case_documents WHERE id = %s AND case_id = %s AND is_deleted = 0",
                                (doc_id, case_id),
                            )
                            row = await cur.fetchone()
                            if row and dict(row).get("content_docx_path"):
                                file_path = row["content_docx_path"]
                                os.makedirs(os.path.dirname(file_path), exist_ok=True)
                                with open(file_path, "wb") as f:
                                    f.write(file_data)
                                await cur.execute(
                                    "UPDATE case_documents SET status = 'edited', updated_at = NOW() WHERE id = %s",
                                    (doc_id,),
                                )
                                await conn.commit()
            except Exception as e:
                logger.error(f"OnlyOffice callback download error for case doc {doc_id}: {e}")
    elif status == 6:
        await cur.execute(
            "UPDATE case_documents SET status = 'editing', updated_at = NOW() WHERE id = %s",
            (doc_id,),
        )
        await conn.commit()

    return {"error": 0}
