from __future__ import annotations

from pathlib import Path
from typing import Any

import yaml

from dockervault.models import (
    ComposeProject,
    DEFAULT_COMPOSE_FILENAMES,
    MountMapping,
    ServiceDefinition,
    normalize_path,
)


def find_compose_files(base_path: Path) -> list[Path]:
    """Find likely Docker Compose files under base_path."""
    matches: list[Path] = []

    for path in base_path.rglob("*"):
        if path.is_file() and path.name in DEFAULT_COMPOSE_FILENAMES:
            matches.append(path)

    return sorted(matches)


def load_yaml_file(compose_path: Path) -> dict[str, Any]:
    try:
        content = compose_path.read_text(encoding="utf-8")
    except UnicodeDecodeError:
        content = compose_path.read_text(encoding="utf-8", errors="ignore")

    data = yaml.safe_load(content) or {}
    if not isinstance(data, dict):
        return {}
    return data


def parse_env_files(value: Any) -> list[str]:
    if isinstance(value, str):
        return [value]

    if isinstance(value, list):
        items: list[str] = []
        for item in value:
            if isinstance(item, str):
                items.append(item)
            elif isinstance(item, dict):
                path = item.get("path")
                if isinstance(path, str):
                    items.append(path)
        return sorted(set(items))

    return []


def normalize_volume_dict(volume: dict[str, Any]) -> MountMapping | None:
    source = volume.get("source") or volume.get("src") or ""
    target = volume.get("target") or volume.get("dst") or volume.get("destination") or ""
    if not isinstance(target, str) or not target:
        return None

    kind = volume.get("type") or ("bind" if source and str(source).startswith(("/", ".", "~")) else "volume")
    read_only = bool(volume.get("read_only") or volume.get("readonly"))

    return MountMapping(
        source=str(source),
        target=target,
        kind=str(kind),
        read_only=read_only,
    )


def normalize_volume_string(value: str) -> MountMapping | None:
    parts = value.split(":")
    if len(parts) == 1:
        return MountMapping(source="", target=parts[0], kind="anonymous", read_only=False)

    if len(parts) >= 2:
        source = parts[0]
        target = parts[1]
        options = parts[2:]
        read_only = any(option == "ro" for option in options)

        if source.startswith(("/", ".", "~")):
            kind = "bind"
        else:
            kind = "volume"

        return MountMapping(source=source, target=target, kind=kind, read_only=read_only)

    return None


def parse_mounts(value: Any) -> list[MountMapping]:
    mounts: list[MountMapping] = []

    if not isinstance(value, list):
        return mounts

    for item in value:
        mapping: MountMapping | None = None
        if isinstance(item, str):
            mapping = normalize_volume_string(item)
        elif isinstance(item, dict):
            mapping = normalize_volume_dict(item)

        if mapping:
            mounts.append(mapping)

    return mounts


def parse_service_definition(name: str, data: Any) -> ServiceDefinition:
    if not isinstance(data, dict):
        return ServiceDefinition(name=name)

    mounts = parse_mounts(data.get("volumes", []))
    env_files = parse_env_files(data.get("env_file"))

    return ServiceDefinition(
        name=name,
        image=data.get("image") if isinstance(data.get("image"), str) else None,
        restart=data.get("restart") if isinstance(data.get("restart"), str) else None,
        env_files=env_files,
        mounts=mounts,
    )


def merge_service(existing: ServiceDefinition, incoming: ServiceDefinition) -> ServiceDefinition:
    mounts_by_key: dict[tuple[str, str, str, bool], MountMapping] = {
        (mount.source, mount.target, mount.kind, mount.read_only): mount
        for mount in existing.mounts
    }
    for mount in incoming.mounts:
        mounts_by_key[(mount.source, mount.target, mount.kind, mount.read_only)] = mount

    env_files = sorted(set(existing.env_files) | set(incoming.env_files))

    return ServiceDefinition(
        name=existing.name,
        image=incoming.image or existing.image,
        restart=incoming.restart or existing.restart,
        env_files=env_files,
        mounts=sorted(mounts_by_key.values(), key=lambda item: (item.target, item.source, item.kind)),
    )


def extract_project_from_compose(folder: Path, compose_files: list[Path]) -> ComposeProject:
    services_by_name: dict[str, ServiceDefinition] = {}
    named_volumes: set[str] = set()
    backup_paths: set[str] = set()

    for compose_file in sorted(compose_files):
        data = load_yaml_file(compose_file)

        for volume_name in (data.get("volumes") or {}).keys() if isinstance(data.get("volumes"), dict) else []:
            if isinstance(volume_name, str):
                named_volumes.add(volume_name)

        raw_services = data.get("services") or {}
        if not isinstance(raw_services, dict):
            continue

        for service_name, service_data in raw_services.items():
            if not isinstance(service_name, str):
                continue

            incoming = parse_service_definition(service_name, service_data)
            if service_name in services_by_name:
                services_by_name[service_name] = merge_service(services_by_name[service_name], incoming)
            else:
                services_by_name[service_name] = incoming

    for service in services_by_name.values():
        for mount in service.mounts:
            if mount.kind == "bind" and mount.source:
                candidate = Path(mount.source).expanduser()
                if not candidate.is_absolute():
                    candidate = (folder / candidate).resolve()
                backup_paths.add(str(candidate))

        for env_file in service.env_files:
            candidate = Path(env_file).expanduser()
            if not candidate.is_absolute():
                candidate = (folder / candidate).resolve()
            backup_paths.add(str(candidate))

    return ComposeProject(
        name=folder.name,
        root_path=normalize_path(folder),
        compose_files=[file.name for file in sorted(compose_files)],
        services=sorted(services_by_name.values(), key=lambda item: item.name),
        named_volumes=sorted(named_volumes),
        backup_paths=sorted(backup_paths),
    )


def group_projects_by_folder(compose_files: list[Path]) -> list[ComposeProject]:
    grouped: dict[Path, list[Path]] = {}

    for compose_file in compose_files:
        grouped.setdefault(compose_file.parent, []).append(compose_file)

    projects: list[ComposeProject] = []

    for folder, files in sorted(grouped.items()):
        projects.append(extract_project_from_compose(folder, files))

    return projects


def scan_projects(base_path: Path) -> list[ComposeProject]:
    if not base_path.exists():
        raise FileNotFoundError(f"Path does not exist: {base_path}")

    if not base_path.is_dir():
        raise NotADirectoryError(f"Path is not a directory: {base_path}")

    compose_files = find_compose_files(base_path)
    return group_projects_by_folder(compose_files)
