"""
Config Loader - Load groknroll configuration from multiple sources

Loads configuration from (in priority order):
1. Project: `./groknroll.json`
2. Global: `~/.groknroll/groknroll.json`
3. Environment: `GROKNROLL_CONFIG` path

Supports JSONC (JSON with comments).

Examples:
    loader = ConfigLoader(project_path=Path.cwd())
    config = loader.load()  # Returns merged config dict

    # Or use convenience function
    config = load_config()
"""

import json
import os
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Optional


@dataclass
class ConfigSource:
    """
    Represents a configuration source

    Attributes:
        path: Path to the config file
        exists: Whether the file exists
        config: Loaded configuration (if exists)
        error: Error message (if loading failed)
    """

    path: Path
    exists: bool = False
    config: Optional[dict[str, Any]] = None
    error: Optional[str] = None


@dataclass
class ConfigLoadResult:
    """
    Result of configuration loading

    Attributes:
        config: The loaded configuration dict
        sources: List of sources that were checked
        success: Whether loading succeeded
        warnings: List of warning messages
        error: Error message (if loading failed)
    """

    config: dict[str, Any] = field(default_factory=dict)
    sources: list[ConfigSource] = field(default_factory=list)
    success: bool = True
    warnings: list[str] = field(default_factory=list)
    error: Optional[str] = None


class ConfigLoader:
    """
    Load groknroll configuration from multiple sources

    Sources are checked in order (later sources override earlier):
    1. Global: `~/.groknroll/groknroll.json`
    2. Environment: path from `GROKNROLL_CONFIG` env var
    3. Project: `./groknroll.json`

    Supports JSONC (JSON with comments).

    Example:
        loader = ConfigLoader(project_path=Path.cwd())
        result = loader.load_with_result()

        if result.success:
            print(f"Loaded config with {len(result.sources)} sources")
            print(result.config)
        else:
            print(f"Error: {result.error}")
    """

    CONFIG_FILENAME = "groknroll.json"
    GLOBAL_CONFIG_DIR = "~/.groknroll"
    ENV_VAR_NAME = "GROKNROLL_CONFIG"

    # Regex patterns for stripping comments
    LINE_COMMENT = re.compile(r"//.*$", re.MULTILINE)
    BLOCK_COMMENT = re.compile(r"/\*.*?\*/", re.DOTALL)

    def __init__(self, project_path: Optional[Path] = None):
        """
        Initialize ConfigLoader

        Args:
            project_path: Project root directory (defaults to cwd)
        """
        self.project_path = project_path or Path.cwd()

    @property
    def project_config_path(self) -> Path:
        """Get project config file path"""
        return self.project_path / self.CONFIG_FILENAME

    @property
    def global_config_path(self) -> Path:
        """Get global config file path"""
        return Path(self.GLOBAL_CONFIG_DIR).expanduser() / self.CONFIG_FILENAME

    @property
    def env_config_path(self) -> Optional[Path]:
        """Get config file path from environment variable"""
        env_path = os.environ.get(self.ENV_VAR_NAME)
        if env_path:
            return Path(env_path).expanduser()
        return None

    def load(self) -> dict[str, Any]:
        """
        Load configuration from all sources

        Returns:
            Merged configuration dict

        Raises:
            ValueError: If a config file has invalid JSON
        """
        result = self.load_with_result()
        if not result.success and result.error:
            raise ValueError(result.error)
        return result.config

    def load_with_result(self) -> ConfigLoadResult:
        """
        Load configuration and return detailed result

        Returns:
            ConfigLoadResult with config and metadata
        """
        sources: list[ConfigSource] = []
        warnings: list[str] = []
        merged_config: dict[str, Any] = {}

        # Load sources in order (later overrides earlier)
        paths_to_check = [
            ("global", self.global_config_path),
            ("env", self.env_config_path),
            ("project", self.project_config_path),
        ]

        for source_name, path in paths_to_check:
            if path is None:
                continue

            source = self._load_source(path)
            sources.append(source)

            if source.exists and source.config is not None:
                # Merge config (shallow for now - deep merge in merger.py)
                merged_config.update(source.config)
            elif source.exists and source.error:
                # File exists but failed to load
                return ConfigLoadResult(
                    config={},
                    sources=sources,
                    success=False,
                    error=f"Failed to load {source_name} config: {source.error}",
                )

        return ConfigLoadResult(
            config=merged_config,
            sources=sources,
            success=True,
            warnings=warnings,
        )

    def _load_source(self, path: Path) -> ConfigSource:
        """
        Load a single configuration source

        Args:
            path: Path to config file

        Returns:
            ConfigSource with loaded config or error
        """
        source = ConfigSource(path=path)

        if not path.exists():
            return source

        source.exists = True

        try:
            content = path.read_text(encoding="utf-8")
            config = self._parse_jsonc(content)
            source.config = config
        except json.JSONDecodeError as e:
            source.error = f"Invalid JSON: {e}"
        except UnicodeDecodeError as e:
            source.error = f"Encoding error: {e}"
        except Exception as e:
            source.error = f"Error reading file: {e}"

        return source

    def _parse_jsonc(self, content: str) -> dict[str, Any]:
        """
        Parse JSONC (JSON with comments)

        Strips // and /* */ comments before parsing.

        Args:
            content: JSONC content string

        Returns:
            Parsed configuration dict

        Raises:
            json.JSONDecodeError: If JSON is invalid
        """
        # Strip comments
        stripped = self._strip_comments(content)

        # Parse JSON
        config = json.loads(stripped)

        # Ensure it's a dict
        if not isinstance(config, dict):
            raise json.JSONDecodeError("Config must be a JSON object", content, 0)

        return config

    def _strip_comments(self, content: str) -> str:
        """
        Strip comments from JSONC content

        Handles:
        - // line comments
        - /* block comments */
        - Comments inside strings are preserved

        Args:
            content: JSONC content string

        Returns:
            JSON content with comments removed
        """
        result = []
        i = 0
        in_string = False
        escape_next = False

        while i < len(content):
            char = content[i]

            # Handle escape sequences in strings
            if escape_next:
                result.append(char)
                escape_next = False
                i += 1
                continue

            if char == "\\" and in_string:
                result.append(char)
                escape_next = True
                i += 1
                continue

            # Handle string boundaries
            if char == '"' and not escape_next:
                in_string = not in_string
                result.append(char)
                i += 1
                continue

            # Handle comments (only outside strings)
            if not in_string:
                # Line comment
                if content[i : i + 2] == "//":
                    # Skip to end of line
                    while i < len(content) and content[i] != "\n":
                        i += 1
                    continue

                # Block comment
                if content[i : i + 2] == "/*":
                    # Skip to end of block comment
                    i += 2
                    while i < len(content) - 1:
                        if content[i : i + 2] == "*/":
                            i += 2
                            break
                        i += 1
                    continue

            result.append(char)
            i += 1

        return "".join(result)

    def load_file(self, path: Path) -> dict[str, Any]:
        """
        Load a single config file

        Args:
            path: Path to config file

        Returns:
            Configuration dict

        Raises:
            FileNotFoundError: If file doesn't exist
            ValueError: If file has invalid JSON
        """
        if not path.exists():
            raise FileNotFoundError(f"Config file not found: {path}")

        source = self._load_source(path)
        if source.error:
            raise ValueError(source.error)

        return source.config or {}

    def get_source_paths(self) -> list[Path]:
        """
        Get all config source paths that would be checked

        Returns:
            List of paths in order of precedence
        """
        paths = [self.global_config_path]

        if self.env_config_path:
            paths.append(self.env_config_path)

        paths.append(self.project_config_path)

        return paths

    def get_active_sources(self) -> list[Path]:
        """
        Get config source paths that exist

        Returns:
            List of existing config file paths
        """
        return [p for p in self.get_source_paths() if p.exists()]

    def __str__(self) -> str:
        return f"ConfigLoader(project={self.project_path})"

    def __repr__(self) -> str:
        return f"ConfigLoader(project_path='{self.project_path}')"


# Convenience functions


def load_config(project_path: Optional[Path] = None) -> dict[str, Any]:
    """
    Load configuration from all sources (convenience function)

    Args:
        project_path: Project root directory

    Returns:
        Merged configuration dict

    Raises:
        ValueError: If a config file has invalid JSON
    """
    loader = ConfigLoader(project_path=project_path)
    return loader.load()


def load_config_with_result(
    project_path: Optional[Path] = None,
) -> ConfigLoadResult:
    """
    Load configuration and return detailed result (convenience function)

    Args:
        project_path: Project root directory

    Returns:
        ConfigLoadResult with config and metadata
    """
    loader = ConfigLoader(project_path=project_path)
    return loader.load_with_result()


def load_config_file(path: Path) -> dict[str, Any]:
    """
    Load a single config file (convenience function)

    Args:
        path: Path to config file

    Returns:
        Configuration dict

    Raises:
        FileNotFoundError: If file doesn't exist
        ValueError: If file has invalid JSON
    """
    loader = ConfigLoader()
    return loader.load_file(path)


def parse_jsonc(content: str) -> dict[str, Any]:
    """
    Parse JSONC content (convenience function)

    Args:
        content: JSONC string

    Returns:
        Parsed configuration dict

    Raises:
        json.JSONDecodeError: If JSON is invalid
    """
    loader = ConfigLoader()
    return loader._parse_jsonc(content)
