"""BaseFileClient — shared base for file-based dataset clients."""

from __future__ import annotations

from pathlib import Path
from typing import ClassVar

from radiens_core._base_client import BaseClient
from radiens_core._converters._dataset import (
    dataset_metadata_map_from_proto,
    file_info_list_from_proto,
    file_type_to_proto,
    guess_file_type,
)
from radiens_core._converters._validation import validate_proto
from radiens_core._generated import common_pb2, datasource_pb2
from radiens_core._services._radiens_core import (
    clear_data_source_proto,
    copy_data_source_file_proto,
    export_data_source_proto,
    list_data_source_ids_proto,
    list_data_sources_proto,
    list_directory_proto,
    move_data_source_file_proto,
    remove_data_source_file_proto,
    set_datasource,
)
from radiens_core._services._radiens_lifecycle import healthcheck_proto
from radiens_core._transport._channel_pool import ChannelPool
from radiens_core._transport._discovery import discover_radiens_addresses
from radiens_core.exceptions import RadiensError
from radiens_core.models.common import ClientType, ServiceEndpoint
from radiens_core.models.dataset import DatasetMetadata, FileInfo, FlexDataset


class BaseFileClient(BaseClient):
    """Shared base for file-based dataset clients (Videre, Curate).

    Handles service discovery, dataset tracking, file linking, and health checks.
    Subclasses must declare ``_client_type``.
    """

    _client_type: ClassVar[ClientType]

    def __init__(self) -> None:
        # 1. Discover addresses
        tmp_pool = ChannelPool()
        try:
            addresses = discover_radiens_addresses(tmp_pool)
        finally:
            tmp_pool.close()

        # 2. Base init
        core_addr = addresses.get(ServiceEndpoint.RADIENS_CORE, "")
        super().__init__(
            addresses=addresses,
            auth_server_address=core_addr,
            fallback_service=ServiceEndpoint.RADIENS_CORE,
        )

        self._datasets: dict[str, DatasetMetadata] = {}

    def _resolve_dataset(self, ref: FlexDataset) -> DatasetMetadata:
        """Resolve a dataset reference to DatasetMetadata."""
        if isinstance(ref, DatasetMetadata):
            return ref
        ref_str = str(ref)
        if ref_str in self._datasets:
            return self._datasets[ref_str]
        raise ValueError(f"Unknown dataset: {ref_str}. Link it first with link_data_file().")

    def healthcheck(self, service: ServiceEndpoint = ServiceEndpoint.RADIENS_LIFECYCLE) -> None:
        """Verify the radiensserver is responsive.

        Args:
            service: Which gRPC service to probe (default ``RADIENS_LIFECYCLE``).
                ``RADIENS_CORE`` is also supported.

        Raises:
            RadiensError: If the health check fails or the server is unresponsive.
        """
        addr = self._server_address(service)
        response = healthcheck_proto(self._pool, addr, self._client_type, service=service)
        if response.errMsg:
            raise RadiensError(f"Health check failed: {response.errMsg}")

    def link_data_file(
        self,
        path: str | Path,
    ) -> DatasetMetadata:
        """Link a recording file and return its metadata.

        Also discovers and registers any associated ``.spikes`` file found
        alongside the recording. If found, the returned ``DatasetMetadata`` will have
        ``associated_spikes_ids`` populated, enabling the spike data methods on
        ``VidereClient``.

        Args:
            path: Path to the recording file (e.g. ``.xdat``, ``.rhd``).

        Returns:
            Metadata for the linked dataset.

        Raises:
            RadiensError: On communication failure or server error.
        """
        addr = self._server_address(ServiceEndpoint.RADIENS_CORE)
        meta = set_datasource(self._pool, addr, str(path))
        self._datasets[meta.id] = meta
        return meta

    def list_data_sources(self, dsource_ids: list[str] | str | None = None) -> dict[str, DatasetMetadata]:
        """List linked data sources and their metadata.

        Args:
            dsource_ids: Optional list of specific IDs to query. If ``None``, lists all.

        Returns:
            Map of data source ID to metadata.
        """
        ids = [dsource_ids] if isinstance(dsource_ids, str) else dsource_ids
        addr = self._server_address(ServiceEndpoint.RADIENS_CORE)
        proto = list_data_sources_proto(self._pool, addr, ids)
        meta_map = dataset_metadata_map_from_proto(proto)

        # Sync local cache
        self._datasets.update(meta_map)
        return meta_map

    def list_data_source_ids(self) -> list[str]:
        """List IDs of all currently linked data sources.

        Returns:
            List of data source IDs.
        """
        addr = self._server_address(ServiceEndpoint.RADIENS_CORE)
        proto = list_data_source_ids_proto(self._pool, addr)
        return list(proto.sortedID)

    def clear_data_sources(self, dsource_ids: list[str] | str) -> list[str]:
        """Clear (unlink) data sources from the server.

        Args:
            dsource_ids: List of IDs (or a single ID) to clear.

        Returns:
            List of successfully cleared IDs.
        """
        ids = [dsource_ids] if isinstance(dsource_ids, str) else dsource_ids
        addr = self._server_address(ServiceEndpoint.RADIENS_CORE)
        proto = clear_data_source_proto(self._pool, addr, ids)
        cleared_ids = list(proto.sortedID)

        # Sync local cache
        for dsid in cleared_ids:
            self._datasets.pop(dsid, None)

        return cleared_ids

    def export_data_source(
        self,
        dsource: FlexDataset,
        dest_path: str | Path,
        force: bool = False,
    ) -> None:
        """Export a data source to a new file.

        Args:
            dsource: The data source to export (ID or metadata object).
            dest_path: Destination file path.
            force: If ``True``, overwrite existing destination file.
        """
        meta = self._resolve_dataset(dsource)
        dest = Path(dest_path)

        src_desc = common_pb2.FileDescriptor(
            path=meta.path,
            baseName=meta.base_name,
            fileType=file_type_to_proto(meta.file_type),
            fileNameUID=meta.id,
        )
        dest_desc = common_pb2.FileDescriptor(
            path=str(dest.parent),
            baseName=dest.stem,
            fileType=file_type_to_proto(meta.file_type),
        )

        req = datasource_pb2.ExportDataSourceFileRequest(
            src=src_desc,
            dest=dest_desc,
            isForce=force,
        )
        validate_proto(req)

        addr = self._server_address(ServiceEndpoint.RADIENS_CORE)
        export_data_source_proto(self._pool, addr, req)

    def list_directory(self, directory: str | Path) -> list[FileInfo]:
        """List files in a remote server-side directory.

        Args:
            directory: Server-side directory path to list.

        Returns:
            Metadata for each file found in the directory.
        """
        addr = self._server_address(ServiceEndpoint.RADIENS_CORE)
        reply = list_directory_proto(self._pool, addr, str(directory))
        return file_info_list_from_proto(reply)

    def copy_data_source_file(
        self,
        src_path: str | Path,
        dest_path: str | Path,
        *,
        force: bool = False,
    ) -> None:
        """Copy a server-side data source file to a new location.

        Args:
            src_path: Source file path on the server.
            dest_path: Destination file path on the server.
            force: If ``True``, overwrite existing destination file.
        """
        req = datasource_pb2.CopyRemoveDataSourceFileRequest(
            one=datasource_pb2.CopyRemoveDataSourceFileRequest.OneFile(
                src=self._path_to_file_descriptor(src_path),
                dest=self._path_to_file_descriptor(dest_path),
            ),
            isForce=force,
        )
        addr = self._server_address(ServiceEndpoint.RADIENS_CORE)
        copy_data_source_file_proto(self._pool, addr, req)

    def move_data_source_file(
        self,
        src_path: str | Path,
        dest_path: str | Path,
        *,
        force: bool = False,
    ) -> None:
        """Move a server-side data source file to a new location.

        Args:
            src_path: Source file path on the server.
            dest_path: Destination file path on the server.
            force: If ``True``, overwrite existing destination file.
        """
        req = datasource_pb2.MoveDataSourceFileRequest(
            src=self._path_to_file_descriptor(src_path),
            dest=self._path_to_file_descriptor(dest_path),
            isForce=force,
        )
        addr = self._server_address(ServiceEndpoint.RADIENS_CORE)
        move_data_source_file_proto(self._pool, addr, req)

    def remove_data_source_file(
        self,
        path: str | Path,
        *,
        force: bool = False,
    ) -> None:
        """Remove a server-side data source file.

        Args:
            path: File path on the server to remove.
            force: If ``True``, force removal even if the file is in use.
        """
        req = datasource_pb2.CopyRemoveDataSourceFileRequest(
            one=datasource_pb2.CopyRemoveDataSourceFileRequest.OneFile(
                src=self._path_to_file_descriptor(path),
            ),
            isForce=force,
        )
        addr = self._server_address(ServiceEndpoint.RADIENS_CORE)
        remove_data_source_file_proto(self._pool, addr, req)

    # --- Private helpers ---

    def _path_to_file_descriptor(self, path: str | Path) -> common_pb2.FileDescriptor:
        """Build a FileDescriptor proto from a file path."""
        p = Path(path)
        return common_pb2.FileDescriptor(
            path=str(p.parent),
            baseName=p.stem,
            fileType=file_type_to_proto(guess_file_type(str(p))),
        )
