"""Load and save devcontainer.json files.

This module provides utilities for finding, loading, and saving devcontainer.json
files in standard locations.
"""

from __future__ import annotations

import json
import re
from pathlib import Path

from gaol.devcontainer.models import DevcontainerSpec


class DevcontainerNotFoundError(Exception):
    """Raised when no devcontainer.json file is found."""

    pass


class DevcontainerParseError(Exception):
    """Raised when devcontainer.json cannot be parsed."""

    def __init__(self, message: str, path: Path | None = None):
        self.path = path
        super().__init__(message)


class DevcontainerLoader:
    """Load and save devcontainer.json files.

    Supports auto-discovery of devcontainer.json in standard locations
    and handles JSON with comments (JSONC) parsing.
    """

    # Standard devcontainer.json locations in priority order
    STANDARD_LOCATIONS = [
        ".devcontainer/devcontainer.json",
        ".devcontainer.json",
    ]

    def __init__(self, workspace_dir: Path | None = None):
        """Initialize loader with optional workspace directory.

        Args:
            workspace_dir: Base directory to search for devcontainer.json.
                          Defaults to current working directory.
        """
        self.workspace_dir = workspace_dir or Path.cwd()

    def find_devcontainer(self) -> Path | None:
        """Find devcontainer.json in standard locations.

        Searches in the following order:
        1. .devcontainer/devcontainer.json
        2. .devcontainer.json
        3. .devcontainer/<folder>/devcontainer.json (first found)

        Returns:
            Path to devcontainer.json if found, None otherwise.
        """
        # Check standard locations first
        for location in self.STANDARD_LOCATIONS:
            path = self.workspace_dir / location
            if path.exists():
                return path

        # Check for subdirectories in .devcontainer/
        devcontainer_dir = self.workspace_dir / ".devcontainer"
        if devcontainer_dir.is_dir():
            for subdir in sorted(devcontainer_dir.iterdir()):
                if subdir.is_dir():
                    candidate = subdir / "devcontainer.json"
                    if candidate.exists():
                        return candidate

        return None

    def load(self, path: Path | None = None) -> DevcontainerSpec:
        """Load and parse devcontainer.json.

        Args:
            path: Path to devcontainer.json. If not provided, auto-discovers
                  using find_devcontainer().

        Returns:
            Parsed DevcontainerSpec.

        Raises:
            DevcontainerNotFoundError: If no devcontainer.json found.
            DevcontainerParseError: If file cannot be parsed.
        """
        if path is None:
            path = self.find_devcontainer()
            if path is None:
                raise DevcontainerNotFoundError(
                    f"No devcontainer.json found in {self.workspace_dir}"
                )

        if not path.exists():
            raise DevcontainerNotFoundError(f"File not found: {path}")

        try:
            content = path.read_text(encoding="utf-8")
            # Strip JSONC comments
            content = self._strip_jsonc_comments(content)
            data = json.loads(content)
            return DevcontainerSpec.from_json(data)
        except json.JSONDecodeError as e:
            raise DevcontainerParseError(
                f"Invalid JSON in {path}: {e}", path=path
            ) from e
        except Exception as e:
            raise DevcontainerParseError(
                f"Error parsing {path}: {e}", path=path
            ) from e

    def save(
        self,
        spec: DevcontainerSpec,
        path: Path | None = None,
        create_dir: bool = True,
    ) -> Path:
        """Save DevcontainerSpec to devcontainer.json.

        Args:
            spec: DevcontainerSpec to save.
            path: Output path. Defaults to .devcontainer/devcontainer.json
                  in workspace_dir.
            create_dir: Whether to create parent directories if they don't exist.

        Returns:
            Path where file was saved.
        """
        if path is None:
            path = self.workspace_dir / ".devcontainer" / "devcontainer.json"

        if create_dir:
            path.parent.mkdir(parents=True, exist_ok=True)

        data = spec.to_json()
        content = json.dumps(data, indent=2, ensure_ascii=False)

        path.write_text(content + "\n", encoding="utf-8")
        return path

    def validate(self, path: Path | None = None) -> list[str]:
        """Validate devcontainer.json and return list of errors.

        Args:
            path: Path to devcontainer.json. Auto-discovers if not provided.

        Returns:
            List of validation error messages. Empty if valid.
        """
        errors: list[str] = []

        if path is None:
            path = self.find_devcontainer()
            if path is None:
                return ["No devcontainer.json found"]

        if not path.exists():
            return [f"File not found: {path}"]

        try:
            content = path.read_text(encoding="utf-8")
            content = self._strip_jsonc_comments(content)
            data = json.loads(content)
        except json.JSONDecodeError as e:
            return [f"Invalid JSON: {e}"]

        # Check for required fields
        if not data.get("image") and not data.get("build") and not data.get("dockerComposeFile"):
            errors.append("Must specify 'image', 'build', or 'dockerComposeFile'")

        # Validate image format if present
        image = data.get("image")
        if image and not isinstance(image, str):
            errors.append("'image' must be a string")

        # Validate build if present
        build = data.get("build")
        if build:
            if not isinstance(build, dict):
                errors.append("'build' must be an object")
            elif "dockerfile" not in build and "context" not in build:
                # At minimum, build should have a dockerfile or context
                pass  # Both are optional with defaults

        # Validate forwardPorts
        forward_ports = data.get("forwardPorts")
        if forward_ports:
            if not isinstance(forward_ports, list):
                errors.append("'forwardPorts' must be an array")
            else:
                for i, port in enumerate(forward_ports):
                    if not isinstance(port, int):
                        errors.append(f"'forwardPorts[{i}]' must be an integer")

        # Validate features
        features = data.get("features")
        if features and not isinstance(features, dict):
            errors.append("'features' must be an object")

        # Validate environment variables
        for env_key in ["containerEnv", "remoteEnv"]:
            env = data.get(env_key)
            if env:
                if not isinstance(env, dict):
                    errors.append(f"'{env_key}' must be an object")
                else:
                    for key, value in env.items():
                        if not isinstance(value, str):
                            errors.append(f"'{env_key}.{key}' must be a string")

        # Try to parse with model for additional validation
        try:
            DevcontainerSpec.from_json(data)
        except Exception as e:
            errors.append(f"Model validation error: {e}")

        return errors

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

        Handles:
        - Single-line comments: // comment
        - Multi-line comments: /* comment */

        Preserves strings that contain comment-like sequences.
        """
        result = []
        in_string = False
        in_single_comment = False
        in_multi_comment = False
        i = 0

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

            # Handle string state
            if char == '"' and not in_single_comment and not in_multi_comment:
                # Check if escaped
                escape_count = 0
                j = i - 1
                while j >= 0 and content[j] == "\\":
                    escape_count += 1
                    j -= 1
                if escape_count % 2 == 0:
                    in_string = not in_string

            # Handle comments
            if not in_string:
                # Check for single-line comment start
                if (
                    not in_multi_comment
                    and char == "/"
                    and i + 1 < len(content)
                    and content[i + 1] == "/"
                ):
                    in_single_comment = True
                    i += 2
                    continue

                # Check for multi-line comment start
                if (
                    not in_single_comment
                    and char == "/"
                    and i + 1 < len(content)
                    and content[i + 1] == "*"
                ):
                    in_multi_comment = True
                    i += 2
                    continue

                # Check for single-line comment end
                if in_single_comment and char == "\n":
                    in_single_comment = False
                    result.append(char)
                    i += 1
                    continue

                # Check for multi-line comment end
                if (
                    in_multi_comment
                    and char == "*"
                    and i + 1 < len(content)
                    and content[i + 1] == "/"
                ):
                    in_multi_comment = False
                    i += 2
                    continue

            # Add character if not in comment
            if not in_single_comment and not in_multi_comment:
                result.append(char)

            i += 1

        return "".join(result)


# Convenience functions
def load_devcontainer(path: Path | None = None) -> DevcontainerSpec:
    """Load devcontainer.json from path or auto-discover.

    Args:
        path: Path to devcontainer.json. Auto-discovers if not provided.

    Returns:
        Parsed DevcontainerSpec.
    """
    loader = DevcontainerLoader()
    return loader.load(path)


def save_devcontainer(
    spec: DevcontainerSpec,
    path: Path | None = None,
) -> Path:
    """Save DevcontainerSpec to devcontainer.json.

    Args:
        spec: DevcontainerSpec to save.
        path: Output path. Defaults to .devcontainer/devcontainer.json.

    Returns:
        Path where file was saved.
    """
    loader = DevcontainerLoader()
    return loader.save(spec, path)


def find_devcontainer(workspace_dir: Path | None = None) -> Path | None:
    """Find devcontainer.json in standard locations.

    Args:
        workspace_dir: Directory to search in. Defaults to cwd.

    Returns:
        Path to devcontainer.json if found, None otherwise.
    """
    loader = DevcontainerLoader(workspace_dir)
    return loader.find_devcontainer()


def validate_devcontainer(path: Path | None = None) -> list[str]:
    """Validate devcontainer.json and return errors.

    Args:
        path: Path to devcontainer.json. Auto-discovers if not provided.

    Returns:
        List of validation errors. Empty if valid.
    """
    loader = DevcontainerLoader()
    return loader.validate(path)
