"""Content indexing and search for novel projects."""

import hashlib
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, Iterable, List, Optional, Tuple

import markdown
import yaml


FRONT_MATTER_RE = re.compile(r"^---\s*\r?\n(.*?)\r?\n---\s*\r?\n", re.DOTALL)
SECTION_ORDER = {
    "正文": 0,
    "草稿": 1,
    "大纲": 2,
    "主题": 3,
    "角色": 4,
    "事件": 5,
    "物品": 6,
    "场景": 7,
}


@dataclass
class ContentItem:
    """Represents a content item in the novel project."""

    item_id: str
    title: str
    label: str
    section: str
    item_type: str
    path: str
    body_markdown: str
    body_html: str
    meta: Dict
    chapter_number: Optional[int]
    characters: List[str]
    events: List[str]
    word_count: Optional[int]
    timeline: Optional[str]
    search_text: str

    def summary(self) -> Dict:
        """Return summary dict for API response."""
        return {
            "id": self.item_id,
            "title": self.title,
            "label": self.label,
            "section": self.section,
            "type": self.item_type,
            "chapter_number": self.chapter_number,
            "characters": self.characters,
            "events": self.events,
            "word_count": self.word_count,
            "timeline": self.timeline,
            "path": self.path,
        }

    def detail(self) -> Dict:
        """Return detailed dict for API response."""
        payload = self.summary()
        payload.update({
            "meta": self.meta,
            "body_html": self.body_html,
            "body_markdown": self.body_markdown,
        })
        return payload


def normalize_list(value) -> List[str]:
    """Normalize a value to a list of strings."""
    if value is None:
        return []
    if isinstance(value, (list, tuple)):
        return [str(item).strip() for item in value if str(item).strip()]
    text = str(value).strip()
    return [text] if text else []


def safe_int(value) -> Optional[int]:
    """Safely convert a value to int."""
    if value is None:
        return None
    try:
        return int(str(value).strip())
    except (ValueError, TypeError):
        return None


def extract_front_matter(text: str) -> Tuple[Dict, str]:
    """Extract YAML front matter from text."""
    match = FRONT_MATTER_RE.match(text)
    if not match:
        return {}, text
    meta_raw = match.group(1)
    body = text[match.end():]
    try:
        meta = yaml.safe_load(meta_raw) or {}
    except yaml.YAMLError:
        meta = {}
    if not isinstance(meta, dict):
        meta = {}
    return meta, body


def strip_md_heading(body: str) -> Optional[str]:
    """Extract first heading from markdown body."""
    for line in body.splitlines():
        if line.startswith("#"):
            return line.lstrip("#").strip()
    return None


def normalize_text(text: str) -> str:
    """Normalize text for search."""
    return " ".join(str(text).split()).lower()


def build_search_text(title: str, path: str, body: str, meta: Dict) -> str:
    """Build searchable text from item data."""
    meta_parts: List[str] = []
    for key, value in meta.items():
        meta_parts.append(str(key))
        if isinstance(value, (list, tuple)):
            meta_parts.extend(str(item) for item in value)
        else:
            meta_parts.append(str(value))
    combined = "\n".join([title, path, body, " ".join(meta_parts)])
    return normalize_text(combined)


def make_excerpt(text: str, terms: List[str], length: int = 160) -> str:
    """Create an excerpt with search term highlighting."""
    text = " ".join(text.split())
    if not text:
        return ""
    if not terms:
        return text[:length] + ("..." if len(text) > length else "")
    lowered = text.lower()
    indices = [lowered.find(term) for term in terms if term]
    indices = [idx for idx in indices if idx >= 0]
    if not indices:
        return text[:length] + ("..." if len(text) > length else "")
    idx = min(indices)
    start = max(0, idx - length // 3)
    end = min(len(text), start + length)
    snippet = text[start:end]
    if start > 0:
        snippet = "..." + snippet
    if end < len(text):
        snippet = snippet + "..."
    return snippet


def sort_key(item: ContentItem) -> Tuple:
    """Sort key for content items."""
    return (
        SECTION_ORDER.get(item.section, 99),
        0 if item.item_type == "chapter" else 1,
        item.chapter_number if item.chapter_number is not None else 9999,
        item.title,
    )


class ContentIndex:
    """Index and search content in a novel project."""

    def __init__(self, root: Path):
        self.root = root
        self.items: Dict[str, ContentItem] = {}
        self.md = markdown.Markdown(extensions=["extra", "tables", "fenced_code"])

    def load(self) -> None:
        """Load all content from the project."""
        self.items = {}
        self._load_chapters("正文")
        self._load_chapters("草稿")
        self._load_docs()
        self._load_info()

    def _load_chapters(self, folder: str) -> None:
        """Load chapter files from a folder."""
        base = self.root / folder
        if not base.exists():
            return
        for path in sorted(base.glob("*.md")):
            self._load_item(path, section=folder, item_type="chapter")

    def _load_docs(self) -> None:
        """Load document files."""
        docs = [
            (self.root / "大纲" / "整体大纲.md", "大纲"),
            (self.root / "小说" / "主题与基调.md", "主题"),
        ]
        for path, section in docs:
            if path.exists():
                self._load_item(path, section=section, item_type="doc")

    def _load_info(self) -> None:
        """Load info files from knowledge base."""
        info_root = self.root / "现有信息"
        if not info_root.exists():
            return
        for section in ["角色", "物品", "场景", "事件"]:
            folder = info_root / section
            if not folder.exists():
                continue
            for path in sorted(folder.glob("*.md")):
                self._load_item(path, section=section, item_type="info")

    def _load_item(self, path: Path, section: str, item_type: str) -> None:
        """Load a single content item."""
        text = path.read_text(encoding="utf-8", errors="replace")
        meta, body = extract_front_matter(text)
        title = str(meta.get("title") or meta.get("name") or "").strip()
        if not title:
            title = strip_md_heading(body) or path.stem

        chapter_number = safe_int(meta.get("chapter_number"))
        if chapter_number is None:
            match = re.match(r"^(\d+)", path.stem)
            chapter_number = safe_int(match.group(1)) if match else None

        characters = normalize_list(meta.get("characters") or meta.get("participants"))
        events = normalize_list(meta.get("events"))
        word_count = safe_int(meta.get("word_count"))
        timeline = str(meta.get("timeline") or "").strip() or None

        label = title
        if item_type == "chapter" and chapter_number is not None:
            label = f"第{chapter_number}章 {title}"

        body_html = self.md.reset().convert(body)
        rel_path = str(path.relative_to(self.root))
        item_id = hashlib.sha1(rel_path.encode("utf-8")).hexdigest()[:12]
        search_text = build_search_text(title, rel_path, body, meta)

        item = ContentItem(
            item_id=item_id,
            title=title,
            label=label,
            section=section,
            item_type=item_type,
            path=rel_path,
            body_markdown=body.strip(),
            body_html=body_html,
            meta=meta,
            chapter_number=chapter_number,
            characters=characters,
            events=events,
            word_count=word_count,
            timeline=timeline,
            search_text=search_text,
        )
        self.items[item_id] = item

    def list_items(self) -> List[ContentItem]:
        """List all items sorted."""
        return sorted(self.items.values(), key=sort_key)

    def find_item(self, item_id: str) -> Optional[ContentItem]:
        """Find an item by ID."""
        return self.items.get(item_id)

    def filters(self) -> Dict:
        """Get available filter values."""
        characters: List[str] = []
        events: List[str] = []
        for item in self.items.values():
            if item.item_type != "chapter":
                continue
            characters.extend(item.characters)
            events.extend(item.events)
        return {
            "characters": sorted(set(characters)),
            "events": sorted(set(events)),
        }

    def totals(self) -> Dict:
        """Get totals by section."""
        totals = {
            "sections": {},
            "words": {},
        }
        for item in self.items.values():
            totals["sections"].setdefault(item.section, 0)
            totals["sections"][item.section] += 1
            if item.item_type == "chapter":
                totals["words"].setdefault(item.section, 0)
                totals["words"][item.section] += item.word_count or 0
        return totals

    def search(
        self,
        query: str,
        sections: List[str],
        characters: List[str],
        events: List[str],
        chapter_min: Optional[int],
        chapter_max: Optional[int],
    ) -> List[ContentItem]:
        """Search items with filters."""
        query = normalize_text(query)
        terms = [term for term in query.split(" ") if term]
        section_set = set(sections)
        char_set = set(characters)
        event_set = set(events)

        results: List[ContentItem] = []
        for item in self.items.values():
            if section_set and item.section not in section_set:
                continue
            if char_set and not (char_set & set(item.characters)):
                continue
            if event_set and not (event_set & set(item.events)):
                continue
            if chapter_min is not None or chapter_max is not None:
                if item.chapter_number is None:
                    continue
                if chapter_min is not None and item.chapter_number < chapter_min:
                    continue
                if chapter_max is not None and item.chapter_number > chapter_max:
                    continue
            if terms and not all(term in item.search_text for term in terms):
                continue
            results.append(item)
        return sorted(results, key=sort_key)


def parse_list_param(values: Iterable[str]) -> List[str]:
    """Parse comma-separated list parameters."""
    merged: List[str] = []
    for value in values:
        if not value:
            continue
        for part in value.split(","):
            part = part.strip()
            if part:
                merged.append(part)
    return merged
