import asyncio
import mimetypes
from pathlib import Path
from typing import Any, List, Optional, Sequence

import aiofiles
import magic

from mineru_flow.internal.storage.base import (
    StorageFile,
    StorageNotFoundError,
    StorageOperator,
    StoragePermissionError,
)


class LocalFileStorageOperator(StorageOperator):
    """Storage operator for the local file system."""

    def __init__(self, base_path: str | Path, **kwargs: Any):
        super().__init__(**kwargs)

        if not base_path:
            raise ValueError("base_path must be specified for LocalFileStorageOperator")

        self._base_path = Path(base_path).expanduser().resolve() if base_path else None

    def _resolve_path(self, path: str | Path) -> Path:
        candidate = Path(path)
        if not candidate.is_absolute():
            if self._base_path is not None:
                candidate = self._base_path / candidate
            else:
                candidate = Path.cwd() / candidate
        candidate = candidate.expanduser()
        resolved = candidate.resolve(strict=False)

        if self._base_path is not None:
            try:
                resolved.relative_to(self._base_path)
            except ValueError as exc:
                raise StoragePermissionError(
                    f"Path '{resolved}' escapes base directory '{self._base_path}'"
                ) from exc

        return resolved

    async def _read(self, path: str, *, max_bytes: Optional[int]) -> bytes:
        resolved = self._resolve_path(path)
        try:
            async with aiofiles.open(resolved, "rb") as handle:
                if max_bytes is not None:
                    return await handle.read(max_bytes)
                return await handle.read()
        except FileNotFoundError as exc:
            raise StorageNotFoundError(f"Local file not found: {resolved}") from exc
        except PermissionError as exc:
            raise StoragePermissionError(f"Permission denied: {resolved}") from exc

    async def _stream(self, path: str, *, chunk_size: int):
        resolved = self._resolve_path(path)
        try:
            async with aiofiles.open(resolved, "rb") as handle:
                while True:
                    chunk = await handle.read(chunk_size)
                    if not chunk:
                        break
                    yield chunk
        except FileNotFoundError as exc:
            raise StorageNotFoundError(f"Local file not found: {resolved}") from exc
        except PermissionError as exc:
            raise StoragePermissionError(f"Permission denied: {resolved}") from exc

    async def _list(self, location: str, *, recursive: bool) -> Sequence[StorageFile]:
        root = self._resolve_path(location)

        def _collect() -> List[StorageFile]:
            if not root.exists():
                raise FileNotFoundError(str(root))
            if root.is_file():
                return [self._build_file(root)]

            iterator = root.rglob("*") if recursive else root.iterdir()
            results: List[StorageFile] = []
            for item in iterator:
                if item.is_file():
                    results.append(self._build_file(item))
            return results

        try:
            return await asyncio.to_thread(_collect)
        except FileNotFoundError as exc:
            raise StorageNotFoundError(f"Local path not found: {root}") from exc
        except PermissionError as exc:
            raise StoragePermissionError(f"Permission denied: {root}") from exc

    async def _get_mime(self, path: str) -> Optional[str]:
        resolved = self._resolve_path(path)

        if magic is not None:

            def _detect() -> Optional[str]:
                try:
                    if hasattr(magic, "from_file"):
                        return magic.from_file(str(resolved), mime=True)  # type: ignore[arg-type]
                    if hasattr(magic, "Magic"):
                        detector = magic.Magic(mime=True)  # type: ignore[attr-defined]
                        return detector.from_file(str(resolved))
                except Exception:
                    return None
                return None

            detected = await asyncio.to_thread(_detect)
            if detected:
                return detected

        return mimetypes.guess_type(str(resolved))[0]

    def _build_file(self, path: Path) -> StorageFile:
        stat = path.stat()
        return StorageFile(
            path=str(path),
            name=path.name,
            size=stat.st_size,
            mime_type=mimetypes.guess_type(str(path))[0],
        )
