from __future__ import annotations

import json
import re
import threading
from dataclasses import dataclass
from pathlib import Path
from time import perf_counter
from typing import Any

MAX_LOCAL_QUERY_TERMS = 14
MAX_LOCAL_HIT_CONTENT_LENGTH = 2200
MAX_LOCAL_HIT_EXCERPT_LENGTH = 240
LOCAL_CONFIDENCE_SCORE = 24
LOCAL_CONFIDENCE_MATCHES = 2
LOCAL_QUERY_STOPWORDS = {
    "什么",
    "多少",
    "哪些",
    "怎么",
    "如何",
    "请问",
    "一下",
    "关于",
    "规定",
    "标准",
    "可以",
    "是否",
    "一个",
    "根据",
    "依据",
    "给出",
    "说明",
    "公司",
    "远光",
    "软件",
    "股份",
    "有限",
    "员工",
    "当前",
    "详细",
    "问题",
}
LOCAL_TABLE_QUERY_HINTS = (
    "表",
    "表格",
    "清单",
    "明细",
    "目录",
    "科目",
    "标准",
    "金额",
    "限额",
    "补贴",
    "住宿",
    "餐费",
    "交通",
    "报销",
    "档位",
    "额度",
)
LOCAL_QUERY_ANCHOR_TERMS = (
    "财务基础知识手册",
    "基础知识手册",
    "会计科目",
    "常用会计科目",
    "财务报表",
    "主要税种",
    "税种",
    "标准",
    "清单",
    "明细",
    "流程",
)
LOCAL_GENERIC_TITLE_TERMS = {"远光软件", "股份有限", "有限公司"}
LOCAL_DOMAIN_TERMS = (
    "报销",
    "费用",
    "报销时限",
    "申请时限",
    "三个月",
    "逾期",
    "住宿费",
    "住宿",
    "差旅费",
    "差旅",
    "出差",
    "超标",
    "超过",
    "审批",
    "分管领导",
    "部门负责人",
    "业务招待",
    "招待费",
    "发票",
    "票据",
    "预算外",
    "预算",
    "补贴",
    "餐补",
    "交通费",
    "会议费",
    "培训费",
    "通信费",
)

_index_lock = threading.RLock()
_index_cache: dict[Path, tuple[tuple[int, int], list[dict[str, Any]]]] = {}


def clear_local_knowledge_index_cache() -> int:
    with _index_lock:
        cleared_count = len(_index_cache)
        _index_cache.clear()
    return cleared_count


@dataclass(frozen=True, slots=True)
class LocalKnowledgeSearchResult:
    hits: list[dict[str, Any]]
    confident: bool
    elapsed_seconds: float
    total_chunks: int
    best_score: int


def query_local_text_chunks(
    *,
    lightrag_root: Path,
    workspace: str,
    query: str,
    limit: int,
) -> LocalKnowledgeSearchResult:
    started_at = perf_counter()
    chunks = _load_text_chunks(lightrag_root / workspace / "kv_store_text_chunks.json")
    query_terms = _extract_local_query_terms(query)
    if not chunks or not query_terms:
        return LocalKnowledgeSearchResult(
            hits=[],
            confident=False,
            elapsed_seconds=perf_counter() - started_at,
            total_chunks=len(chunks),
            best_score=0,
        )

    prefers_tabular_evidence = any(hint in query for hint in LOCAL_TABLE_QUERY_HINTS)
    candidates: list[dict[str, Any]] = []
    for rank, chunk in enumerate(chunks, start=1):
        content = str(chunk.get("content") or "").strip()
        if not content:
            continue
        file_path = str(chunk.get("file_path") or "").strip()
        document_id, document_name = _parse_document_identity(
            file_path,
            fallback_doc_id=str(chunk.get("full_doc_id") or "").strip(),
        )
        score, matched_terms = _score_local_chunk(
            content=content,
            title=document_name,
            query_terms=query_terms,
            rank=rank,
            prefers_tabular_evidence=prefers_tabular_evidence,
        )
        if score <= 0:
            continue

        chunk_id = str(chunk.get("_id") or chunk.get("chunk_id") or "").strip()
        normalized_content = _truncate_text(
            content,
            max_length=MAX_LOCAL_HIT_CONTENT_LENGTH,
        )
        candidates.append(
            {
                "code": f"knowledge.{document_id or 'unknown'}.{chunk_id or rank}",
                "candidate_id": chunk_id or f"local-{rank}",
                "title": document_name or "知识库文档",
                "content": normalized_content,
                "excerpt": _build_query_focused_excerpt(
                    normalized_content,
                    query_terms=query_terms,
                    max_length=MAX_LOCAL_HIT_EXCERPT_LENGTH,
                ),
                "document_id": document_id,
                "document_name": document_name or Path(file_path).name,
                "version": None,
                "updated_at": None,
                "score": score,
                "tags": [],
                "evidence": [chunk_id] if chunk_id else [],
                "file_path": file_path,
                "_matched_terms": matched_terms,
            }
        )

    ranked = sorted(
        candidates,
        key=lambda item: (
            int(item.get("score") or 0),
            len(list(item.get("_matched_terms") or [])),
            -len(str(item.get("content") or "")),
        ),
        reverse=True,
    )
    hits: list[dict[str, Any]] = []
    for item in ranked[: max(1, limit)]:
        normalized = dict(item)
        normalized.pop("_matched_terms", None)
        hits.append(normalized)

    best_score = int(ranked[0].get("score") or 0) if ranked else 0
    best_match_count = len(list(ranked[0].get("_matched_terms") or [])) if ranked else 0
    confident = bool(
        hits
        and best_score >= LOCAL_CONFIDENCE_SCORE
        and best_match_count >= LOCAL_CONFIDENCE_MATCHES
    )
    return LocalKnowledgeSearchResult(
        hits=hits,
        confident=confident,
        elapsed_seconds=perf_counter() - started_at,
        total_chunks=len(chunks),
        best_score=best_score,
    )


def _load_text_chunks(path: Path) -> list[dict[str, Any]]:
    try:
        stat = path.stat()
    except OSError:
        return []

    signature = (int(stat.st_mtime_ns), int(stat.st_size))
    resolved_path = path.resolve()
    with _index_lock:
        cached = _index_cache.get(resolved_path)
        if cached is not None and cached[0] == signature:
            return cached[1]

        try:
            payload = json.loads(path.read_text(encoding="utf-8"))
        except (OSError, json.JSONDecodeError):
            chunks: list[dict[str, Any]] = []
        else:
            chunks = [
                dict(value)
                for value in payload.values()
                if isinstance(value, dict) and str(value.get("content") or "").strip()
            ]
            chunks.sort(
                key=lambda item: (
                    str(item.get("full_doc_id") or ""),
                    int(item.get("chunk_order_index") or 0),
                    str(item.get("_id") or ""),
                )
            )

        _index_cache[resolved_path] = (signature, chunks)
        return chunks


def _score_local_chunk(
    *,
    content: str,
    title: str,
    query_terms: list[str],
    rank: int,
    prefers_tabular_evidence: bool,
) -> tuple[int, list[str]]:
    lowered_content = content.lower()
    lowered_title = title.lower()
    matched_terms = [
        term for term in query_terms if term in lowered_content or term in lowered_title
    ]
    if not matched_terms:
        return 0, []

    score = max(1, 32 - min(rank, 20))
    for term in matched_terms:
        weight = 8 if len(term) >= 4 else 5 if len(term) == 3 else 2
        score += weight
        if term in lowered_title:
            score += max(4, weight)
            if len(term) >= 4 and term not in LOCAL_GENERIC_TITLE_TERMS:
                score += (len(term) - 3) * 12
        occurrences = lowered_content.count(term)
        if occurrences > 1:
            score += min(8, occurrences * 2)

    if prefers_tabular_evidence and ("|" in content or "表" in content):
        score += 12
    if "# 结构化表格补充" in content:
        score += 10 if prefers_tabular_evidence else 4
    if "# 问答线索补充" in content:
        score += 8
    if "# 章节导航" in content[:260]:
        score -= 20
    if any(marker in content for marker in ("第", "条", "：", "；", "-", "•")):
        score += 4

    return score, matched_terms


def _extract_local_query_terms(query: str) -> list[str]:
    normalized_query = str(query or "").strip().lower()
    if not normalized_query:
        return []

    terms: list[str] = []
    seen: set[str] = set()

    def remember(term: str) -> None:
        normalized_term = str(term or "").strip().lower()
        if (
            not normalized_term
            or normalized_term in seen
            or normalized_term in LOCAL_QUERY_STOPWORDS
            or len(normalized_term) < 2
        ):
            return
        seen.add(normalized_term)
        terms.append(normalized_term)

    for item in re.findall(r"[a-z0-9][a-z0-9_\-]{1,}", normalized_query):
        remember(item)

    for item in LOCAL_DOMAIN_TERMS:
        if item in normalized_query:
            remember(item)

    for block in re.findall(r"[\u4e00-\u9fff]{2,24}", normalized_query):
        for marker in ("标准", "金额", "限额", "额度"):
            marker_index = block.find(marker)
            if marker_index <= 0:
                continue
            subject = block[:marker_index]
            for width in (6, 4, 3, 2):
                remember(subject[-width:])
        for anchor in LOCAL_QUERY_ANCHOR_TERMS:
            if anchor in block:
                remember(anchor)
        tail = block[-14:]
        for size in (8, 7, 6, 5, 4):
            for start in range(0, len(tail) - size + 1):
                piece = tail[start : start + size]
                if any(anchor in piece for anchor in LOCAL_QUERY_ANCHOR_TERMS):
                    remember(piece)
                    if len(terms) >= MAX_LOCAL_QUERY_TERMS:
                        return terms
        if len(block) <= 4:
            remember(block)
            continue
        for size in (4, 3, 2, 5):
            for start in range(0, len(block) - size + 1):
                remember(block[start : start + size])
                if len(terms) >= MAX_LOCAL_QUERY_TERMS:
                    return terms

    return terms[:MAX_LOCAL_QUERY_TERMS]


def _parse_document_identity(file_path: str, *, fallback_doc_id: str = "") -> tuple[str, str]:
    path = Path(str(file_path or "").strip())
    name = path.name
    if "__" not in name:
        return fallback_doc_id, name
    document_id, document_name = name.split("__", maxsplit=1)
    return document_id.strip() or fallback_doc_id, document_name.strip()


def _build_query_focused_excerpt(
    text: str,
    *,
    query_terms: list[str],
    max_length: int,
) -> str:
    normalized = " ".join(str(text or "").split()).strip()
    if not normalized:
        return ""
    lowered = normalized.lower()
    match_positions = [
        lowered.find(term) for term in query_terms if term and lowered.find(term) >= 0
    ]
    if not match_positions:
        return _truncate_text(normalized, max_length=max_length)

    start = max(0, min(match_positions) - max_length // 3)
    end = min(len(normalized), start + max_length)
    snippet = normalized[start:end].strip()
    if start > 0:
        snippet = f"...{snippet.lstrip()}"
    if end < len(normalized):
        snippet = f"{snippet.rstrip()}..."
    return snippet


def _truncate_text(text: str, *, max_length: int) -> str:
    normalized = str(text or "").strip()
    if len(normalized) <= max_length:
        return normalized
    return f"{normalized[: max_length - 3].rstrip()}..."
