from abc import ABC, abstractmethod
from collections.abc import Callable
from pathlib import Path
from typing import Any

from packaging.version import InvalidVersion, Version

from harbor.models.dataset_item import DownloadedDatasetItem
from harbor.models.registry import DatasetMetadata, DatasetSummary
from harbor.tasks.client import TaskClient, TaskDownloadResult, TaskIdType


def resolve_version(versions: list[str]) -> str:
    """
    Resolve the best version from a list of versions.

    Priority:
    1. "head" if it exists
    2. Highest semantic version (using PEP 440)
    3. Lexically last version
    """
    if not versions:
        raise ValueError("No versions available")

    if "head" in versions:
        return "head"

    semver_versions: list[tuple[Version, str]] = []
    for v in versions:
        try:
            semver_versions.append((Version(v), v))
        except InvalidVersion:
            pass

    if semver_versions:
        semver_versions.sort(key=lambda x: x[0], reverse=True)
        return semver_versions[0][1]

    return sorted(versions, reverse=True)[0]


class BaseRegistryClient(ABC):
    def __init__(self, url: str | None = None, path: Path | None = None):
        self._url = url
        self._path = path
        self._task_client = TaskClient()

    @abstractmethod
    async def _get_dataset_metadata(self, name: str) -> DatasetMetadata:
        """Internal method to get dataset metadata by name.

        The name string may contain version info (e.g. "dataset@version").
        Each client parses it in its own way.
        """
        pass

    @abstractmethod
    async def list_datasets(self) -> list[DatasetSummary]:
        """List all datasets available in the registry."""
        pass

    async def get_dataset_metadata(self, name: str) -> DatasetMetadata:
        """Get dataset metadata by name.

        The name string may contain version info (e.g. "dataset@version").
        Each client parses it in its own way.
        """
        return await self._get_dataset_metadata(name)

    async def download_dataset_files(
        self,
        metadata: DatasetMetadata,
        overwrite: bool = False,
        output_dir: Path | None = None,
    ) -> dict[str, Path]:
        """Download dataset-level files. Returns {filename: local_path}.

        Base implementation is a no-op. Subclasses with dataset-level files
        (e.g., PackageDatasetClient) should override this.
        """
        return {}

    async def download_dataset(
        self,
        name: str,
        overwrite: bool = False,
        output_dir: Path | None = None,
        on_task_download_start: Callable[[TaskIdType], Any] | None = None,
        on_task_download_complete: Callable[[TaskIdType, TaskDownloadResult], Any]
        | None = None,
        on_total_known: Callable[[int], Any] | None = None,
    ) -> list[DownloadedDatasetItem]:
        metadata = await self.get_dataset_metadata(name)

        if on_total_known is not None:
            on_total_known(len(metadata.task_ids))

        result = await self._task_client.download_tasks(
            task_ids=metadata.task_ids,
            overwrite=overwrite,
            output_dir=output_dir,
            on_task_download_start=on_task_download_start,
            on_task_download_complete=on_task_download_complete,
        )

        await self.download_dataset_files(
            metadata, overwrite=overwrite, output_dir=output_dir
        )

        return [
            DownloadedDatasetItem(
                id=task_id,
                downloaded_path=path,
            )
            for task_id, path in zip(metadata.task_ids, result.paths)
        ]
