"""Path mapping utilities for source paths and index paths.

This module provides bidirectional mapping between source code directories
and their corresponding index storage locations.

Storage Structure:
    ~/.codexlens/
    ├── registry.db                    # Global mapping table
    └── indexes/
        └── D/
            └── Claude_dms3/
                ├── _index.db          # Root directory index
                └── src/
                    └── _index.db      # src/ directory index
"""

import json
import os
import platform
from pathlib import Path
from typing import Optional


def _get_configured_index_root() -> Path:
    """Get the index root from environment or config file.

    Priority order:
    1. CODEXLENS_INDEX_DIR environment variable
    2. index_dir from ~/.codexlens/config.json
    3. Default: ~/.codexlens/indexes
    """
    env_override = os.getenv("CODEXLENS_INDEX_DIR")
    if env_override:
        return Path(env_override).expanduser().resolve()

    config_file = Path.home() / ".codexlens" / "config.json"
    if config_file.exists():
        try:
            cfg = json.loads(config_file.read_text(encoding="utf-8"))
            if "index_dir" in cfg:
                return Path(cfg["index_dir"]).expanduser().resolve()
        except (json.JSONDecodeError, OSError):
            pass

    return Path.home() / ".codexlens" / "indexes"


class PathMapper:
    """Bidirectional mapping tool for source paths ↔ index paths.

    Handles cross-platform path normalization and conversion between
    source code directories and their index storage locations.

    Attributes:
        DEFAULT_INDEX_ROOT: Default root directory for all indexes
        INDEX_DB_NAME: Standard name for index database files
        index_root: Configured index root directory
    """

    DEFAULT_INDEX_ROOT = _get_configured_index_root()
    INDEX_DB_NAME = "_index.db"

    def __init__(self, index_root: Optional[Path] = None):
        """Initialize PathMapper with optional custom index root.

        Args:
            index_root: Custom index root directory. If None, uses DEFAULT_INDEX_ROOT.
        """
        self.index_root = (index_root or self.DEFAULT_INDEX_ROOT).resolve()

    def source_to_index_dir(self, source_path: Path) -> Path:
        """Convert source directory to its index directory path.

        Maps a source code directory to where its index data should be stored.
        The mapping preserves the directory structure but normalizes paths
        for cross-platform compatibility.

        Args:
            source_path: Source directory path to map

        Returns:
            Index directory path under index_root

        Examples:
            >>> mapper = PathMapper()
            >>> mapper.source_to_index_dir(Path("D:/Claude_dms3/src"))
            PosixPath('/home/user/.codexlens/indexes/D/Claude_dms3/src')

            >>> mapper.source_to_index_dir(Path("/home/user/project"))
            PosixPath('/home/user/.codexlens/indexes/home/user/project')
        """
        source_path = source_path.resolve()
        normalized = self.normalize_path(source_path)
        return self.index_root / normalized

    def source_to_index_db(self, source_path: Path) -> Path:
        """Convert source directory to its index database file path.

        Maps a source directory to the full path of its index database file,
        including the standard INDEX_DB_NAME.

        Args:
            source_path: Source directory path to map

        Returns:
            Full path to the index database file

        Examples:
            >>> mapper = PathMapper()
            >>> mapper.source_to_index_db(Path("D:/Claude_dms3/src"))
            PosixPath('/home/user/.codexlens/indexes/D/Claude_dms3/src/_index.db')
        """
        index_dir = self.source_to_index_dir(source_path)
        return index_dir / self.INDEX_DB_NAME

    def index_to_source(self, index_path: Path) -> Path:
        """Convert index path back to original source path.

        Performs reverse mapping from an index storage location to the
        original source directory. Handles both directory paths and
        database file paths.

        Args:
            index_path: Index directory or database file path

        Returns:
            Original source directory path

        Raises:
            ValueError: If index_path is not under index_root

        Examples:
            >>> mapper = PathMapper()
            >>> mapper.index_to_source(
            ...     Path("~/.codexlens/indexes/D/Claude_dms3/src/_index.db")
            ... )
            WindowsPath('D:/Claude_dms3/src')

            >>> mapper.index_to_source(
            ...     Path("~/.codexlens/indexes/D/Claude_dms3/src")
            ... )
            WindowsPath('D:/Claude_dms3/src')
        """
        index_path = index_path.resolve()

        # Remove _index.db if present
        if index_path.name == self.INDEX_DB_NAME:
            index_path = index_path.parent

        # Verify path is under index_root
        try:
            relative = index_path.relative_to(self.index_root)
        except ValueError:
            raise ValueError(
                f"Index path {index_path} is not under index root {self.index_root}"
            )

        # Convert normalized path back to source path
        normalized_str = str(relative).replace("\\", "/")
        return self.denormalize_path(normalized_str)

    def get_project_root(self, source_path: Path) -> Path:
        """Find the project root directory (topmost indexed directory).

        Walks up the directory tree to find the highest-level directory
        that has an index database.

        Args:
            source_path: Source directory to start from

        Returns:
            Project root directory path. Returns source_path itself if
            no parent index is found.

        Examples:
            >>> mapper = PathMapper()
            >>> mapper.get_project_root(Path("D:/Claude_dms3/src/codexlens"))
            WindowsPath('D:/Claude_dms3')
        """
        source_path = source_path.resolve()
        current = source_path
        project_root = source_path

        # Walk up the tree
        while current.parent != current:  # Stop at filesystem root
            parent_index_db = self.source_to_index_db(current.parent)
            if parent_index_db.exists():
                project_root = current.parent
                current = current.parent
            else:
                break

        return project_root

    def get_relative_depth(self, source_path: Path, project_root: Path) -> int:
        """Calculate directory depth relative to project root.

        Args:
            source_path: Target directory path
            project_root: Project root directory path

        Returns:
            Number of directory levels from project_root to source_path

        Raises:
            ValueError: If source_path is not under project_root

        Examples:
            >>> mapper = PathMapper()
            >>> mapper.get_relative_depth(
            ...     Path("D:/Claude_dms3/src/codexlens"),
            ...     Path("D:/Claude_dms3")
            ... )
            2
        """
        source_path = source_path.resolve()
        project_root = project_root.resolve()

        try:
            relative = source_path.relative_to(project_root)
            # Count path components
            return len(relative.parts)
        except ValueError:
            raise ValueError(
                f"Source path {source_path} is not under project root {project_root}"
            )

    def normalize_path(self, path: Path) -> str:
        """Normalize path to cross-platform storage format.

        Converts OS-specific paths to a standardized format for storage:
        - Windows: Removes drive colons (D: → D)
        - Unix: Removes leading slash
        - Uses forward slashes throughout

        Args:
            path: Path to normalize

        Returns:
            Normalized path string

        Examples:
            >>> mapper = PathMapper()
            >>> mapper.normalize_path(Path("D:/path/to/dir"))
            'D/path/to/dir'

            >>> mapper.normalize_path(Path("/home/user/path"))
            'home/user/path'
        """
        path = path.resolve()
        path_str = str(path)

        # Handle Windows paths with drive letters
        if platform.system() == "Windows" and len(path.parts) > 0:
            # Convert D:\path\to\dir → D/path/to/dir
            drive = path.parts[0].replace(":", "")  # D: → D
            rest = Path(*path.parts[1:]) if len(path.parts) > 1 else Path()
            normalized = f"{drive}/{rest}".replace("\\", "/")
            return normalized.rstrip("/")

        # Handle Unix paths
        # /home/user/path → home/user/path
        return path_str.lstrip("/").replace("\\", "/")

    def denormalize_path(self, normalized: str) -> Path:
        """Convert normalized path back to OS-specific path.

        Reverses the normalization process to restore OS-native path format:
        - Windows: Adds drive colons (D → D:)
        - Unix: Adds leading slash

        Args:
            normalized: Normalized path string

        Returns:
            OS-specific Path object

        Examples:
            >>> mapper = PathMapper()
            >>> mapper.denormalize_path("D/path/to/dir")  # On Windows
            WindowsPath('D:/path/to/dir')

            >>> mapper.denormalize_path("home/user/path")  # On Unix
            PosixPath('/home/user/path')
        """
        parts = normalized.split("/")

        # Handle Windows paths
        if platform.system() == "Windows" and len(parts) > 0:
            # Check if first part is a drive letter
            if len(parts[0]) == 1 and parts[0].isalpha():
                # D/path/to/dir → D:/path/to/dir
                drive = f"{parts[0]}:/"
                if len(parts) > 1:
                    return Path(drive) / Path(*parts[1:])
                return Path(drive)

        # Handle Unix paths or relative paths
        # home/user/path → /home/user/path
        return Path("/") / Path(*parts)
