"""
RBSP Configuration System.

Reads configuration from ``.rbsp.toml``, ``.rbsp/config.json``, or
environment variables.  Falls back to sensible defaults when nothing is
configured.
"""

from __future__ import annotations

import json
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Set


# ---------------------------------------------------------------------------
# Default values
# ---------------------------------------------------------------------------

_DEFAULTS: Dict[str, Any] = {
    # Paths
    "index_path": ".rbsp/index",
    "plugins_path": ".rbsp/plugins",
    "watch_paths": ["."],

    # File filters
    "file_extensions": [
        ".py", ".js", ".ts", ".jsx", ".tsx", ".java", ".c", ".cpp", ".h",
        ".hpp", ".cs", ".go", ".rs", ".rb", ".php", ".swift", ".kt",
        ".scala", ".lua", ".r", ".sh", ".bash",
        ".html", ".css", ".scss", ".xml", ".json", ".yaml", ".yml",
        ".toml", ".md", ".rst", ".txt", ".sql",
    ],
    "ignored_dirs": [
        ".git", ".svn", "__pycache__", "node_modules",
        ".tox", ".nox", ".mypy_cache", ".pytest_cache",
        "venv", ".venv", "dist", "build",
    ],
    "max_file_size": 1_048_576,  # 1 MiB

    # Bloom filter
    "bloom_filter_size": 1_000_000,
    "bloom_hash_count": 7,

    # LSH
    "lsh_bands": 20,
    "lsh_rows_per_band": 5,
    "lsh_dimensions": 100,

    # Resonance matching
    "resonance_threshold": 0.35,
    "max_results": 50,

    # Watcher
    "watch_interval": 2.0,  # seconds
    "watch_debounce": 0.5,

    # Misc
    "verbose": False,
    "color": True,
}

# Environment variable prefix
_ENV_PREFIX = "RBSP_"

# Keys that are lists in the config
_LIST_KEYS: Set[str] = {"watch_paths", "file_extensions", "ignored_dirs"}


# ---------------------------------------------------------------------------
# TOML mini-parser (stdlib only — no tomllib on 3.9/3.10)
# ---------------------------------------------------------------------------

def _parse_toml_value(raw: str) -> Any:
    """Parse a single TOML value (string, number, bool, or array)."""
    raw = raw.strip()

    # Boolean
    if raw == "true":
        return True
    if raw == "false":
        return False

    # String (basic or literal)
    if (raw.startswith('"') and raw.endswith('"')) or (
        raw.startswith("'") and raw.endswith("'")
    ):
        return raw[1:-1]

    # Array — simplistic: only single-line arrays of strings or numbers
    if raw.startswith("[") and raw.endswith("]"):
        inner = raw[1:-1].strip()
        if not inner:
            return []
        items: List[Any] = []
        for part in _split_array(inner):
            items.append(_parse_toml_value(part.strip()))
        return items

    # Integer
    try:
        return int(raw)
    except ValueError:
        pass

    # Float
    try:
        return float(raw)
    except ValueError:
        pass

    return raw


def _split_array(s: str) -> List[str]:
    """Split a TOML array body by commas, respecting quoted strings."""
    parts: List[str] = []
    current: List[str] = []
    in_string = False
    quote_char = ""

    for ch in s:
        if in_string:
            current.append(ch)
            if ch == quote_char:
                in_string = False
        elif ch in ('"', "'"):
            in_string = True
            quote_char = ch
            current.append(ch)
        elif ch == ",":
            parts.append("".join(current))
            current = []
        else:
            current.append(ch)

    if current:
        parts.append("".join(current))
    return parts


def _load_toml_file(path: Path) -> Dict[str, Any]:
    """Load a TOML file using a minimal parser (flat keys and [section] headers)."""
    result: Dict[str, Any] = {}
    current_section = ""

    try:
        text = path.read_text(encoding="utf-8")
    except OSError:
        return result

    for raw_line in text.splitlines():
        line = raw_line.strip()
        # Skip blanks and comments
        if not line or line.startswith("#"):
            continue
        # Section header
        if line.startswith("[") and line.endswith("]"):
            current_section = line[1:-1].strip() + "."
            continue
        # Key = value
        if "=" in line:
            key, _, value = line.partition("=")
            full_key = current_section + key.strip()
            # Strip inline comments (outside of strings)
            comment_idx = _find_inline_comment(value)
            if comment_idx >= 0:
                value = value[:comment_idx]
            result[full_key] = _parse_toml_value(value)

    return result


def _find_inline_comment(value: str) -> int:
    """Return the index of an inline ``#`` comment, or -1."""
    in_string = False
    quote_char = ""
    for i, ch in enumerate(value):
        if in_string:
            if ch == quote_char:
                in_string = False
        elif ch in ('"', "'"):
            in_string = True
            quote_char = ch
        elif ch == "#":
            return i
    return -1


# ---------------------------------------------------------------------------
# Configuration dataclass
# ---------------------------------------------------------------------------

@dataclass
class RBSPConfig:
    """Runtime configuration for RBSP.

    Resolution order (highest priority first):
        1. Explicit overrides passed at construction time
        2. Environment variables (``RBSP_<KEY>``)
        3. ``.rbsp.toml`` in the project root
        4. ``.rbsp/config.json`` in the project root
        5. Built-in defaults
    """

    # Paths
    index_path: str = ""
    plugins_path: str = ""
    watch_paths: List[str] = field(default_factory=list)

    # File filters
    file_extensions: List[str] = field(default_factory=list)
    ignored_dirs: List[str] = field(default_factory=list)
    max_file_size: int = 0

    # Bloom filter
    bloom_filter_size: int = 0
    bloom_hash_count: int = 0

    # LSH
    lsh_bands: int = 0
    lsh_rows_per_band: int = 0
    lsh_dimensions: int = 0

    # Resonance
    resonance_threshold: float = 0.0
    max_results: int = 0

    # Watcher
    watch_interval: float = 0.0
    watch_debounce: float = 0.0

    # Misc
    verbose: bool = False
    color: bool = True

    # The root directory this config was loaded from
    root: str = ""

    # --- Factories --------------------------------------------------------

    @classmethod
    def load(cls, root: str = ".", overrides: Optional[Dict[str, Any]] = None) -> "RBSPConfig":
        """Load configuration with the full resolution chain.

        Parameters
        ----------
        root:
            Project root directory to look for config files in.
        overrides:
            Explicit key-value overrides (highest priority).
        """
        root_path = Path(root).resolve()
        merged: Dict[str, Any] = dict(_DEFAULTS)

        # Layer 4: .rbsp/config.json
        json_path = root_path / ".rbsp" / "config.json"
        if json_path.is_file():
            try:
                with json_path.open("r", encoding="utf-8") as fh:
                    data = json.load(fh)
                if isinstance(data, dict):
                    merged.update(data)
            except (json.JSONDecodeError, OSError):
                pass

        # Layer 3: .rbsp.toml
        toml_path = root_path / ".rbsp.toml"
        if toml_path.is_file():
            merged.update(_load_toml_file(toml_path))

        # Layer 2: environment variables
        for key in _DEFAULTS:
            env_key = _ENV_PREFIX + key.upper()
            env_val = os.environ.get(env_key)
            if env_val is not None:
                if key in _LIST_KEYS:
                    merged[key] = [v.strip() for v in env_val.split(",") if v.strip()]
                elif isinstance(_DEFAULTS[key], bool):
                    merged[key] = env_val.lower() in ("1", "true", "yes")
                elif isinstance(_DEFAULTS[key], int):
                    try:
                        merged[key] = int(env_val)
                    except ValueError:
                        pass
                elif isinstance(_DEFAULTS[key], float):
                    try:
                        merged[key] = float(env_val)
                    except ValueError:
                        pass
                else:
                    merged[key] = env_val

        # Layer 1: explicit overrides
        if overrides:
            merged.update(overrides)

        merged["root"] = str(root_path)

        return cls(**{k: v for k, v in merged.items() if k in cls.__dataclass_fields__})

    # --- Persistence -------------------------------------------------------

    def save_json(self, path: Optional[str] = None) -> Path:
        """Write current configuration to a JSON file.

        Returns the path written to.
        """
        if path is None:
            out = Path(self.root) / ".rbsp" / "config.json"
        else:
            out = Path(path)
        out.parent.mkdir(parents=True, exist_ok=True)

        data = {
            k: v
            for k, v in self.to_dict().items()
            if k != "root"
        }
        with out.open("w", encoding="utf-8") as fh:
            json.dump(data, fh, indent=2)
            fh.write("\n")
        return out

    def save_toml(self, path: Optional[str] = None) -> Path:
        """Write current configuration to a TOML file.

        Returns the path written to.
        """
        if path is None:
            out = Path(self.root) / ".rbsp.toml"
        else:
            out = Path(path)
        out.parent.mkdir(parents=True, exist_ok=True)

        lines: List[str] = [
            "# RBSP configuration",
            "# Generated by rbsp init",
            "",
        ]

        data = {k: v for k, v in self.to_dict().items() if k != "root"}

        for key, value in data.items():
            if isinstance(value, bool):
                lines.append(f"{key} = {'true' if value else 'false'}")
            elif isinstance(value, (int, float)):
                lines.append(f"{key} = {value}")
            elif isinstance(value, str):
                lines.append(f'{key} = "{value}"')
            elif isinstance(value, list):
                items = ", ".join(f'"{v}"' for v in value)
                lines.append(f"{key} = [{items}]")
            else:
                lines.append(f'{key} = "{value}"')

        lines.append("")
        out.write_text("\n".join(lines), encoding="utf-8")
        return out

    # --- Utilities ---------------------------------------------------------

    def to_dict(self) -> Dict[str, Any]:
        """Return the configuration as a plain dictionary."""
        return {
            f.name: getattr(self, f.name)
            for f in self.__dataclass_fields__.values()
        }

    def get(self, key: str, default: Any = None) -> Any:
        """Get a config value by key, with an optional default."""
        return getattr(self, key, default)

    def resolve_path(self, key: str) -> Path:
        """Resolve a path-valued config key relative to the project root."""
        value = getattr(self, key, "")
        p = Path(value)
        if p.is_absolute():
            return p
        return Path(self.root) / p
