"""ACMS Index Data Model and File Traversal Engine.

Provides the foundational data model for indexed context entries and a
file traversal engine that can handle 10,000+ files without timeout using
chunked processing.

Based on issue #9579 and ``docs/specification.md`` ~lines 44405-44420.
"""

from __future__ import annotations

from collections.abc import Iterator
from datetime import datetime
from enum import StrEnum
from pathlib import Path

from pydantic import BaseModel, Field


class FileType(StrEnum):
    """File type enumeration for index entries."""

    PYTHON = "python"
    JAVASCRIPT = "javascript"
    TYPESCRIPT = "typescript"
    JAVA = "java"
    RUST = "rust"
    MARKDOWN = "markdown"
    TEXT = "text"
    JSON = "json"
    YAML = "yaml"
    XML = "xml"
    OTHER = "other"


class TierLevel(StrEnum):
    """Tier assignment levels for context entries.

    Aligns with the hot/warm/cold storage tier vocabulary used throughout
    the ACMS specification and milestone v3.4.0 acceptance criteria.
    """

    HOT = "hot"  # Core/essential
    WARM = "warm"  # Important
    COLD = "cold"  # Supporting
    ARCHIVE = "archive"  # Reference


class IndexEntry(BaseModel):
    """Represents a single indexed context entry.

    Attributes:
        path: Absolute or relative file path
        file_type: Type of file (from FileType enum)
        size_bytes: File size in bytes
        created_at: File creation timestamp
        modified_at: File modification timestamp
        tags: Set of tags for categorization
        tier: Tier assignment level
        metadata: Additional metadata dictionary
    """

    path: str
    file_type: FileType
    size_bytes: int
    created_at: datetime
    modified_at: datetime
    tags: set[str] = Field(default_factory=set)
    tier: TierLevel = TierLevel.COLD
    metadata: dict[str, str] = Field(default_factory=dict)

    def add_tag(self, tag: str) -> None:
        """Add a tag to this entry.

        Args:
            tag: Tag string to add. Must be non-empty.

        Raises:
            ValueError: If tag is empty or whitespace-only.
        """
        if not tag or not tag.strip():
            raise ValueError("tag must be a non-empty string")
        self.tags.add(tag)

    def remove_tag(self, tag: str) -> None:
        """Remove a tag from this entry.

        Args:
            tag: Tag string to remove. Must be non-empty.

        Raises:
            ValueError: If tag is empty or whitespace-only.
        """
        if not tag or not tag.strip():
            raise ValueError("tag must be a non-empty string")
        self.tags.discard(tag)

    def has_tag(self, tag: str) -> bool:
        """Check if entry has a specific tag."""
        return tag in self.tags

    def set_tier(self, tier: TierLevel) -> None:
        """Set the tier level for this entry."""
        self.tier = tier


class ACMSIndex:
    """ACMS Index for storing and querying indexed context entries.

    Provides storage and query capabilities for indexed files with support
    for filtering by path, tags, type, and recency.

    Attributes:
        entries: Dictionary mapping file paths to IndexEntry objects
    """

    def __init__(self) -> None:
        self.entries: dict[str, IndexEntry] = {}

    def add_entry(self, entry: IndexEntry) -> None:
        """Add an index entry to the index.

        Args:
            entry: The IndexEntry to add. Must not be None.

        Raises:
            TypeError: If entry is not an IndexEntry instance.
        """
        if not isinstance(entry, IndexEntry):
            raise TypeError(
                f"entry must be an IndexEntry instance, got {type(entry).__name__}"
            )
        self.entries[entry.path] = entry

    def remove_entry(self, path: str) -> bool:
        """Remove an entry by path. Returns True if removed, False if not found."""
        if path in self.entries:
            del self.entries[path]
            return True
        return False

    def get_entry(self, path: str) -> IndexEntry | None:
        """Get an entry by path."""
        return self.entries.get(path)

    def query_by_path(self, path_pattern: str) -> list[IndexEntry]:
        """Query entries by path pattern (substring match).

        Args:
            path_pattern: Substring pattern to match against entry paths.
                Must be non-empty.

        Raises:
            ValueError: If path_pattern is empty or whitespace-only.
        """
        if not path_pattern or not path_pattern.strip():
            raise ValueError("path_pattern must be a non-empty string")
        return [entry for entry in self.entries.values() if path_pattern in entry.path]

    def query_by_tag(self, tag: str) -> list[IndexEntry]:
        """Query entries by tag."""
        return [entry for entry in self.entries.values() if entry.has_tag(tag)]

    def query_by_type(self, file_type: FileType) -> list[IndexEntry]:
        """Query entries by file type."""
        return [
            entry for entry in self.entries.values() if entry.file_type == file_type
        ]

    def query_by_tier(self, tier: TierLevel) -> list[IndexEntry]:
        """Query entries by tier level."""
        return [entry for entry in self.entries.values() if entry.tier == tier]

    def query_by_recency(
        self, after: datetime, before: datetime | None = None
    ) -> list[IndexEntry]:
        """Query entries by modification recency.

        Args:
            after: Return entries modified after this datetime.
            before: Return entries modified before this datetime (optional).
                When both are provided, before must be >= after.

        Returns:
            List of entries matching the recency criteria.

        Raises:
            ValueError: If before is earlier than after when both are provided.
        """
        if before is not None and before < after:
            raise ValueError(
                f"before ({before}) must be >= after ({after}) when both are provided"
            )
        results = [
            entry for entry in self.entries.values() if entry.modified_at >= after
        ]
        if before:
            results = [entry for entry in results if entry.modified_at <= before]
        return results

    def query_combined(
        self,
        path_pattern: str | None = None,
        tags: set[str] | None = None,
        file_type: FileType | None = None,
        tier: TierLevel | None = None,
        after: datetime | None = None,
    ) -> list[IndexEntry]:
        """Query entries with multiple filters (AND logic).

        Args:
            path_pattern: Filter by path pattern
            tags: Filter by any of these tags
            file_type: Filter by file type
            tier: Filter by tier level
            after: Filter by modification date

        Returns:
            List of entries matching all specified criteria
        """
        results = list(self.entries.values())

        if path_pattern:
            results = [entry for entry in results if path_pattern in entry.path]

        if tags:
            results = [
                entry for entry in results if any(tag in entry.tags for tag in tags)
            ]

        if file_type:
            results = [entry for entry in results if entry.file_type == file_type]

        if tier:
            results = [entry for entry in results if entry.tier == tier]

        if after:
            results = [entry for entry in results if entry.modified_at >= after]

        return results

    def get_all_entries(self) -> list[IndexEntry]:
        """Get all entries in the index."""
        return list(self.entries.values())

    def get_entry_count(self) -> int:
        """Get the total number of entries in the index."""
        return len(self.entries)


class FileTraversalEngine:
    """Engine for traversing and indexing files in large projects.

    Handles 10,000+ files without timeout using chunked processing to
    prevent memory exhaustion.

    Attributes:
        chunk_size: Number of files to process in each chunk
        index: The ACMS index to populate
    """

    def __init__(self, chunk_size: int = 100, index: ACMSIndex | None = None) -> None:
        """Initialize the traversal engine.

        Args:
            chunk_size: Number of files to process per chunk (default: 100).
                Must be a positive integer.
            index: Optional pre-populated ACMSIndex to use. If None, a new
                empty index is created (supports Dependency Inversion).

        Raises:
            ValueError: If chunk_size is not a positive integer.
        """
        if chunk_size <= 0:
            raise ValueError(f"chunk_size must be a positive integer, got {chunk_size}")
        self.chunk_size = chunk_size
        self.index = index if index is not None else ACMSIndex()

    def _get_file_type(self, file_path: Path) -> FileType:
        """Determine file type from extension."""
        suffix = file_path.suffix.lower()
        type_map = {
            ".py": FileType.PYTHON,
            ".js": FileType.JAVASCRIPT,
            ".ts": FileType.TYPESCRIPT,
            ".tsx": FileType.TYPESCRIPT,
            ".java": FileType.JAVA,
            ".rs": FileType.RUST,
            ".md": FileType.MARKDOWN,
            ".txt": FileType.TEXT,
            ".json": FileType.JSON,
            ".yaml": FileType.YAML,
            ".yml": FileType.YAML,
            ".xml": FileType.XML,
        }
        return type_map.get(suffix, FileType.OTHER)

    def _create_index_entry(self, file_path: Path) -> IndexEntry | None:
        """Create an index entry from a file path.

        Args:
            file_path: Path to the file

        Returns:
            IndexEntry if successful, None if file cannot be read
        """
        try:
            stat = file_path.stat()
            return IndexEntry(
                path=str(file_path),
                file_type=self._get_file_type(file_path),
                size_bytes=stat.st_size,
                created_at=datetime.fromtimestamp(stat.st_ctime),
                modified_at=datetime.fromtimestamp(stat.st_mtime),
            )
        except (OSError, ValueError):
            return None

    def _traverse_directory(
        self,
        root_path: Path,
    ) -> Iterator[Path]:
        """Recursively traverse directory and yield file paths.

        Args:
            root_path: Root directory to traverse

        Yields:
            Path objects for each file found
        """
        try:
            for item in root_path.iterdir():
                if item.is_file():
                    yield item
                elif item.is_dir():
                    # Recursively traverse subdirectories
                    yield from self._traverse_directory(item)
        except (OSError, PermissionError):
            # Skip directories we can't read
            pass

    def traverse_and_index(
        self,
        root_path: str | Path,
        exclude_patterns: list[str] | None = None,
    ) -> ACMSIndex:
        """Traverse a directory and index all files.

        Uses chunked processing to handle large projects without timeout
        or memory exhaustion.

        Args:
            root_path: Root directory to traverse
            exclude_patterns: List of patterns to exclude
                (e.g., ['.git', '__pycache__'])

        Returns:
            Populated ACMSIndex
        """
        root = Path(root_path)
        if not root.exists():
            raise ValueError(f"Path does not exist: {root_path}")

        exclude_patterns = exclude_patterns or []
        chunk: list[IndexEntry] = []

        for file_path in self._traverse_directory(root):
            # Check if file matches any exclude pattern
            if any(pattern in str(file_path) for pattern in exclude_patterns):
                continue

            # Create index entry
            entry = self._create_index_entry(file_path)
            if entry:
                chunk.append(entry)

            # Process chunk when it reaches the size limit
            if len(chunk) >= self.chunk_size:
                self._process_chunk(chunk)
                chunk = []

        # Process remaining entries
        if chunk:
            self._process_chunk(chunk)

        return self.index

    def _process_chunk(self, chunk: list[IndexEntry]) -> None:
        """Process a chunk of index entries.

        Args:
            chunk: List of IndexEntry objects to add to the index
        """
        for entry in chunk:
            self.index.add_entry(entry)

    def get_index(self) -> ACMSIndex:
        """Get the current index."""
        return self.index

    def reset_index(self) -> None:
        """Reset the index to empty state."""
        self.index = ACMSIndex()


__all__ = [
    "ACMSIndex",
    "FileTraversalEngine",
    "FileType",
    "IndexEntry",
    "TierLevel",
]
