"""Specification loader for YAML and TOML container specs.

This module handles loading, parsing, and validating container
specifications from YAML and TOML files.
"""

from __future__ import annotations

import os
import re
from pathlib import Path
from typing import TYPE_CHECKING

import yaml

if TYPE_CHECKING:
    from gaol.models.container import ContainerConfig
    from gaol.sharing.models import ImportResult

from gaol.specs.models import ContainerSpec


class SpecError(Exception):
    """Base exception for spec-related errors."""

    pass


class SpecNotFoundError(SpecError):
    """Raised when a spec file is not found."""

    def __init__(self, path: str | Path) -> None:
        self.path = Path(path)
        super().__init__(f"Spec file not found: {self.path}")


class SpecParseError(SpecError):
    """Raised when a spec file cannot be parsed."""

    def __init__(self, path: str | Path, reason: str) -> None:
        self.path = Path(path)
        self.reason = reason
        super().__init__(f"Failed to parse {self.path}: {reason}")


class SpecValidationError(SpecError):
    """Raised when a spec fails validation."""

    def __init__(self, path: str | Path, errors: list[str]) -> None:
        self.path = Path(path)
        self.errors = errors
        msg = f"Validation failed for {self.path}:\n" + "\n".join(f"  - {e}" for e in errors)
        super().__init__(msg)


def _load_toml(content: str) -> dict:
    """Load TOML content, using tomllib (3.11+) or tomli as fallback."""
    try:
        import tomllib

        return tomllib.loads(content)
    except ImportError:
        try:
            import tomli

            return tomli.loads(content)
        except ImportError:
            raise ImportError(
                "TOML support requires Python 3.11+ or the 'tomli' package. "
                "Install with: pip install tomli"
            ) from None


def _expand_variables(value: str, env: dict[str, str]) -> str:
    """Expand environment variables in a string.

    Supports both ${VAR} and $VAR syntax, with ${VAR:-default} for defaults.
    """
    # Pattern for ${VAR} or ${VAR:-default}
    def replace_braced(match: re.Match) -> str:
        var = match.group(1)
        default = match.group(2)
        if var in env:
            return env[var]
        elif default is not None:
            return default
        elif var in os.environ:
            return os.environ[var]
        return match.group(0)  # Keep original if not found

    # Pattern for $VAR (simple form)
    def replace_simple(match: re.Match) -> str:
        var = match.group(1)
        if var in env:
            return env[var]
        elif var in os.environ:
            return os.environ[var]
        return match.group(0)

    # First expand ${VAR:-default} and ${VAR}
    result = re.sub(r"\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}", replace_braced, value)
    # Then expand $VAR
    result = re.sub(r"\$([A-Za-z_][A-Za-z0-9_]*)", replace_simple, result)

    return result


def _expand_dict(data: dict, env: dict[str, str]) -> dict:
    """Recursively expand environment variables in a dict."""
    result = {}
    for key, value in data.items():
        if isinstance(value, str):
            result[key] = _expand_variables(value, env)
        elif isinstance(value, dict):
            result[key] = _expand_dict(value, env)
        elif isinstance(value, list):
            result[key] = _expand_list(value, env)
        else:
            result[key] = value
    return result


def _expand_list(data: list, env: dict[str, str]) -> list:
    """Recursively expand environment variables in a list."""
    result = []
    for item in data:
        if isinstance(item, str):
            result.append(_expand_variables(item, env))
        elif isinstance(item, dict):
            result.append(_expand_dict(item, env))
        elif isinstance(item, list):
            result.append(_expand_list(item, env))
        else:
            result.append(item)
    return result


def load_env_file(path: Path) -> dict[str, str]:
    """Load environment variables from a .env file.

    Format:
        KEY=value
        KEY2="quoted value"
        # Comment
        export KEY3=value
    """
    env: dict[str, str] = {}

    if not path.exists():
        return env

    for line in path.read_text().splitlines():
        line = line.strip()

        # Skip empty lines and comments
        if not line or line.startswith("#"):
            continue

        # Handle 'export KEY=value' syntax
        if line.startswith("export "):
            line = line[7:].strip()

        # Parse KEY=value
        if "=" not in line:
            continue

        key, value = line.split("=", 1)
        key = key.strip()
        value = value.strip()

        # Remove quotes
        if (value.startswith('"') and value.endswith('"')) or (
            value.startswith("'") and value.endswith("'")
        ):
            value = value[1:-1]

        env[key] = value

    return env


class SpecLoader:
    """Load and validate container specifications."""

    def __init__(self, base_dir: Path | None = None) -> None:
        """Initialize the spec loader.

        Args:
            base_dir: Base directory for resolving relative paths.
                      Defaults to current working directory.
        """
        self.base_dir = base_dir or Path.cwd()

    def load(
        self,
        path: str | Path,
        *,
        expand_env: bool = True,
        extra_env: dict[str, str] | None = None,
    ) -> ContainerSpec:
        """Load a container spec from a file.

        Args:
            path: Path to the spec file (YAML or TOML)
            expand_env: Whether to expand environment variables
            extra_env: Additional environment variables for expansion

        Returns:
            Validated ContainerSpec

        Raises:
            SpecNotFoundError: If the file doesn't exist
            SpecParseError: If the file can't be parsed
            SpecValidationError: If the spec is invalid
        """
        path = Path(path)
        if not path.is_absolute():
            path = self.base_dir / path

        if not path.exists():
            raise SpecNotFoundError(path)

        # Load file content
        content = path.read_text()

        # Parse based on extension
        suffix = path.suffix.lower()
        try:
            if suffix in (".yaml", ".yml"):
                data = yaml.safe_load(content)
            elif suffix == ".toml":
                data = _load_toml(content)
            else:
                # Try YAML first, then TOML
                try:
                    data = yaml.safe_load(content)
                except yaml.YAMLError:
                    data = _load_toml(content)
        except yaml.YAMLError as e:
            raise SpecParseError(path, f"Invalid YAML: {e}") from e
        except Exception as e:
            raise SpecParseError(path, str(e)) from e

        if not isinstance(data, dict):
            raise SpecParseError(path, "Expected a mapping at top level")

        # Build environment for expansion
        env: dict[str, str] = {}

        # Load env_file if specified
        env_file = data.get("env_file")
        if env_file:
            env_path = Path(env_file)
            if not env_path.is_absolute():
                env_path = path.parent / env_path
            env.update(load_env_file(env_path))

        # Add extra env
        if extra_env:
            env.update(extra_env)

        # Expand environment variables
        if expand_env:
            data = _expand_dict(data, env)

        # Validate and create spec
        try:
            spec = ContainerSpec(**data)
        except Exception as e:
            # Extract validation errors from Pydantic
            errors: list[str] = []
            if hasattr(e, "errors"):
                for err in e.errors():
                    loc = ".".join(str(x) for x in err.get("loc", []))
                    msg = err.get("msg", str(err))
                    errors.append(f"{loc}: {msg}")
            else:
                errors.append(str(e))
            raise SpecValidationError(path, errors) from e

        return spec

    def load_config(
        self,
        path: str | Path,
        *,
        expand_env: bool = True,
        extra_env: dict[str, str] | None = None,
    ) -> ContainerConfig:
        """Load a spec and convert to ContainerConfig.

        This is a convenience method that loads the spec and
        converts it to the internal ContainerConfig model.

        Args:
            path: Path to the spec file
            expand_env: Whether to expand environment variables
            extra_env: Additional environment variables

        Returns:
            ContainerConfig ready for use with runtimes
        """
        spec = self.load(path, expand_env=expand_env, extra_env=extra_env)
        return spec.to_container_config()

    def validate(self, path: str | Path) -> list[str]:
        """Validate a spec file without loading it.

        Args:
            path: Path to the spec file

        Returns:
            List of validation errors (empty if valid)
        """
        try:
            self.load(path)
            return []
        except SpecNotFoundError as e:
            return [str(e)]
        except SpecParseError as e:
            return [str(e)]
        except SpecValidationError as e:
            return e.errors


def load_spec(
    path: str | Path,
    *,
    expand_env: bool = True,
    extra_env: dict[str, str] | None = None,
) -> ContainerSpec:
    """Load a container spec from a file.

    This is a convenience function that creates a SpecLoader
    with the spec file's parent directory as base.

    Args:
        path: Path to the spec file
        expand_env: Whether to expand environment variables
        extra_env: Additional environment variables

    Returns:
        Validated ContainerSpec
    """
    # Resolve to absolute path to avoid double-prefixing in SpecLoader.load()
    path = Path(path).resolve()
    loader = SpecLoader(base_dir=path.parent)
    return loader.load(path, expand_env=expand_env, extra_env=extra_env)


def load_spec_config(
    path: str | Path,
    *,
    expand_env: bool = True,
    extra_env: dict[str, str] | None = None,
) -> ContainerConfig:
    """Load a spec and convert to ContainerConfig.

    Args:
        path: Path to the spec file
        expand_env: Whether to expand environment variables
        extra_env: Additional environment variables

    Returns:
        ContainerConfig ready for use with runtimes
    """
    path = Path(path).resolve()
    spec = load_spec(path, expand_env=expand_env, extra_env=extra_env)
    config = spec.to_container_config()

    # Resolve relative volume paths to absolute paths based on spec file location
    spec_dir = path.parent
    resolved_volumes: dict[str, str] = {}
    for host_path, container_path in config.volumes.items():
        # Resolve relative host paths (./foo or foo) to absolute paths
        if not host_path.startswith("/") and not host_path.startswith("~"):
            host_path = str((spec_dir / host_path).resolve())
        resolved_volumes[host_path] = container_path
    config.volumes = resolved_volumes

    return config


def spec_to_yaml(spec: ContainerSpec, include_defaults: bool = False) -> str:
    """Convert a ContainerSpec to YAML string.

    Args:
        spec: The spec to convert
        include_defaults: Whether to include default values

    Returns:
        YAML string representation
    """
    data = spec.model_dump(
        exclude_defaults=not include_defaults,
        exclude_none=True,
    )

    # Clean up empty collections
    if not include_defaults:
        data = {k: v for k, v in data.items() if v not in ([], {}, None)}
        if "resources" in data:
            data["resources"] = {
                k: v for k, v in data["resources"].items() if v is not None
            }
            if not data["resources"]:
                del data["resources"]
        if "network" in data:
            data["network"] = {
                k: v for k, v in data["network"].items() if v not in ([], None, "shared")
            }
            if not data["network"]:
                del data["network"]

    return yaml.dump(data, default_flow_style=False, sort_keys=False)


def spec_to_toml(spec: ContainerSpec, include_defaults: bool = False) -> str:
    """Convert a ContainerSpec to TOML string.

    Args:
        spec: The spec to convert
        include_defaults: Whether to include default values

    Returns:
        TOML string representation
    """
    try:
        import tomli_w
    except ImportError:
        raise ImportError(
            "TOML writing requires the 'tomli-w' package. "
            "Install with: pip install tomli-w"
        ) from None

    data = spec.model_dump(
        exclude_defaults=not include_defaults,
        exclude_none=True,
    )

    # Clean up empty collections
    if not include_defaults:
        data = {k: v for k, v in data.items() if v not in ([], {}, None)}
        if "resources" in data:
            data["resources"] = {
                k: v for k, v in data["resources"].items() if v is not None
            }
            if not data["resources"]:
                del data["resources"]
        if "network" in data:
            data["network"] = {
                k: v for k, v in data["network"].items() if v not in ([], None, "shared")
            }
            if not data["network"]:
                del data["network"]

    return tomli_w.dumps(data)


def import_spec_from_url(
    url: str,
    name: str | None = None,
    skip_security_scan: bool = False,
    overwrite: bool = False,
) -> ImportResult:
    """Import a spec from a URL.

    Args:
        url: URL or reference to import from (https://, github:, gist:)
        name: Override name for the spec
        skip_security_scan: Skip security scanning
        overwrite: Overwrite if spec already exists

    Returns:
        ImportResult with status and details

    Raises:
        ImportError: If import fails
        FetchError: If fetching fails
        ContentValidationError: If content is invalid
        SpecExistsError: If spec exists and overwrite=False
    """
    from gaol.sharing import get_content_importer
    from gaol.sharing.exceptions import SpecExistsError
    from gaol.specs.store import get_spec_store

    # Check if already exists
    store = get_spec_store()
    check_name = name
    if check_name is not None and store.exists(check_name) and not overwrite:
        raise SpecExistsError(check_name)

    importer = get_content_importer()
    return importer.import_spec(
        url,
        name=name,
        save_locally=True,
        skip_security_scan=skip_security_scan,
    )


def load_imported_spec(name: str) -> ContainerSpec | None:
    """Load an imported spec by name.

    Args:
        name: Spec name

    Returns:
        ContainerSpec or None if not found
    """
    from gaol.specs.store import get_spec_store

    return get_spec_store().get(name)


def list_imported_specs() -> list[str]:
    """List all imported spec names.

    Returns:
        Sorted list of spec names
    """
    from gaol.specs.store import get_spec_store

    return get_spec_store().list()
