import asyncio
import mimetypes
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, AsyncIterator, Dict, Iterable, List, Optional, Sequence, Set

DEFAULT_MIME_TYPE = "application/octet-stream"

try:
    import magic
except Exception:
    magic = None


@dataclass(slots=True)
class StorageFile:
    path: str
    name: str
    size: Optional[int] = None
    mime_type: Optional[str] = None
    metadata: Optional[Dict[str, Any]] = None


class StorageError(Exception):
    """Base exception class for storage operations."""


class StorageNotFoundError(StorageError):
    """Raised when a file or directory cannot be found in storage."""


class StoragePermissionError(StorageError):
    """Raised when the user has insufficient permissions."""


class StorageAuthenticationError(StorageError):
    """Raised when authentication against the backend fails."""


class StorageUnsupportedError(StorageError):
    """Raised when an operation is not supported by the backend."""


class StorageOperator(ABC):
    """Abstract base class for asynchronous storage operators."""

    def __init__(
        self, *, mime_probe_bytes: int = 8192, mime_detect_concurrency: int = 5
    ):
        self._mime_probe_bytes = mime_probe_bytes
        self._mime_detect_concurrency = max(1, mime_detect_concurrency)

    async def stream_read(
        self, path: str, *, chunk_size: int = 4 * 1024 * 1024
    ) -> AsyncIterator[bytes]:
        """Stream ``path`` from the backend in chunks."""

        if chunk_size <= 0:
            raise ValueError("chunk_size must be positive")

        async for chunk in self._stream(path, chunk_size=chunk_size):
            yield chunk

    async def read(self, path: str, *, max_bytes: Optional[int] = None) -> bytes:
        """Read ``path`` from the backend."""
        try:
            data = await self._read(path, max_bytes=max_bytes)
        except StorageError:
            raise
        except Exception as exc:
            raise StorageError(f"Failed to read '{path}': {exc}") from exc

        if max_bytes is not None and len(data) > max_bytes:
            return data[:max_bytes]
        return data

    async def list_files(
        self,
        location: str,
        *,
        suffixes: Optional[Iterable[str]] = None,
        recursive: bool = True,
        with_mime: bool = False,
    ) -> List[StorageFile]:
        """List files beneath ``location`` filtered by ``suffixes``."""
        try:
            files = list(await self._list(location, recursive=recursive))
        except StorageError:
            raise
        except Exception as exc:
            raise StorageError(f"Failed to list files for '{location}': {exc}") from exc

        suffix_set = self._normalize_suffixes(suffixes)
        if suffix_set:
            files = [f for f in files if self._matches_suffix(f.path, suffix_set)]

        if with_mime:
            await self._populate_mime(files)

        return files

    async def detect_mime(self, path: str, *, data: Optional[bytes] = None) -> str:
        """Detect the MIME type for ``path`` as efficiently as possible."""
        mime: Optional[str]
        try:
            mime = await self._get_mime(path)
        except StorageError:
            raise
        except Exception as exc:  # pragma: no cover - defensive
            raise StorageError(f"Failed to detect mime for '{path}': {exc}") from exc

        if mime:
            return mime

        if data is None:
            try:
                data = await self.read(path, max_bytes=self._mime_probe_bytes)
            except StorageError:
                raise

        if data:
            sniffed = await self._detect_from_buffer(data)
            if sniffed:
                return sniffed

        guess = mimetypes.guess_type(self._strip_url_params(path))[0]
        return guess or DEFAULT_MIME_TYPE

    async def _populate_mime(self, files: Sequence[StorageFile]) -> None:
        semaphore = asyncio.Semaphore(self._mime_detect_concurrency)

        async def _fill(file: StorageFile) -> None:
            if file.mime_type:
                return
            async with semaphore:
                file.mime_type = await self.detect_mime(file.path)

        await asyncio.gather(*(_fill(file) for file in files))

    def _normalize_suffixes(self, suffixes: Optional[Iterable[str]]) -> Set[str]:
        if not suffixes:
            return set()
        normalized = set()
        for suffix in suffixes:
            if not suffix:
                continue
            s = suffix.lower().strip()
            if not s.startswith("."):
                s = f".{s}"
            normalized.add(s)
        return normalized

    def _matches_suffix(self, path: str, suffixes: Set[str]) -> bool:
        target = self._extract_suffix(path)
        return not suffixes or target in suffixes

    @staticmethod
    def _extract_suffix(path: str) -> str:
        clean = StorageOperator._strip_url_params(path).lower()
        dot_index = clean.rfind(".")
        if dot_index == -1:
            return ""
        return clean[dot_index:]

    @staticmethod
    def _strip_url_params(path: str) -> str:
        for sep in ("?", "#"):
            if sep in path:
                path = path.split(sep, 1)[0]
        return path

    async def _detect_from_buffer(self, data: bytes) -> Optional[str]:
        if not data:
            return None

        if magic is None:
            return None

        def _detect(buffer: bytes) -> Optional[str]:
            try:
                if hasattr(magic, "from_buffer"):
                    return magic.from_buffer(buffer, mime=True)  # type: ignore[arg-type]
                if hasattr(magic, "Magic"):
                    detector = magic.Magic(mime=True)  # type: ignore[attr-defined]
                    return detector.from_buffer(buffer)
            except Exception:  # pragma: no cover - best effort
                return None
            return None

        return await asyncio.to_thread(_detect, data)

    @abstractmethod
    async def _read(self, path: str, *, max_bytes: Optional[int]) -> bytes:
        """Backend-specific implementation for ``read``."""

    @abstractmethod
    async def _list(self, location: str, *, recursive: bool) -> Sequence[StorageFile]:
        """Backend-specific implementation for ``list_files``."""

    @abstractmethod
    async def _stream(
        self, path: str, *, chunk_size: int
    ) -> AsyncIterator[bytes]:
        """Backend-specific implementation for ``stream_read``."""

    async def _get_mime(self, path: str) -> Optional[str]:
        """Backends can override to provide faster MIME detection."""
        return None
