"""Devcontainer auto-discovery for CleverAgents.

Scans a resource's location for ``.devcontainer/`` directories and
root-level ``.devcontainer.json`` files.  When found, creates child
``devcontainer-instance`` and ``devcontainer-file`` resources in the
resource registry.

Auto-discovery triggers when a ``git-checkout`` or ``fs-directory``
resource is linked to a project.  The scanner walks the top level
of the resource location (no recursive descent) and validates each
``devcontainer.json`` before registration.

Named configurations (``devcontainer/<name>/devcontainer.json``) are
supported for monorepo projects.  Each named configuration produces a
separate ``devcontainer-instance`` resource with the configuration name
preserved as the ``config_name`` attribute.

Based on:
    - docs/specification.md -- Devcontainer, Auto-Discovery
    - ADR-043: Devcontainer Integration
"""

from __future__ import annotations

import json
import logging
from pathlib import Path

logger = logging.getLogger(__name__)

#: Resource types that trigger devcontainer auto-discovery.
_TRIGGER_TYPES: frozenset[str] = frozenset(
    {"git-checkout", "fs-directory"},
)

#: Fixed paths to scan relative to the resource location (no config name).
_FIXED_SCAN_PATHS: tuple[str, ...] = (
    ".devcontainer/devcontainer.json",
    ".devcontainer.json",
)


class DevcontainerDiscoveryResult:
    """Describes a discovered devcontainer configuration.

    Attributes:
        config_path: Absolute path to the ``devcontainer.json`` file.
        config_data: Parsed JSON content of the devcontainer config.
        parent_location: Location of the parent resource.
        config_name: Name of the named configuration (e.g. ``"api"``,
            ``"frontend"``), or ``None`` for root-level configurations
            (``.devcontainer/devcontainer.json`` and ``.devcontainer.json``).
    """

    __slots__ = ("config_data", "config_name", "config_path", "parent_location")

    def __init__(
        self,
        *,
        config_path: Path,
        config_data: dict[str, object],
        parent_location: str,
        config_name: str | None = None,
    ) -> None:
        if not isinstance(config_path, Path):
            raise TypeError(
                f"config_path must be a Path, got {type(config_path).__name__}"
            )
        if not isinstance(config_data, dict):
            raise TypeError(
                f"config_data must be a dict, got {type(config_data).__name__}"
            )
        if not parent_location:
            raise ValueError("parent_location must not be empty")
        if config_name is not None and not isinstance(config_name, str):
            raise TypeError(
                f"config_name must be a str or None, got {type(config_name).__name__}"
            )
        if config_name is not None and not config_name:
            raise ValueError(
                "config_name must not be an empty string; use None instead"
            )
        self.config_path = config_path
        self.config_data = config_data
        self.parent_location = parent_location
        self.config_name = config_name


def discover_devcontainers(
    resource_location: str,
    resource_type: str,
) -> list[DevcontainerDiscoveryResult]:
    """Scan a resource location for devcontainer configurations.

    Scans the well-known fixed paths (``.devcontainer/devcontainer.json``
    and root ``.devcontainer.json``) as well as named configurations at
    ``.devcontainer/<name>/devcontainer.json`` (one subdirectory level).

    Each file is validated as JSON before being included in the results.
    Named configurations carry the subdirectory name as ``config_name``.

    Args:
        resource_location: Filesystem path of the parent resource.
        resource_type: Type name of the parent resource.

    Returns:
        List of discovery results, one per valid devcontainer config.
        Invalid JSON files are skipped with a warning log.

    Raises:
        ValueError: If *resource_location* is empty or
            *resource_type* is not a trigger type.
    """
    if not resource_location:
        raise ValueError("resource_location must not be empty")
    if not resource_type:
        raise ValueError("resource_type must not be empty")

    if resource_type not in _TRIGGER_TYPES:
        return []

    base = Path(resource_location)
    if not base.is_dir():
        logger.debug(
            "Resource location is not a directory, skipping devcontainer discovery: %s",
            resource_location,
        )
        return []

    results: list[DevcontainerDiscoveryResult] = []

    # ── Fixed paths (root-level configs, no config_name) ──────────────────
    for relative in _FIXED_SCAN_PATHS:
        candidate = base / relative
        if not candidate.is_file():
            continue

        config_data = _load_devcontainer_json(candidate)
        if config_data is None:
            continue

        results.append(
            DevcontainerDiscoveryResult(
                config_path=candidate,
                config_data=config_data,
                parent_location=resource_location,
                config_name=None,
            )
        )

    # ── Named configurations: .devcontainer/<name>/devcontainer.json ──────
    dc_dir = base / ".devcontainer"
    if dc_dir.is_dir():
        for subdir in sorted(dc_dir.iterdir()):
            if not subdir.is_dir():
                continue
            candidate = subdir / "devcontainer.json"
            if not candidate.is_file():
                continue

            config_data = _load_devcontainer_json(candidate)
            if config_data is None:
                continue

            results.append(
                DevcontainerDiscoveryResult(
                    config_path=candidate,
                    config_data=config_data,
                    parent_location=resource_location,
                    config_name=subdir.name,
                )
            )

    return results


def is_trigger_type(resource_type: str) -> bool:
    """Check whether a resource type triggers devcontainer discovery.

    Args:
        resource_type: The resource type name to check.

    Returns:
        ``True`` if the type triggers auto-discovery.
    """
    if not resource_type:
        raise ValueError("resource_type must not be empty")
    return resource_type in _TRIGGER_TYPES


def _load_devcontainer_json(path: Path) -> dict[str, object] | None:
    """Read and validate a devcontainer.json file.

    Args:
        path: Absolute path to the JSON file.

    Returns:
        Parsed dict on success, ``None`` on invalid JSON.
    """
    try:
        text = path.read_text(encoding="utf-8")
    except OSError as exc:
        logger.warning("Cannot read devcontainer config %s: %s", path, exc)
        return None

    try:
        data = json.loads(text)
    except json.JSONDecodeError as exc:
        logger.warning("Invalid JSON in devcontainer config %s: %s", path, exc)
        return None

    if not isinstance(data, dict):
        logger.warning(
            "Devcontainer config %s is not a JSON object (got %s)",
            path,
            type(data).__name__,
        )
        return None

    return data
