"""Helpers to read the `externai.json` configuration stored in the current project."""

from __future__ import annotations

import json
import os
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any, List, Optional

DEFAULT_VERSION = 1
DEFAULT_DIR = ".externals"
CONFIG_FILENAME = "externai.json"


def project_root() -> Path:
    """Return the directory from which the CLI is executed."""
    return Path.cwd().resolve()


def _candidate_config_paths() -> List[Path]:
    """Return the ordered list of locations to look for `externai.json`."""
    root = project_root()
    return [
        root / CONFIG_FILENAME,
        root / DEFAULT_DIR / CONFIG_FILENAME,
    ]


@dataclass(frozen=True)
class ExternalImport:
    """Represents one entry from the `imports` array."""

    name: str
    path: Optional[str]
    mount: str

    @classmethod
    def from_raw(cls, raw: Any) -> "ExternalImport":
        """Build an `ExternalImport` from the JSON entry."""
        if isinstance(raw, str):
            return cls(name=raw, path=None, mount=raw)
        if not isinstance(raw, dict):
            sys.exit("[externals] Each import must be a string or object.")
        name = raw.get("name")
        if not isinstance(name, str) or not name:
            sys.exit("[externals] Every import must specify a non-empty `name`.")
        if "mount" in raw:
            mount = raw["mount"]
            if not isinstance(mount, str) or not mount:
                sys.exit("[externals] `mount` must be a non-empty string when provided.")
        else:
            mount = name
        raw_path = raw.get("path")
        if raw_path is not None and not isinstance(raw_path, str):
            sys.exit("[externals] `path` must be a string when provided.")
        path = (
            os.path.expandvars(os.path.expanduser(raw_path))
            if isinstance(raw_path, str)
            else None
        )
        return cls(name=name, path=path, mount=mount)


@dataclass(frozen=True)
class ExternalsConfig:
    """Configuration for the CLI surface and where to mount imports."""

    version: int
    externals_dir: Path
    imports: List[ExternalImport]


def _existing_config_path() -> Optional[Path]:
    """
    Return the first config file that exists among known candidate locations.

    Returns:
        Existing config path, or None when no candidate file is present.
    """
    return next((path for path in _candidate_config_paths() if path.exists()), None)


def _read_raw_config(require: bool = True) -> tuple[Path, dict]:
    """
    Load the raw JSON config payload and validate its top-level shape.

    Args:
        require: Whether to abort when no config file is found.

    Returns:
        A tuple with the resolved config file path and parsed JSON object.

    Raises:
        FileNotFoundError: Raised when config is missing and `require` is False.
        SystemExit: Raised for missing required config, invalid JSON, or wrong root type.
    """
    config_path = _existing_config_path()
    if config_path is None:
        if require:
            candidates = ", ".join(str(path) for path in _candidate_config_paths())
            sys.exit(f"[externals] Config not found; looked in {candidates}")
        raise FileNotFoundError("externai.json not found")
    try:
        data = json.loads(config_path.read_text(encoding="utf-8"))
    except json.JSONDecodeError as exc:
        sys.exit(f"[externals] Invalid JSON in {config_path}: {exc}")
    if not isinstance(data, dict):
        sys.exit("[externals] Config root must be an object.")
    return config_path, data


def load_config() -> ExternalsConfig:
    """Parse the config stored in the project root."""
    config_path, data = _read_raw_config()
    version = data.get("version", DEFAULT_VERSION)
    if version != DEFAULT_VERSION:
        sys.exit(f"[externals] Unsupported config version: {version}")
    raw_dir = data.get("externalsDir", DEFAULT_DIR)
    if not isinstance(raw_dir, str) or not raw_dir:
        sys.exit("[externals] `externalsDir` must be a non-empty string.")
    externals_dir = Path(raw_dir)
    if not externals_dir.is_absolute():
        externals_dir = (project_root() / externals_dir).resolve()
    imports_raw = data.get("imports")
    if not isinstance(imports_raw, list):
        sys.exit("[externals] `imports` must be an array.")
    imports = [ExternalImport.from_raw(entry) for entry in imports_raw]
    return ExternalsConfig(version=version, externals_dir=externals_dir, imports=imports)


def _ensure_imports(data: dict) -> list:
    """
    Ensure the mutable config payload contains an `imports` list.

    Args:
        data: Raw config object loaded from JSON.

    Returns:
        The existing or newly created `imports` list reference.
    """
    imports_raw = data.get("imports")
    if not isinstance(imports_raw, list):
        imports_raw = []
        data["imports"] = imports_raw
    return imports_raw


def _default_config_data() -> dict:
    """
    Build a minimal in-memory configuration payload.

    Returns:
        Default config dictionary used when creating a new `externai.json`.
    """
    return {"version": DEFAULT_VERSION, "externalsDir": DEFAULT_DIR, "imports": []}


def ensure_config_exists() -> Path:
    """
    Create a default config file in the project root when missing.

    Returns:
        Absolute path to the project-level `externai.json`.
    """
    path = project_root() / CONFIG_FILENAME
    path.parent.mkdir(parents=True, exist_ok=True)
    if not path.exists():
        path.write_text(json.dumps(_default_config_data(), indent=2), encoding="utf-8")
    return path


def _write_raw_config(data: dict, path: Path) -> None:
    """
    Persist a raw config object to disk with stable indentation.

    Args:
        data: Raw config dictionary to serialize.
        path: Destination file path.
    """
    path.write_text(json.dumps(data, indent=2), encoding="utf-8")


def add_import_entry(name: str, path: Optional[str] = None) -> bool:
    """Add a new import entry unless the name already exists."""
    try:
        config_path, data = _read_raw_config(require=False)
    except FileNotFoundError:
        config_path = ensure_config_exists()
        data = _default_config_data()
    imports_raw = _ensure_imports(data)
    normalized = str(Path(path).resolve()) if path else None
    for entry in imports_raw:
        if isinstance(entry, str) and entry == name:
            return False
        if isinstance(entry, dict) and entry.get("name") == name:
            return False
    new_entry = name if path is None else {"name": name, "path": normalized}
    imports_raw.append(new_entry)
    _write_raw_config(data, config_path)
    return True


def remove_import_entry(name: str) -> bool:
    """Remove every import that targets the given name."""
    config_path, data = _read_raw_config()
    imports_raw = _ensure_imports(data)
    before = len(imports_raw)
    imports_raw[:] = [entry for entry in imports_raw if not ((isinstance(entry, str) and entry == name) or (isinstance(entry, dict) and entry.get("name") == name))]
    if len(imports_raw) == before:
        return False
    _write_raw_config(data, config_path)
    return True
