from __future__ import annotations

import json
import shutil
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import Any


@dataclass
class NamedVolumeResolution:
    compose_name: str
    docker_name: str | None
    mountpoint: Path | None
    available: bool
    reason: str | None = None


def docker_available() -> bool:
    return shutil.which("docker") is not None


def run_docker_volume_inspect(volume_name: str) -> dict[str, Any] | None:
    if not docker_available():
        return None

    try:
        result = subprocess.run(
            ["docker", "volume", "inspect", volume_name],
            capture_output=True,
            text=True,
            check=False,
        )
    except OSError:
        return None

    if result.returncode != 0:
        return None

    try:
        data = json.loads(result.stdout)
    except json.JSONDecodeError:
        return None

    if not isinstance(data, list) or not data:
        return None

    item = data[0]
    if not isinstance(item, dict):
        return None

    return item


def infer_project_name(compose_path: Path, compose_data: dict[str, Any]) -> str:
    top_level_name = compose_data.get("name")
    if isinstance(top_level_name, str) and top_level_name.strip():
        return top_level_name.strip()

    return compose_path.parent.name


def normalize_top_level_volume_name(
    volume_key: str,
    compose_data: dict[str, Any],
) -> tuple[str | None, bool]:
    """
    Returns:
      (explicit_name_or_none, is_external)
    """
    volumes = compose_data.get("volumes", {})
    if not isinstance(volumes, dict):
        return None, False

    cfg = volumes.get(volume_key)
    if not isinstance(cfg, dict):
        return None, False

    explicit_name = cfg.get("name")
    if not isinstance(explicit_name, str):
        explicit_name = None

    external = cfg.get("external", False)
    is_external = False

    if isinstance(external, bool):
        is_external = external
    elif isinstance(external, dict):
        is_external = True
        ext_name = external.get("name")
        if isinstance(ext_name, str) and ext_name.strip():
            explicit_name = ext_name.strip()

    return explicit_name, is_external


def build_volume_candidates(
    compose_name: str,
    compose_path: Path,
    compose_data: dict[str, Any],
) -> list[str]:
    """
    Try likely Docker volume names in a sensible order.
    """
    candidates: list[str] = []
    project_name = infer_project_name(compose_path, compose_data)

    explicit_name, is_external = normalize_top_level_volume_name(compose_name, compose_data)

    # 1) explicit external/name override
    if explicit_name:
        candidates.append(explicit_name)

    # 2) external volumes often use raw name directly
    if is_external:
        candidates.append(compose_name)

    # 3) raw compose source
    candidates.append(compose_name)

    # 4) compose-created default name: <project>_<volume>
    candidates.append(f"{project_name}_{compose_name}")

    # de-dup while preserving order
    unique: list[str] = []
    seen: set[str] = set()
    for c in candidates:
        if c not in seen:
            unique.append(c)
            seen.add(c)

    return unique


def resolve_named_volume(
    compose_name: str,
    compose_path: Path,
    compose_data: dict[str, Any],
) -> NamedVolumeResolution:
    if not docker_available():
        return NamedVolumeResolution(
            compose_name=compose_name,
            docker_name=None,
            mountpoint=None,
            available=False,
            reason="docker CLI not available",
        )

    for candidate in build_volume_candidates(compose_name, compose_path, compose_data):
        inspected = run_docker_volume_inspect(candidate)
        if not inspected:
            continue

        mountpoint = inspected.get("Mountpoint")
        if isinstance(mountpoint, str) and mountpoint.strip():
            return NamedVolumeResolution(
                compose_name=compose_name,
                docker_name=candidate,
                mountpoint=Path(mountpoint),
                available=True,
                reason=None,
            )

    return NamedVolumeResolution(
        compose_name=compose_name,
        docker_name=None,
        mountpoint=None,
        available=False,
        reason="docker volume not found or not inspectable",
    )
