"""
ShelfAI index manager.

Handles reading, writing, and querying the shelf/index.md file.
The index is a plain markdown file with one-line abstracts for every shelf item.
"""

import re
from dataclasses import dataclass
from pathlib import Path
from typing import Optional


@dataclass
class IndexEntry:
    """A single entry in the index."""

    file_path: str  # Relative path from shelf root, e.g. "skills/code_review.md"
    abstract: str  # One-line abstract text
    category: str  # Section header this entry lives under, e.g. "Skills"

    def to_markdown(self) -> str:
        """Render as a markdown list item."""
        return f"- **{self.file_path}** — {self.abstract}"


class Index:
    """
    Parser and writer for shelf/index.md.

    The index file has the following structure:

        # ShelfAI Index

        ## Skills
        - **skills/code_review.md** — Use when asked to review code...
        - **skills/data_analysis.md** — Use when given a dataset...

        ## Knowledge
        - **knowledge/api_docs.md** — Internal API reference...

    This class provides methods to parse, query, add, update, and remove entries.
    """

    # Pattern matching: - **path/to/file.md** — abstract text
    ENTRY_PATTERN = re.compile(
        r"^- \*\*(.+?)\*\* — (.+)$",
        re.MULTILINE,
    )

    # Pattern matching: ## Category Name
    SECTION_PATTERN = re.compile(r"^## (.+)$", re.MULTILINE)

    def __init__(self, path: Path):
        """
        Initialize the index.

        Args:
            path: Path to the index.md file
        """
        self.path = path
        self._entries: list[IndexEntry] = []
        self._raw: str = ""

        if path.exists():
            self.reload()

    def reload(self):
        """Reload the index from disk."""
        self._raw = self.path.read_text(encoding="utf-8")
        self._entries = self._parse(self._raw)

    def _parse(self, content: str) -> list[IndexEntry]:
        """Parse index.md content into IndexEntry objects."""
        entries = []
        current_category = "Uncategorized"

        for line in content.split("\n"):
            # Check for section header
            section_match = self.SECTION_PATTERN.match(line)
            if section_match:
                current_category = section_match.group(1).strip()
                continue

            # Check for entry
            entry_match = self.ENTRY_PATTERN.match(line.strip())
            if entry_match:
                file_path = entry_match.group(1).strip()
                abstract = entry_match.group(2).strip()
                entries.append(
                    IndexEntry(
                        file_path=file_path,
                        abstract=abstract,
                        category=current_category,
                    )
                )

        return entries

    @property
    def entries(self) -> list[IndexEntry]:
        """All entries in the index."""
        return list(self._entries)

    @property
    def categories(self) -> list[str]:
        """Unique categories in the index, in order of appearance."""
        seen = []
        for entry in self._entries:
            if entry.category not in seen:
                seen.append(entry.category)
        return seen

    @property
    def count(self) -> int:
        """Total number of entries."""
        return len(self._entries)

    def get(self, file_path: str) -> Optional[IndexEntry]:
        """
        Get an entry by its file path.

        Args:
            file_path: Relative path, e.g. "skills/code_review.md"

        Returns:
            The matching IndexEntry, or None if not found
        """
        for entry in self._entries:
            if entry.file_path == file_path:
                return entry
        return None

    def search(self, query: str, top_n: int = 5) -> list[IndexEntry]:
        """
        Search abstracts by keyword matching.

        Simple keyword search — splits the query into words and scores entries
        by how many query words appear in the abstract or file path.
        Case-insensitive.

        Args:
            query: Search query string
            top_n: Maximum number of results

        Returns:
            List of matching IndexEntry objects, sorted by relevance
        """
        query_words = query.lower().split()
        if not query_words:
            return []

        scored = []
        for entry in self._entries:
            searchable = f"{entry.file_path} {entry.abstract} {entry.category}".lower()
            score = sum(1 for word in query_words if word in searchable)
            if score > 0:
                scored.append((score, entry))

        scored.sort(key=lambda x: x[0], reverse=True)
        return [entry for _, entry in scored[:top_n]]

    def add(self, file_path: str, abstract: str, category: str):
        """
        Add a new entry to the index.

        If the category section doesn't exist, it will be created.
        If an entry with the same file_path already exists, it will be updated.

        Args:
            file_path: Relative path from shelf root
            abstract: One-line abstract
            category: Category section name (e.g. "Skills")
        """
        # Check for existing entry
        existing = self.get(file_path)
        if existing:
            self.update(file_path, abstract)
            return

        self._entries.append(
            IndexEntry(file_path=file_path, abstract=abstract, category=category)
        )
        self._write()

    def update(self, file_path: str, new_abstract: str):
        """
        Update the abstract for an existing entry.

        Args:
            file_path: Relative path of the entry to update
            new_abstract: New abstract text

        Raises:
            KeyError: If no entry exists with the given file_path
        """
        for entry in self._entries:
            if entry.file_path == file_path:
                entry.abstract = new_abstract
                self._write()
                return

        raise KeyError(f"No index entry found for: {file_path}")

    def remove(self, file_path: str):
        """
        Remove an entry from the index.

        Args:
            file_path: Relative path of the entry to remove

        Raises:
            KeyError: If no entry exists with the given file_path
        """
        original_count = len(self._entries)
        self._entries = [e for e in self._entries if e.file_path != file_path]

        if len(self._entries) == original_count:
            raise KeyError(f"No index entry found for: {file_path}")

        self._write()

    def entries_by_category(self, category: str) -> list[IndexEntry]:
        """Get all entries in a given category."""
        return [e for e in self._entries if e.category == category]

    def unindexed_files(self, shelf_files: list[str]) -> list[str]:
        """
        Find shelf files that don't have an index entry.

        Args:
            shelf_files: List of relative file paths in the shelf

        Returns:
            List of file paths that are not in the index
        """
        indexed_paths = {e.file_path for e in self._entries}
        return [f for f in shelf_files if f not in indexed_paths]

    def stale_entries(self, shelf_files: list[str]) -> list[IndexEntry]:
        """
        Find index entries whose files no longer exist.

        Args:
            shelf_files: List of relative file paths currently in the shelf

        Returns:
            List of IndexEntry objects pointing to non-existent files
        """
        file_set = set(shelf_files)
        return [e for e in self._entries if e.file_path not in file_set]

    def to_markdown(self) -> str:
        """Render the full index as markdown."""
        lines = ["# ShelfAI Index", ""]

        for category in self.categories:
            lines.append(f"## {category}")
            for entry in self.entries_by_category(category):
                lines.append(entry.to_markdown())
            lines.append("")

        return "\n".join(lines)

    def _write(self):
        """Write the current state to disk."""
        self.path.write_text(self.to_markdown(), encoding="utf-8")
        self._raw = self.path.read_text(encoding="utf-8")

    def __repr__(self) -> str:
        return f"Index({self.count} entries across {len(self.categories)} categories)"
