#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Markdown 知识库文本搜索 API
直接扫描 /zhishiku/ 目录的 .md 文件，支持：
1. 标题模糊搜索
2. 全文模糊搜索
3. 法条定位搜索（支持中文数字 / 阿拉伯数字）
"""

import os
import re
import time
import json
import logging
import jieba
jieba.setLogLevel(logging.WARNING)  # 抑制 jieba 初始化日志
from fastapi import APIRouter, Depends, Query, Request
from typing import List, Optional, Dict, Any

from ...core.permission import require_login
from ...core.config import settings
from ...core.database import get_db, get_system_db, _has_split_databases
from ...core.crypto import read_text_file, is_encrypted
from ...utils.request_utils import safe_json

logger = logging.getLogger(__name__)

# ==================== jieba 分词配置 ====================

# 加载法律领域自定义词典（防止专业术语被过度切分）
# md_search.py 在 app/api/user/，词典在 app/data/，需往上跳两层
_DICT_PATH = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'legal_dict.txt')
_DICT_PATH_ABS = os.path.abspath(_DICT_PATH)
if os.path.exists(_DICT_PATH_ABS):
    jieba.load_userdict(_DICT_PATH_ABS)
    with open(_DICT_PATH_ABS, 'r', encoding='utf-8') as _f:
        _dict_lines = len([l for l in _f if l.strip()])
    logger.info(f"已加载法律术语词典: {_DICT_PATH_ABS} ({_dict_lines} 条)")
else:
    logger.warning(f"法律术语词典未找到: {_DICT_PATH_ABS}")

# 验证分词效果
_test_seg = jieba.lcut("食品安全法")
logger.info(f"jieba 分词测试: '食品安全法' → {_test_seg}")


def _segment_query(query: str) -> List[str]:
    """用 jieba 对查询词分词，返回去重后的 token 列表（>=2 字符），原始查询排在最前。

    示例:
        "电子取证要点" → ["电子取证要点", "电子取证", "要点"]
        "食品安全法 第五十条" → ["食品安全法 第五十条", "食品安全法", "第五十条"]
    """
    tokens = jieba.lcut(query)
    seen = set()
    result = []
    # 原始查询优先（精确匹配）
    q_stripped = query.strip()
    if len(q_stripped) >= 2:
        result.append(q_stripped)
        seen.add(q_stripped)
    for t in tokens:
        t = t.strip()
        if len(t) >= 2 and t not in seen:
            result.append(t)
            seen.add(t)
    return result

router = APIRouter()

ZHISHIKU_BASE = getattr(settings, 'ZHISHIKU_PATH', './zhishiku')

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

# ==================== 文档扫描缓存 ====================

_docs_cache: Optional[List[Dict[str, Any]]] = None
_docs_cache_time: float = 0
_DOCS_CACHE_TTL: float = 300.0  # 缓存有效期 5 分钟


def _get_cached_docs() -> List[Dict[str, Any]]:
    """获取文档列表（带内存缓存，避免每次搜索都扫描文件系统）"""
    global _docs_cache, _docs_cache_time
    now = time.time()
    if _docs_cache is not None and (now - _docs_cache_time) < _DOCS_CACHE_TTL:
        return _docs_cache
    docs = _scan_all_md_files()
    _docs_cache = docs
    _docs_cache_time = now
    return docs


def invalidate_docs_cache():
    """手动清除文档缓存（扫描/上传/删除文档后调用）"""
    global _docs_cache, _docs_cache_time
    _docs_cache = None
    _docs_cache_time = 0

# ==================== 数字转换工具 ====================

CN_NUMBERS = {
    '零': 0, '一': 1, '二': 2, '三': 3, '四': 4,
    '五': 5, '六': 6, '七': 7, '八': 8, '九': 9,
    '十': 10, '百': 100, '千': 1000, '万': 10000
}

ARABIC_TO_CN = {
    '0': '零', '1': '一', '2': '二', '3': '三', '4': '四',
    '5': '五', '6': '六', '7': '七', '8': '八', '9': '九',
}


def chinese_to_arabic(cn: str) -> int:
    """中文数字转阿拉伯数字"""
    if not cn:
        return 0
    if cn.isdigit():
        return int(cn)
    total = 0
    current = 0
    for ch in cn:
        if ch in CN_NUMBERS:
            num = CN_NUMBERS[ch]
            if num >= 10:
                if current == 0:
                    current = 1
                total += current * num
                current = 0
            else:
                current = current * 10 + num if current > 0 else num
    return total + current


def arabic_to_chinese(num: int) -> str:
    """阿拉伯数字转中文数字（1-99999）"""
    if num <= 0:
        return ''
    if num <= 10:
        return list(CN_NUMBERS.keys())[list(CN_NUMBERS.values()).index(num)]
    result = []
    str_num = str(num)
    length = len(str_num)
    for i, ch in enumerate(str_num):
        n = int(ch)
        if n != 0:
            # 补零：如果当前位前面有非零数字且不是第一位
            if result and i > 0 and str_num[i-1] == '0':
                result.append('零')
            result.append(ARABIC_TO_CN[ch])
            # 添加位值
            pos = length - i - 1
            if pos == 4:
                result.append('万')
            elif pos == 3:
                result.append('千')
            elif pos == 2:
                result.append('百')
            elif pos == 1:
                result.append('十')
    return ''.join(result)


# ==================== 数据库查询函数 ====================

def _parse_json_field(val, default=None):
    """安全解析 JSON 字段"""
    if default is None:
        default = []
    if val is None:
        return default
    if isinstance(val, (list, dict)):
        return val
    if isinstance(val, str):
        try:
            return json.loads(val)
        except Exception:
            return default
    return default


async def _get_docs_from_db() -> List[Dict[str, Any]]:
    """从 kb_documents 表获取文档元数据列表（替代文件系统扫描）"""
    docs = []
    try:
        async for conn, cur in (get_system_db() if _has_split_databases() else get_db()):
            await cur.execute("""
                SELECT id, title, category, region, status, file_path, file_size,
                       anyou, tags, sub_dir, view_count, issuer, effective_date,
                       version, version_name, source, source_url
                FROM kb_documents
            """)
            rows = await cur.fetchall()
            for raw_row in rows:
                row = dict(raw_row)  # sqlite3.Row 不支持 .get()
                anyou_val = _parse_json_field(row.get('anyou', '[]'))
                tags_val = _parse_json_field(row.get('tags', '[]'))

                rel_path = row.get('file_path', '') or ''
                filename = os.path.basename(rel_path) if rel_path else ''
                docs.append({
                    'rel_path': rel_path,
                    'full_path': os.path.join(ZHISHIKU_BASE, rel_path) if rel_path else '',
                    'filename': filename,
                    'title': row.get('title', '') or filename.replace('.md', ''),
                    'category': row.get('category', ''),
                    'region': row.get('region', ''),
                    'status': row.get('status', '有效'),
                    'anyou': anyou_val,
                    'tags': tags_val,
                    'frontmatter': {
                        'title': row.get('title', ''),
                        'region': row.get('region', ''),
                        'status': row.get('status', '有效'),
                        'anyou': anyou_val,
                        'issuer': row.get('issuer', ''),
                        'effective_date': row.get('effective_date', ''),
                        'version': row.get('version', ''),
                        'version_name': row.get('version_name', ''),
                        'source': row.get('source', ''),
                        'source_url': row.get('source_url', ''),
                        'tags': tags_val,
                    },
                    '_from_db': True,
                })
    except Exception as e:
        logger.warning(f"从数据库获取文档列表失败: {e}")
    return docs


async def _get_content_from_db(file_path: str) -> str:
    """从数据库获取文档完整内容"""
    try:
        async for conn, cur in (get_system_db() if _has_split_databases() else get_db()):
            await cur.execute(
                "SELECT content FROM kb_documents WHERE file_path = %s",
                (file_path,)
            )
            row = await cur.fetchone()
            if row:
                row = dict(row)  # sqlite3.Row 不支持 .get()
            if row and row.get('content') is not None and row['content'] != '':
                return row['content']
    except Exception as e:
        logger.warning(f"从数据库获取文档内容失败: {e}")
    return ''


async def _get_all_docs_for_search() -> List[Dict[str, Any]]:
    """获取搜索用的文档列表，从数据库读取"""
    db_docs = await _get_docs_from_db()
    if db_docs:
        return db_docs
    return _get_cached_docs()  # fallback：同步读取 DB（_scan_all_md_files 已改为查 DB）


# ==================== Markdown 解析工具 ====================

def _parse_frontmatter(text: str) -> dict:
    """解析 Markdown frontmatter，支持简单 key:value 和 YAML 列表格式。"""
    fm_regex = r'^---\s*\n(.*?)\n---\s*\n'
    match = re.match(fm_regex, text, re.DOTALL)
    if not match or not match.group(1):
        return {}
    fm = {}
    list_key = None  # 当前正在收集列表的 key
    list_items = []  # 列表项缓存

    def _flush_list():
        """将缓存的列表项写入 fm"""
        nonlocal list_key, list_items
        if list_key and list_items:
            fm[list_key] = list_items
        list_key = None
        list_items = []

    for line in match.group(1).split('\n'):
        stripped = line.strip()
        # YAML 列表项：以 "- " 开头（缩进行）
        if stripped.startswith('- '):
            item = stripped[2:].strip()
            # 去掉引号
            if (item.startswith('"') and item.endswith('"')) or (item.startswith("'") and item.endswith("'")):
                item = item[1:-1]
            if list_key is not None:
                list_items.append(item)
            continue
        # 普通 key: value 行
        ci = line.find(':')
        if ci > 0:
            # 先 flush 之前的列表
            _flush_list()
            key = line[:ci].strip()
            val = line[ci+1:].strip()
            if val == '':
                # 值为空，可能是列表的起始行（如 anyou:）
                list_key = key
                list_items = []
            else:
                if (val.startswith('"') and val.endswith('"')) or (val.startswith("'") and val.endswith("'")):
                    val = val[1:-1]
                fm[key] = val
        elif not stripped:
            # 空行，flush 当前列表
            _flush_list()

    # 文末 flush
    _flush_list()
    return fm


def _strip_frontmatter(text: str) -> str:
    fm_regex = r'^---\s*\n.*?\n---\s*\n'
    return re.sub(fm_regex, '', text, count=1, flags=re.DOTALL).strip()


def _scan_all_md_files() -> List[Dict[str, Any]]:
    """从 system.db 获取所有文档的元数据（不含完整内容）"""
    import sqlite3 as _sqlite3
    from ...core.config import settings
    from ...core.paths import get_data_dir
    db_path = os.path.join(get_data_dir(), settings.SYSTEM_DB_PATH)
    if not os.path.isfile(db_path):
        return []
    try:
        conn = _sqlite3.connect(db_path)
        conn.row_factory = _sqlite3.Row
        cur = conn.cursor()
        cur.execute("SELECT file_path, title, category, region, status, anyou FROM kb_documents")
        results = []
        for row in cur.fetchall():
            rel_path = row['file_path'] or ''
            filename = os.path.basename(rel_path) if rel_path else ''
            results.append({
                'rel_path': rel_path,
                'full_path': '',  # 不再使用文件系统路径
                'filename': filename,
                'title': row['title'] or filename,
                'category': row['category'] or '',
                'region': row['region'] or '',
                'status': row['status'] or '有效',
                'anyou': row['anyou'] or '',
                'frontmatter': {},
            })
        conn.close()
        return results
    except Exception as e:
        logger.warning(f"从数据库扫描文档失败: {e}")
        return []


def _read_full_content(full_path: str) -> str:
    """读取文件完整内容"""
    try:
        return read_text_file(full_path)
    except Exception:
        return ''


def _strip_md_heading(line: str) -> str:
    """去掉 Markdown 标题前缀（### 、## 、# 等）"""
    return re.sub(r'^#{1,6}\s+', '', line.strip())


def _extract_article_content(content: str, article_patterns: List[str]) -> Optional[Dict[str, str]]:
    """
    在文档内容中定位法条
    article_patterns: 要匹配的法条编号列表，如 ['第五十条', '第50条']
    返回匹配到的法条段落
    """
    # 去掉 frontmatter
    body = _strip_frontmatter(content)
    lines = body.split('\n')

    for pattern in article_patterns:
        # 法条标题正则：### 第X条 或 ### 第X条 标题
        article_header_re = re.compile(rf'^###\s*{re.escape(pattern)}(?:\s+.*)?$')
        for i, line in enumerate(lines):
            if article_header_re.match(line.strip()):
                # 找到了法条标题，收集后续内容直到下一个 ### 或 ##
                article_lines = [_strip_md_heading(line)]
                for j in range(i + 1, len(lines)):
                    next_line = lines[j].strip()
                    if re.match(r'^#{2,3}\s', next_line):
                        break
                    if next_line:
                        article_lines.append(next_line)
                return {
                    'header': _strip_md_heading(line),
                    'content': '\n'.join(article_lines),
                }
    return None


def _generate_article_variants(article_num_raw: str) -> List[str]:
    """
    生成法条编号的多种格式变体
    输入: '第五十条' 或 '第50条' 或 '50'
    输出: ['第五十条', '第50条']
    """
    variants = set()
    # 提取数字部分
    num_match = re.search(r'第?\s*([一二三四五六七八九十百千万零\d]+)\s*条', article_num_raw)
    if not num_match:
        return [article_num_raw]
    num_str = num_match.group(1).strip()
    # 转为阿拉伯数字
    arabic = chinese_to_arabic(num_str)
    if arabic <= 0:
        return [article_num_raw]
    # 中文格式
    cn = arabic_to_chinese(arabic)
    if cn:
        variants.add(f'第{cn}条')
    # 阿拉伯数字格式
    variants.add(f'第{arabic}条')
    return sorted(variants)


# ==================== 搜索核心逻辑 ====================

def _search_by_title(docs: List[Dict], keyword: str) -> List[Dict]:
    """标题模糊搜索（同时匹配标题、文件名、案由），支持多关键词命中计数排序"""
    # 用 jieba 分词，取 >=2 字符的 token
    keywords = [t.strip().lower() for t in jieba.lcut(keyword) if len(t.strip()) >= 2]
    if not keywords:
        keywords = [keyword.lower()]
    kw_set = list(dict.fromkeys(keywords))  # 去重保序
    logger.info(f"[_search_by_title] query='{keyword}' → jieba segments={kw_set}, docs count={len(docs)}")

    results = []
    for doc in docs:
        title = (doc.get('title') or '').lower()
        filename = doc.get('filename', '').lower()
        # 匹配案由（anyou 可能是字符串或列表）
        anyou = doc.get('anyou', '')
        anyou_text = ''
        if isinstance(anyou, list):
            anyou_text = ' '.join(str(a) for a in anyou).lower()
        elif anyou:
            anyou_text = str(anyou).lower()

        # 多关键词命中计数
        title_hits = sum(1 for kw in kw_set if kw in title)
        filename_hits = sum(1 for kw in kw_set if kw in filename)
        anyou_hits = sum(1 for kw in kw_set if kw in anyou_text)
        total_hits = max(title_hits, filename_hits, anyou_hits)

        if total_hits > 0:
            match_type = 'title'
            if anyou_hits > 0 and title_hits == 0 and filename_hits == 0:
                match_type = 'anyou'
            results.append({
                **doc,
                'match_type': match_type,
                'highlight': doc.get('title', doc['filename']),
                '_hit_count': total_hits,
            })

    # 按命中数降序排序
    results.sort(key=lambda r: r.get('_hit_count', 0), reverse=True)
    logger.info(f"[_search_by_title] query='{keyword}' → {len(results)} results, top3={[r.get('title','')[:30] for r in results[:3]]}")
    return results


def _search_by_anyou(docs: List[Dict], keyword: str, limit: int = 50) -> List[Dict]:
    """按案由搜索：优先匹配 frontmatter 中的 anyou 字段，再对处罚案例文档做全文匹配"""
    kw = keyword.lower()
    seen_paths = set()
    results = []

    # 第一轮：匹配 anyou 元数据（优先级最高）
    for doc in docs:
        anyou = doc.get('anyou', '')
        if not anyou:
            continue
        if isinstance(anyou, list):
            anyou_text = ' '.join(str(a) for a in anyou).lower()
        else:
            anyou_text = str(anyou).lower()
        if kw in anyou_text:
            seen_paths.add(doc['rel_path'])
            results.append({
                **doc,
                'match_type': 'anyou',
                'highlight': str(anyou) if not isinstance(anyou, list) else '；'.join(anyou),
            })

    # 第二轮：对处罚案例文档做全文匹配（补充结果）
    if len(results) < limit:
        case_docs = [d for d in docs if d.get('category') == '处罚案例' and d['rel_path'] not in seen_paths]
        for doc in case_docs:
            if len(results) >= limit:
                break
            content = _read_full_content(doc['full_path'])
            if not content:
                continue
            body = _strip_frontmatter(content).lower()
            if kw in body:
                idx = body.find(kw)
                start = max(0, idx - 100)
                end = min(len(body), idx + 200)
                preview = body[start:end]
                preview = re.sub(r'[#*>`\[\]\(\)]', '', preview)
                results.append({
                    **doc,
                    'match_type': 'fulltext',
                    'preview': preview,
                })

    return results


def _search_by_fulltext(docs: List[Dict], keyword: str, limit: int = 50) -> List[Dict]:
    """全文模糊搜索，支持多关键词命中计数排序"""
    keywords = [t.strip().lower() for t in jieba.lcut(keyword) if len(t.strip()) >= 2]
    if not keywords:
        keywords = [keyword.lower()]
    kw_set = list(dict.fromkeys(keywords))  # 去重保序

    results = []
    for doc in docs:
        content = _read_full_content(doc['full_path'])
        if not content:
            continue
        body = _strip_frontmatter(content).lower()

        # 多关键词命中计数
        hit_count = sum(1 for kw in kw_set if kw in body)
        if hit_count > 0:
            # 用第一个命中的关键词提取预览
            first_kw = next((kw for kw in kw_set if kw in body), kw_set[0])
            idx = body.find(first_kw)
            start = max(0, idx - 100)
            end = min(len(body), idx + 200)
            preview = body[start:end]
            preview = re.sub(r'[#*>`\[\]\(\)]', '', preview)
            results.append({
                **doc,
                'match_type': 'fulltext',
                'preview': preview,
                '_hit_count': hit_count,
            })
        if len(results) >= limit * 2:
            break  # 多取一些，排序后截断

    # 按命中数降序排序
    results.sort(key=lambda r: r.get('_hit_count', 0), reverse=True)
    return results[:limit]


def _search_by_article(docs: List[Dict], query: str) -> List[Dict]:
    """法条定位搜索
    支持格式：《食品安全法》第五十条、食品安全法 第50条、第20条 食品安全法、食品安全法第二十条
    """
    # 1. 解析查询
    law_match = re.search(r'《([^》]+)》', query)
    law_name = law_match.group(1).strip() if law_match else None

    article_match = re.search(r'第\s*([一二三四五六七八九十百千万零\d]+)\s*条', query)
    if not article_match:
        # 没有法条编号，退化为标题搜索
        return _search_by_title(docs, query)

    num_str = article_match.group(1).strip()
    article_variants = _generate_article_variants(f'第{num_str}条')

    # 如果没有《》，尝试提取法律名称
    if not law_name:
        parts = re.split(r'第\s*[一二三四五六七八九十百千万零\d]+\s*条', query)
        # 先尝试"第X条"之前的文本（如 "食品安全法 第二十条"）
        if parts and parts[0].strip():
            candidate = parts[0].strip().rstrip(' \n,，;；')
            if len(candidate) > 2:
                law_name = candidate
        # 再尝试"第X条"之后的文本（如 "第二十条 食品安全法"）
        if not law_name and len(parts) > 1 and parts[1].strip():
            candidate = parts[1].strip().lstrip(' \n,，;；').rstrip(' \n,，;；')
            if len(candidate) > 2:
                law_name = candidate

    # 2. 先按法律名称匹配文档
    matched_docs = docs
    if law_name:
        law_kw = law_name.lower()
        matched_docs = [
            d for d in docs
            if law_kw in (d.get('title') or '').lower()
            or law_kw in d.get('filename', '').lower()
        ]
        # 如果精确匹配不到，尝试简化匹配（去掉"中华人民共和国"）
        if not matched_docs and law_kw.startswith('中华人民共和国'):
            short_kw = law_kw.replace('中华人民共和国', '').strip()
            matched_docs = [
                d for d in docs
                if short_kw in (d.get('title') or '').lower()
                or short_kw in d.get('filename', '').lower()
            ]

    # 3. 在匹配到的文档中定位法条
    results = []
    for doc in matched_docs:
        content = _read_full_content(doc['full_path'])
        if not content:
            continue
        article = _extract_article_content(content, article_variants)
        if article:
            results.append({
                **doc,
                'match_type': 'article',
                'article_header': article['header'],
                'article_content': article['content'],
                'article_variants': article_variants,
                'preview': article['content'][:300],
            })

    return results


def _search_all(docs: List[Dict], query: str, limit: int = 50) -> List[Dict]:
    """全部搜索：智能合并 title / anyou / fulltext 结果
    - 包含法条特征时优先 article
    - 否则按分类优先级排序：标题(法律法规) → 法律法规全文 → 处罚案例案由 → 处罚案例全文 → 处罚裁量全文
    - 整体搜索无结果时，用 jieba 分词回退
    """
    results = []
    seen_paths = set()

    # 1. 如果包含法条特征，优先 article 搜索
    if re.search(r'第\s*[一二三四五六七八九十百千万零\d]+\s*条', query):
        article_results = _search_by_article(docs, query)
        for r in article_results:
            if r['rel_path'] not in seen_paths:
                seen_paths.add(r['rel_path'])
                results.append(r)
        if results:
            return results[:limit]

    # 2. title 搜索（多关键词命中计数，已内置 jieba 分词）
    title_results = _search_by_title(docs, query)
    for r in title_results:
        if r['rel_path'] not in seen_paths:
            seen_paths.add(r['rel_path'])
            results.append(r)

    # 3. fulltext 搜索（分分类搜索，确保法律法规优先被搜到）
    #    处罚案例太多(26669)，不分分类的话法律法规根本轮不到
    for cat in ['法律法规', '处罚案例', '处罚裁量']:
        cat_docs = [d for d in docs if d.get('category') == cat and d['rel_path'] not in seen_paths]
        cat_limit = limit if cat == '法律法规' else limit  # 法律法规多取一些
        cat_results = _search_by_fulltext(cat_docs, query, cat_limit)
        for r in cat_results:
            if r['rel_path'] not in seen_paths:
                seen_paths.add(r['rel_path'])
                results.append(r)

    # 4. anyou 搜索（处罚案例的案由元数据匹配）
    anyou_results = _search_by_anyou(docs, query, limit)
    for r in anyou_results:
        if r['rel_path'] not in seen_paths:
            seen_paths.add(r['rel_path'])
            results.append(r)

    # 5. 按分类优先级排序：标题 > 法律法规全文 > 处罚案例案由 > 处罚案例全文 > 处罚裁量
    # 组合优先级（越小越靠前）：title+法律法规=1, title+处罚案例=2, title+处罚裁量=3,
    #   fulltext+法律法规=4, anyou+处罚案例=5, fulltext+处罚案例=6, fulltext+处罚裁量=7
    _COMBO_PRIORITY = {
        ('title', '法律法规'): 1,
        ('title', '处罚案例'): 2,
        ('title', '处罚裁量'): 3,
        ('fulltext', '法律法规'): 4,
        ('anyou', '处罚案例'): 5,
        ('fulltext', '处罚案例'): 6,
        ('fulltext', '处罚裁量'): 7,
    }

    def _sort_key(r: Dict) -> tuple:
        match_type = r.get('match_type', 'title')
        category = r.get('category', '')
        hit_count = r.get('_hit_count', 1)
        combo = (match_type, category)
        priority = _COMBO_PRIORITY.get(combo, 99)
        return (priority, -hit_count)

    results.sort(key=_sort_key)

    # 6. jieba 分词回退：整体搜索无结果时，用分词后的子词分别搜索
    if not results:
        sub_keywords = _segment_query(query)
        # 去掉原始查询（已在上面搜过）
        sub_keywords = [kw for kw in sub_keywords if kw != query.strip()]
        for kw in sub_keywords:
            for r in _search_by_title(docs, kw):
                if r['rel_path'] not in seen_paths:
                    seen_paths.add(r['rel_path'])
                    results.append(r)
            if len(results) < limit:
                for r in _search_by_fulltext(docs, kw, 5):
                    if r['rel_path'] not in seen_paths:
                        seen_paths.add(r['rel_path'])
                        results.append(r)
            if len(results) >= limit:
                break

    return results[:limit]


# ==================== 数据库搜索函数 ====================

async def _db_check_has_data() -> bool:
    """检查 kb_documents 表是否有数据"""
    try:
        async for conn, cur in (get_system_db() if _has_split_databases() else get_db()):
            await cur.execute("SELECT COUNT(*) as cnt FROM kb_documents")
            row = await cur.fetchone()
            if row:
                row = dict(row)  # sqlite3.Row 不支持 .get()
            return bool(row and row.get('cnt', 0) > 0)
    except Exception as e:
        logger.warning(f"检查数据库文档数量失败: {e}")
        return False


async def _db_search_by_title(kw: str, limit: int) -> List[Dict]:
    """数据库标题搜索（支持 jieba 多关键词命中计数）"""
    keywords = [t.strip().lower() for t in jieba.lcut(kw) if len(t.strip()) >= 2]
    if not keywords:
        keywords = [kw.lower()]
    kw_set = list(dict.fromkeys(keywords))
    logger.info(f"[_db_search_by_title] query='{kw}' → jieba segments={kw_set}")

    results = []
    seen_ids = set()

    async for conn, cur in (get_system_db() if _has_split_databases() else get_db()):
        # 策略1：LIKE AND（所有关键词都出现在标题中）→ 高精度
        if len(kw_set) >= 2:
            and_conditions = []
            and_params = []
            for k in kw_set:
                and_conditions.append("LOWER(title) LIKE %s")
                and_params.append(f"%{k}%")
            where_and = " AND ".join(and_conditions)
            await cur.execute(
                f"""SELECT id, title, category, region, status, file_path, anyou, tags
                   FROM kb_documents
                   WHERE ({where_and})
                   LIMIT %s""",
                (*and_params, limit * 2)
            )
            for raw_row in await cur.fetchall():
                row = dict(raw_row)
                if row['id'] not in seen_ids:
                    seen_ids.add(row['id'])
                    results.append(_db_build_title_result(row, kw_set))

        # 策略2：LIKE OR（任一关键词出现在标题中）→ 补充召回
        if len(results) < limit:
            or_conditions = []
            or_params = []
            for k in kw_set:
                or_conditions.append("LOWER(title) LIKE %s")
                or_params.append(f"%{k}%")
            where_or = " OR ".join(or_conditions)
            await cur.execute(
                f"""SELECT id, title, category, region, status, file_path, anyou, tags
                   FROM kb_documents
                   WHERE ({where_or})
                   LIMIT %s""",
                (*or_params, limit * 2)
            )
            for raw_row in await cur.fetchall():
                row = dict(raw_row)
                if row['id'] not in seen_ids:
                    seen_ids.add(row['id'])
                    results.append(_db_build_title_result(row, kw_set))

    results.sort(key=lambda r: r.get('_hit_count', 0), reverse=True)
    return results


def _db_build_title_result(row: dict, kw_set: list) -> dict:
    """构建数据库标题搜索的结果项"""
    anyou_val = _parse_json_field(row.get('anyou', '[]'))
    anyou_text = ''
    if isinstance(anyou_val, list):
        anyou_text = ' '.join(str(a) for a in anyou_val).lower()
    elif anyou_val:
        anyou_text = str(anyou_val).lower()

    title_lower = (row.get('title', '') or '').lower()
    filename_lower = (row.get('file_path', '') or '').lower()

    title_hits = sum(1 for k in kw_set if k in title_lower)
    filename_hits = sum(1 for k in kw_set if k in filename_lower)
    anyou_hits = sum(1 for k in kw_set if k in anyou_text)
    total_hits = max(title_hits, filename_hits, anyou_hits)

    match_type = 'title'
    if anyou_hits > 0 and title_hits == 0 and filename_hits == 0:
        match_type = 'anyou'

    return {
        'rel_path': row.get('file_path', ''),
        'filename': os.path.basename(row.get('file_path', '')),
        'title': row.get('title', ''),
        'category': row.get('category', ''),
        'region': row.get('region', ''),
        'status': row.get('status', '有效'),
        'anyou': anyou_val,
        'match_type': match_type,
        'highlight': row.get('title', ''),
        '_hit_count': total_hits,
    }


async def _db_search_by_fulltext(kw: str, limit: int) -> List[Dict]:
    """数据库全文搜索（使用 FTS5 全文索引）"""
    keywords = [t.strip().lower() for t in jieba.lcut(kw) if len(t.strip()) >= 2]
    if not keywords:
        keywords = [kw.lower()]
    kw_set = list(dict.fromkeys(keywords))

    # 构建 FTS5 MATCH 查询（OR 连接所有关键词）
    fts_query = ' OR '.join(f'"{k}"' for k in kw_set)

    results = []
    seen_ids = set()

    async for conn, cur in (get_system_db() if _has_split_databases() else get_db()):
        try:
            await cur.execute(
                """SELECT kb.id, kb.title, kb.category, kb.region, kb.status, kb.file_path, kb.anyou, kb.content
                   FROM kb_documents kb
                   JOIN kb_documents_fts fts ON kb.id = fts.rowid
                   WHERE kb_documents_fts MATCH %s
                   ORDER BY rank
                   LIMIT %s""",
                (fts_query, limit)
            )
            for raw_row in await cur.fetchall():
                row = dict(raw_row)
                if row['id'] not in seen_ids:
                    seen_ids.add(row['id'])
                    results.append(_db_build_fulltext_result(row, kw_set))
        except Exception as e:
            logger.warning(f"FTS5 全文搜索失败，回退到 LIKE: {e}")
            # 回退到 LIKE 搜索
            for k in kw_set:
                if len(results) >= limit:
                    break
                await cur.execute(
                    """SELECT id, title, category, region, status, file_path, anyou, content
                       FROM kb_documents
                       WHERE LOWER(content) LIKE %s
                       LIMIT %s""",
                    (f"%{k}%", limit - len(results))
                )
                for raw_row in await cur.fetchall():
                    row = dict(raw_row)
                    if row['id'] in seen_ids:
                        continue
                    seen_ids.add(row['id'])
                    results.append(_db_build_fulltext_result(row, kw_set))

    results.sort(key=lambda r: r.get('_hit_count', 0), reverse=True)
    return results


async def _db_search_by_fulltext_cat(kw: str, category: str, limit: int) -> List[Dict]:
    """数据库全文搜索（指定分类，使用 FTS5 全文索引）"""
    keywords = [t.strip().lower() for t in jieba.lcut(kw) if len(t.strip()) >= 2]
    if not keywords:
        keywords = [kw.lower()]
    kw_set = list(dict.fromkeys(keywords))

    # 构建 FTS5 MATCH 查询
    fts_query = ' OR '.join(f'"{k}"' for k in kw_set)

    results = []
    seen_ids = set()

    async for conn, cur in (get_system_db() if _has_split_databases() else get_db()):
        try:
            await cur.execute(
                """SELECT kb.id, kb.title, kb.category, kb.region, kb.status, kb.file_path, kb.anyou, kb.content
                   FROM kb_documents kb
                   JOIN kb_documents_fts fts ON kb.id = fts.rowid
                   WHERE kb_documents_fts MATCH %s AND kb.category = %s
                   ORDER BY rank
                   LIMIT %s""",
                (fts_query, category, limit)
            )
            for raw_row in await cur.fetchall():
                row = dict(raw_row)
                if row['id'] not in seen_ids:
                    seen_ids.add(row['id'])
                    results.append(_db_build_fulltext_result(row, kw_set))
        except Exception as e:
            logger.warning(f"FTS5 全文搜索失败，回退到 LIKE: {e}")
            # 回退到 LIKE 搜索
            for k in kw_set:
                if len(results) >= limit:
                    break
                await cur.execute(
                    """SELECT id, title, category, region, status, file_path, anyou, content
                       FROM kb_documents
                       WHERE category = %s AND LOWER(content) LIKE %s
                       LIMIT %s""",
                    (category, f"%{k}%", limit - len(results))
                )
                for raw_row in await cur.fetchall():
                    row = dict(raw_row)
                    if row['id'] in seen_ids:
                        continue
                    seen_ids.add(row['id'])
                    results.append(_db_build_fulltext_result(row, kw_set))

    results.sort(key=lambda r: r.get('_hit_count', 0), reverse=True)
    return results[:limit]


def _db_build_fulltext_result(row: dict, kw_set: list) -> dict:
    """构建数据库全文搜索的结果项"""
    content = row.get('content', '') or ''
    content_lower = content.lower()

    hit_count = sum(1 for k in kw_set if k in content_lower)
    first_kw = next((k for k in kw_set if k in content_lower), kw_set[0])
    idx = content_lower.find(first_kw)
    preview = ''
    if idx >= 0:
        start = max(0, idx - 100)
        end = min(len(content), idx + 200)
        preview = content[start:end]
        preview = re.sub(r'[#*>`\[\]\(\)]', '', preview)

    anyou_val = _parse_json_field(row.get('anyou', '[]'))
    return {
        'rel_path': row.get('file_path', ''),
        'filename': os.path.basename(row.get('file_path', '')),
        'title': row.get('title', ''),
        'category': row.get('category', ''),
        'region': row.get('region', ''),
        'status': row.get('status', '有效'),
        'anyou': anyou_val,
        'match_type': 'fulltext',
        'preview': preview,
        '_hit_count': hit_count,
    }


async def _db_search_by_article(query: str, kw: str, limit: int) -> List[Dict]:
    """数据库法条定位搜索"""
    law_match = re.search(r'《([^》]+)》', query)
    law_name = law_match.group(1).strip() if law_match else None
    article_match = re.search(r'第\s*([一二三四五六七八九十百千万零\d]+)\s*条', query)

    if not article_match:
        return await _db_search_by_title(kw, limit)

    num_str = article_match.group(1).strip()
    article_variants = _generate_article_variants(f'第{num_str}条')

    if not law_name:
        parts = re.split(r'第\s*[一二三四五六七八九十百千万零\d]+\s*条', query)
        if parts and parts[0].strip():
            candidate = parts[0].strip().rstrip(' \n,，;；')
            if len(candidate) > 2:
                law_name = candidate
        if not law_name and len(parts) > 1 and parts[1].strip():
            candidate = parts[1].strip().lstrip(' \n,，;；').rstrip(' \n,，;；')
            if len(candidate) > 2:
                law_name = candidate

    results = []
    async for conn, cur in (get_system_db() if _has_split_databases() else get_db()):
        if law_name:
            like_law = f"%{law_name.lower()}%"
            await cur.execute(
                """SELECT id, title, category, region, status, file_path, anyou, content
                   FROM kb_documents
                   WHERE (LOWER(title) LIKE %s OR LOWER(file_path) LIKE %s)""",
                (like_law, like_law)
            )
        else:
            await cur.execute(
                """SELECT id, title, category, region, status, file_path, anyou, content
                   FROM kb_documents"""
            )
        rows = await cur.fetchall()

        for raw_row in rows:
            row = dict(raw_row)  # sqlite3.Row 不支持 .get()
            content = row.get('content', '') or ''
            if not content:
                continue
            article = _extract_article_content(content, article_variants)
            if article:
                anyou_val = _parse_json_field(row.get('anyou', '[]'))
                results.append({
                    'rel_path': row.get('file_path', ''),
                    'filename': os.path.basename(row.get('file_path', '')),
                    'title': row.get('title', ''),
                    'category': row.get('category', ''),
                    'region': row.get('region', ''),
                    'status': row.get('status', '有效'),
                    'anyou': anyou_val,
                    'match_type': 'article',
                    'article_header': article['header'],
                    'article_content': article['content'],
                    'article_variants': article_variants,
                    'preview': article['content'][:300],
                })
            if len(results) >= limit:
                break

    return results


async def _db_search_by_anyou(kw: str, limit: int) -> List[Dict]:
    """数据库案由搜索"""
    results = []
    async for conn, cur in (get_system_db() if _has_split_databases() else get_db()):
        like_kw = f"%{kw}%"
        await cur.execute(
            """SELECT id, title, category, region, status, file_path, anyou, content
               FROM kb_documents
                   WHERE LOWER(anyou) LIKE %s
               LIMIT %s""",
            (like_kw, limit)
        )
        rows = await cur.fetchall()
        for raw_row in rows:
            row = dict(raw_row)  # sqlite3.Row 不支持 .get()
            anyou_val = _parse_json_field(row.get('anyou', '[]'))
            results.append({
                'rel_path': row.get('file_path', ''),
                'filename': os.path.basename(row.get('file_path', '')),
                'title': row.get('title', ''),
                'category': row.get('category', ''),
                'region': row.get('region', ''),
                'status': row.get('status', '有效'),
                'anyou': anyou_val,
                'match_type': 'anyou',
                'highlight': '；'.join(anyou_val) if isinstance(anyou_val, list) else str(anyou_val),
            })

        # 补充：对处罚案例做全文匹配
        if len(results) < limit:
            await cur.execute(
                """SELECT id, title, category, region, status, file_path, anyou, content
                   FROM kb_documents
                   WHERE category = '处罚案例' AND LOWER(content) LIKE %s
                   LIMIT %s""",
                (like_kw, limit - len(results))
            )
            extra_rows = await cur.fetchall()
            seen_paths = {r['rel_path'] for r in results}
            for raw_row in extra_rows:
                row = dict(raw_row)  # sqlite3.Row 不支持 .get()
                if row.get('file_path', '') in seen_paths:
                    continue
                content = row.get('content', '') or ''
                content_lower = content.lower()
                idx = content_lower.find(kw)
                preview = ''
                if idx >= 0:
                    start = max(0, idx - 100)
                    end = min(len(content), idx + 200)
                    preview = content[start:end]
                    preview = re.sub(r'[#*>`\[\]\(\)]', '', preview)

                anyou_val = _parse_json_field(row.get('anyou', '[]'))

                results.append({
                    'rel_path': row.get('file_path', ''),
                    'filename': os.path.basename(row.get('file_path', '')),
                    'title': row.get('title', ''),
                    'category': row.get('category', ''),
                    'region': row.get('region', ''),
                    'status': row.get('status', '有效'),
                    'anyou': anyou_val,
                    'match_type': 'fulltext',
                    'preview': preview,
                })
    return results


async def _db_search_all(query: str, kw: str, limit: int) -> List[Dict]:
    """数据库全部搜索：title → fulltext(按分类) → anyou → 优先级排序，支持 jieba 分词回退"""
    results = []
    seen_paths = set()

    logger.info(f"[_db_search_all] query='{query}' kw='{kw}' limit={limit}")

    # 1. 法条搜索
    if re.search(r'第\s*[一二三四五六七八九十百千万零\d]+\s*条', query):
        article_results = await _db_search_by_article(query, kw, limit)
        for r in article_results:
            if r['rel_path'] not in seen_paths:
                seen_paths.add(r['rel_path'])
                results.append(r)
        if results:
            return results[:limit]

    # 2. 标题搜索
    title_results = await _db_search_by_title(kw, limit)
    logger.info(f"[_db_search_all] title search returned {len(title_results)} results")
    for r in title_results:
        if r['rel_path'] not in seen_paths:
            seen_paths.add(r['rel_path'])
            results.append(r)

    # 3. 全文搜索（分分类搜索，确保法律法规优先被搜到）
    for cat in ['法律法规', '处罚案例', '处罚裁量']:
        cat_results = await _db_search_by_fulltext_cat(kw, cat, limit)
        logger.info(f"[_db_search_all] fulltext [{cat}] returned {len(cat_results)} results")
        for r in cat_results:
            if r['rel_path'] not in seen_paths:
                seen_paths.add(r['rel_path'])
                results.append(r)

    # 4. 案由搜索（处罚案例的案由元数据匹配）
    anyou_results = await _db_search_by_anyou(kw, limit)
    logger.info(f"[_db_search_all] anyou search returned {len(anyou_results)} results")
    for r in anyou_results:
        if r['rel_path'] not in seen_paths:
            seen_paths.add(r['rel_path'])
            results.append(r)

    # 5. 按分类优先级排序：标题 > 法律法规全文 > 处罚案例案由 > 处罚案例全文 > 处罚裁量
    _COMBO_PRIORITY = {
        ('title', '法律法规'): 1, ('title', '处罚案例'): 2, ('title', '处罚裁量'): 3,
        ('fulltext', '法律法规'): 4, ('anyou', '处罚案例'): 5,
        ('fulltext', '处罚案例'): 6, ('fulltext', '处罚裁量'): 7,
    }

    def _sort_key(r: Dict) -> tuple:
        match_type = r.get('match_type', 'title')
        category = r.get('category', '')
        hit_count = r.get('_hit_count', 1)
        priority = _COMBO_PRIORITY.get((match_type, category), 99)
        return (priority, -hit_count)

    results.sort(key=_sort_key)

    # 6. jieba 分词回退：整体搜索无结果时，用分词后的子词分别搜索
    if not results:
        sub_keywords = _segment_query(query)
        sub_keywords = [k for k in sub_keywords if k != query.strip()]
        logger.info(f"[_db_search_all] jieba fallback: sub_keywords={sub_keywords}")
        for k in sub_keywords:
            title_results = await _db_search_by_title(k, limit)
            for r in title_results:
                if r['rel_path'] not in seen_paths:
                    seen_paths.add(r['rel_path'])
                    results.append(r)
            if len(results) < limit:
                for cat in ['法律法规', '处罚案例', '处罚裁量']:
                    cat_results = await _db_search_by_fulltext_cat(k, cat, 5)
                    for r in cat_results:
                        if r['rel_path'] not in seen_paths:
                            seen_paths.add(r['rel_path'])
                            results.append(r)
            if len(results) >= limit:
                break

    logger.info(f"[_db_search_all] FINAL: {len(results)} results, first 5: {[(r.get('match_type'), r.get('category'), r.get('title','')[:30]) for r in results[:5]]}")
    return results[:limit]


async def _search_from_db(
    query: str, search_type: str, limit: int,
    region: str, region_keywords: list = None
) -> Optional[dict]:
    """从数据库搜索，返回结果字典；如果数据库不可用返回 None"""

    has_data = await _db_check_has_data()
    logger.info(f"[_search_from_db] query='{query}' type={search_type} db_has_data={has_data}")
    if not has_data:
        return None

    kw = query.lower()
    results = []

    if search_type == "title":
        results = await _db_search_by_title(kw, limit)
    elif search_type == "fulltext":
        results = await _db_search_by_fulltext(kw, limit)
    elif search_type == "article":
        results = await _db_search_by_article(query, kw, limit)
    elif search_type == "anyou":
        results = await _db_search_by_anyou(kw, limit)
    else:
        results = await _db_search_all(query, kw, limit)

    # 区域过滤
    before_filter = len(results)
    results = _filter_by_region(results, region, region_keywords)
    logger.info(f"[_search_from_db] results before filter={before_filter}, after filter={len(results)}")

    # 格式化返回
    formatted = []
    for r in results[:limit]:
        anyou = r.get('anyou', [])
        if isinstance(anyou, list):
            anyou = anyou
        elif anyou:
            anyou = [str(anyou)]
        else:
            anyou = []
        formatted.append({
            "id": r['rel_path'].replace('/', '__').replace('\\', '__'),
            "rel_path": r['rel_path'],
            "title": r['title'],
            "filename": r.get('filename', ''),
            "category": r['category'],
            "region": r['region'],
            "status": r.get('status', '有效'),
            "anyou": anyou,
            "match_type": r.get('match_type', 'title'),
            "preview": r.get('preview', ''),
            "article_header": r.get('article_header', ''),
            "article_content": r.get('article_content', ''),
            "article_variants": r.get('article_variants', []),
        })

    return {
        "success": True,
        "results": formatted,
        "total": len(results),
        "search_type": search_type,
        "query": query,
    }


# ==================== API 端点 ====================

def _doc_matches_keywords(doc: Dict, keywords: list) -> bool:
    """文档是否匹配任意一个区域关键词（文件名 / region / title / preview）"""
    for kw in keywords:
        if kw in doc.get('filename', ''):
            return True
        if kw in doc.get('region', ''):
            return True
        if kw in doc.get('title', ''):
            return True
        if kw in doc.get('preview', ''):
            return True
    return False


# 后端内置区域关键词映射（与 knowledge_lib.py 保持一致）
_BUILTIN_REGION_KEYWORDS = {
    '全国': [],
    '北京市': ['北京', '北京市'], '天津市': ['天津', '天津市'],
    '上海市': ['上海', '上海市'], '重庆市': ['重庆', '重庆市'],
    '河北省': ['河北', '河北省'], '山西省': ['山西', '山西省'],
    '辽宁省': ['辽宁', '辽宁省'], '吉林省': ['吉林', '吉林省'],
    '黑龙江省': ['黑龙江', '黑龙江省', '龙江'],
    '江苏省': ['江苏', '江苏省'], '浙江省': ['浙江', '浙江省'],
    '安徽省': ['安徽', '安徽省'], '福建省': ['福建', '福建省'],
    '江西省': ['江西', '江西省'], '山东省': ['山东', '山东省'],
    '河南省': ['河南', '河南省'], '湖北省': ['湖北', '湖北省'],
    '湖南省': ['湖南', '湖南省'], '广东省': ['广东', '广东省'],
    '海南省': ['海南', '海南省'], '四川省': ['四川', '四川省'],
    '贵州省': ['贵州', '贵州省'], '云南省': ['云南', '云南省'],
    '陕西省': ['陕西', '陕西省'], '甘肃省': ['甘肃', '甘肃省'],
    '青海省': ['青海', '青海省'], '台湾省': ['台湾', '台湾省'],
    '内蒙古自治区': ['内蒙古', '内蒙', '内蒙古自治区'],
    '广西壮族自治区': ['广西', '广西壮族自治区'],
    '西藏自治区': ['西藏', '西藏自治区'],
    '宁夏回族自治区': ['宁夏', '宁夏回族自治区'],
    '新疆维吾尔自治区': ['新疆', '新疆维吾尔自治区', '新疆生产建设兵团', '兵团'],
    '香港特别行政区': ['香港', '香港特别行政区'],
    '澳门特别行政区': ['澳门', '澳门特别行政区'],
}


def _filter_by_region(results: List[Dict], region: Optional[str], keywords: Optional[list] = None) -> List[Dict]:
    """按适用区域过滤搜索结果，region 为 '全国' 或空时不过滤。
    仅对法律法规和处罚裁量生效，处罚案例不受影响。
    全国性法规（region='全国'）始终保留。"""
    if not region or region == '全国':
        return results
    if not keywords:
        keywords = _BUILTIN_REGION_KEYWORDS.get(region, [region])
    filtered = []
    for r in results:
        cat = r.get('category', '')
        # 处罚案例不过滤
        if cat == '处罚案例':
            filtered.append(r)
            continue
        # 全国性法规始终保留
        if r.get('region') == '全国':
            filtered.append(r)
            continue
        # 法律法规 / 处罚裁量：关键词匹配
        if _doc_matches_keywords(r, keywords):
            filtered.append(r)
    return filtered


@router.post("/md-search")
async def md_search_post(
    request: Request,
    user: dict = Depends(require_login),
):
    """
    Markdown 知识库文本搜索

    请求体:
    {
        "query": "搜索关键词",
        "search_type": "title",   // title | fulltext | article
        "limit": 20,
        "region": "全国"          // 适用区域过滤，为空或"全国"不过滤
    }
    """
    body = await safe_json(request)
    query = (body.get("query") or "").strip()
    search_type = body.get("search_type", "title")
    limit = min(max(body.get("limit", 20), 1), 100)
    region_filter = body.get("region", "")
    # 解析 region_keywords（前端传入的关键词列表）
    region_keywords_raw = body.get("region_keywords", [])
    if isinstance(region_keywords_raw, str):
        try:
            import json as _json
            region_keywords_list = _json.loads(region_keywords_raw)
        except Exception:
            region_keywords_list = []
    elif isinstance(region_keywords_raw, list):
        region_keywords_list = region_keywords_raw
    else:
        region_keywords_list = []

    if not query:
        return {"success": True, "results": [], "total": 0}

    # 搜索策略：纯数据库检索（kb_documents 表）
    try:
        db_result = await _search_from_db(
            query, search_type, limit, region_filter, region_keywords_list or None
        )
        if db_result:
            logger.info(f"[md_search] DB search OK: query='{query}' type={search_type} total={db_result.get('total', 0)}")
            return db_result
    except Exception as e:
        logger.error(f"[md_search] DB search failed: {e}")

    # DB 搜索失败或无结果，返回空（不再 fallback 文件系统扫描，避免 4s 延迟）
    logger.warning(f"[md_search] DB search returned no results for query='{query}' type={search_type}")
    return {"success": True, "results": [], "total": 0, "search_type": search_type, "query": query}


@router.get("/md-search")
async def md_search_get(
    q: str = Query(""),
    query: str = Query(""),
    search_type: str = Query("title"),
    limit: int = Query(20, ge=1, le=100),
    region: str = Query(""),
    user: dict = Depends(require_login),
):
    """GET 方式搜索（兼容旧接口参数，支持 region 过滤）"""
    search_query = (q or query).strip()
    if not search_query:
        return {"success": True, "results": [], "total": 0}

    # 搜索策略：纯数据库检索（kb_documents 表）
    try:
        db_result = await _search_from_db(search_query, search_type, limit, region, None)
        if db_result:
            logger.info(f"[md_search GET] DB search OK: query='{search_query}' type={search_type} total={db_result.get('total', 0)}")
            return db_result
    except Exception as e:
        logger.error(f"[md_search GET] DB search failed: {e}")

    return {"success": True, "results": [], "total": 0, "search_type": search_type, "query": search_query}


@router.get("/md-content")
async def get_md_content(
    path: str = Query(..., description="相对于 zhishiku 的文件路径，如 law/中华人民共和国食品安全法.md"),
    user: dict = Depends(require_login),
):
    """
    读取知识库 Markdown 文件的完整内容
    前端通过此接口加载文档，避免直接 fetch 静态路径被 Vite 拦截返回 index.html
    """
    # 安全检查：防止路径穿越攻击
    safe_path = path.replace('..', '').replace('\\', '/').lstrip('/')
    if not (safe_path.endswith('.md') or safe_path.endswith('.md.enc')):
        return {"error": "仅支持 .md/.md.enc 文件", "detail": f"非法文件类型: {path}"}

    # 优先从数据库读取
    try:
        async for conn, cur in (get_system_db() if _has_split_databases() else get_db()):
            await cur.execute(
                """SELECT content, title, region, status, version, version_name,
                          source, source_url, tags, anyou, effective_date, expiry_date, region_code
                   FROM kb_documents WHERE file_path = %s""",
                (safe_path,)
            )
            row = await cur.fetchone()
            if row:
                row = dict(row)  # sqlite3.Row 不支持 .get()
            if row and row.get('content') is not None and row['content'] != '':
                tags = _parse_json_field(row.get('tags', '[]'))
                anyou = _parse_json_field(row.get('anyou', '[]'))
                return {
                    "success": True,
                    "data": {
                        "content": _strip_frontmatter(row['content']),
                        "frontmatter": {
                            'title': row.get('title', '') or '',
                            'version': row.get('version', '') or '',
                            'version_name': row.get('version_name', '') or '',
                            'region': row.get('region', '') or '',
                            'region_code': row.get('region_code', '') or '',
                            'status': row.get('status', '有效') or '有效',
                            'source': row.get('source', '') or '',
                            'source_url': row.get('source_url', '') or '',
                            'tags': tags,
                            'anyou': anyou,
                            'effective_date': row.get('effective_date', '') or '',
                            'expiry_date': row.get('expiry_date', '') or '',
                        },
                        "raw_content": row['content'],
                        "filename": os.path.basename(safe_path),
                    }
                }
    except Exception as e:
        logger.warning(f"从数据库读取文档内容失败: {e}")

    return {"error": "文档不存在", "detail": f"未找到: {safe_path}"}


@router.get("/law-detail")
async def get_law_article_detail(
    law_name: str = Query("", description="法规名称"),
    article_number: str = Query("", description="条号（如 第三十四条、第34条）"),
):
    """
    根据法规名+条号查询知识库中的法条详情。
    用于案由法律依据自动填充：选中标准案由后，自动获取对应法条的完整内容。
    返回 { law_title, article_text, article_full_ref, source_url }
    """
    if not law_name or not article_number:
        return {"success": False, "error": "请提供 law_name 和 article_number"}

    # 生成条号变体（第34条 / 第三十四条）
    article_variants = _generate_article_variants(article_number)

    try:
        db_gen = get_system_db() if _has_split_databases() else get_db()
        async for conn, cur in db_gen:
            like_law = f"%{law_name.lower()}%"
            await cur.execute(
                """SELECT id, title, content, file_path, source_url
                   FROM kb_documents
                   WHERE (LOWER(title) LIKE %s OR LOWER(file_path) LIKE %s)
                   AND category = '法律法规'""",
                (like_law, like_law),
            )
            rows = await cur.fetchall()

            for raw_row in rows:
                row = dict(raw_row)
                content = row.get('content', '') or ''
                if not content:
                    continue
                article = _extract_article_content(content, article_variants)
                if article:
                    return {
                        "success": True,
                        "data": {
                            "law_title": row.get('title', ''),
                            "article_text": article.get('content', ''),
                            "article_full_ref": f"《{row.get('title', '')}》{article_number}",
                            "source_url": row.get('source_url', ''),
                            "file_path": row.get('file_path', ''),
                        },
                    }
            break
    except Exception as e:
        logger.warning(f"法条详情查询失败: {e}")
        return {"success": False, "error": str(e)}

    return {"success": False, "error": f"未找到匹配法条: {law_name} {article_number}"}
