"""
Object storage with pluggable backends.

Supports local filesystem (development) and S3-compatible storage (production).

Usage:
    from core.storage import get_storage

    storage = get_storage()

    # Upload a file
    url = await storage.upload("avatars/user-123.jpg", file_content, content_type="image/jpeg")

    # Download a file
    content = await storage.download("avatars/user-123.jpg")

    # Delete a file
    await storage.delete("avatars/user-123.jpg")

    # Generate a presigned URL (S3 only)
    url = await storage.presign("avatars/user-123.jpg", expires_in=3600)
"""

import logging
from abc import ABC, abstractmethod
from functools import lru_cache
from pathlib import Path
from typing import BinaryIO

from settings import settings

logger = logging.getLogger(__name__)


class StorageBackend(ABC):
    """Abstract base class for storage backends."""

    @abstractmethod
    async def upload(
        self,
        key: str,
        data: bytes | BinaryIO,
        content_type: str | None = None,
    ) -> str:
        """
        Upload a file to storage.

        Args:
            key: Storage key (path-like, e.g., "avatars/user-123.jpg")
            data: File content as bytes or file-like object
            content_type: MIME type of the file

        Returns:
            URL or path to the uploaded file
        """
        ...

    @abstractmethod
    async def download(self, key: str) -> bytes:
        """
        Download a file from storage.

        Args:
            key: Storage key

        Returns:
            File content as bytes

        Raises:
            FileNotFoundError: If the file doesn't exist
        """
        ...

    @abstractmethod
    async def delete(self, key: str) -> None:
        """
        Delete a file from storage.

        Args:
            key: Storage key
        """
        ...

    @abstractmethod
    async def exists(self, key: str) -> bool:
        """
        Check if a file exists in storage.

        Args:
            key: Storage key

        Returns:
            True if file exists, False otherwise
        """
        ...

    async def presign(self, key: str, expires_in: int = 3600) -> str | None:
        """
        Generate a presigned URL for temporary access.

        Args:
            key: Storage key
            expires_in: URL expiration time in seconds

        Returns:
            Presigned URL or None if not supported
        """
        return None


class LocalStorageBackend(StorageBackend):
    """Local filesystem storage backend for development."""

    def __init__(self, base_path: str | Path):
        self.base_path = Path(base_path)
        self.base_path.mkdir(parents=True, exist_ok=True)

    def _get_path(self, key: str) -> Path:
        """Get full path for a key, ensuring it's within base_path."""
        path = (self.base_path / key).resolve()
        if not str(path).startswith(str(self.base_path.resolve())):
            raise ValueError(f"Invalid key: {key}")
        return path

    async def upload(
        self,
        key: str,
        data: bytes | BinaryIO,
        content_type: str | None = None,
    ) -> str:
        import aiofiles

        path = self._get_path(key)
        path.parent.mkdir(parents=True, exist_ok=True)

        if isinstance(data, bytes):
            async with aiofiles.open(path, "wb") as f:
                await f.write(data)
        else:
            async with aiofiles.open(path, "wb") as f:
                await f.write(data.read())

        logger.debug(f"Uploaded file to local storage: {key}")
        return str(path)

    async def download(self, key: str) -> bytes:
        import aiofiles

        path = self._get_path(key)
        if not path.exists():
            raise FileNotFoundError(f"File not found: {key}")

        async with aiofiles.open(path, "rb") as f:
            return await f.read()

    async def delete(self, key: str) -> None:
        path = self._get_path(key)
        if path.exists():
            path.unlink()
            logger.debug(f"Deleted file from local storage: {key}")

    async def exists(self, key: str) -> bool:
        path = self._get_path(key)
        return path.exists()


class S3StorageBackend(StorageBackend):
    """S3-compatible storage backend for production."""

    def __init__(
        self,
        bucket: str,
        region: str = "us-east-1",
        endpoint_url: str | None = None,
        access_key: str | None = None,
        secret_key: str | None = None,
    ):
        self.bucket = bucket
        self.region = region
        self.endpoint_url = endpoint_url or None
        self.access_key = access_key
        self.secret_key = secret_key

    def _get_session(self):
        """Get aioboto3 session with configured credentials."""
        import aioboto3

        return aioboto3.Session(
            aws_access_key_id=self.access_key,
            aws_secret_access_key=self.secret_key,
            region_name=self.region,
        )

    async def upload(
        self,
        key: str,
        data: bytes | BinaryIO,
        content_type: str | None = None,
    ) -> str:
        session = self._get_session()

        extra_args = {}
        if content_type:
            extra_args["ContentType"] = content_type

        async with session.client("s3", endpoint_url=self.endpoint_url) as s3:
            if isinstance(data, bytes):
                await s3.put_object(
                    Bucket=self.bucket,
                    Key=key,
                    Body=data,
                    **extra_args,
                )
            else:
                await s3.upload_fileobj(
                    data,
                    self.bucket,
                    key,
                    ExtraArgs=extra_args if extra_args else None,
                )

        logger.debug(f"Uploaded file to S3: {key}")

        # Return URL
        if self.endpoint_url:
            return f"{self.endpoint_url}/{self.bucket}/{key}"
        return f"https://{self.bucket}.s3.{self.region}.amazonaws.com/{key}"

    async def download(self, key: str) -> bytes:
        session = self._get_session()

        async with session.client("s3", endpoint_url=self.endpoint_url) as s3:
            try:
                response = await s3.get_object(Bucket=self.bucket, Key=key)
                async with response["Body"] as stream:
                    return await stream.read()
            except s3.exceptions.NoSuchKey:
                raise FileNotFoundError(f"File not found: {key}")

    async def delete(self, key: str) -> None:
        session = self._get_session()

        async with session.client("s3", endpoint_url=self.endpoint_url) as s3:
            await s3.delete_object(Bucket=self.bucket, Key=key)
            logger.debug(f"Deleted file from S3: {key}")

    async def exists(self, key: str) -> bool:
        session = self._get_session()

        async with session.client("s3", endpoint_url=self.endpoint_url) as s3:
            try:
                await s3.head_object(Bucket=self.bucket, Key=key)
                return True
            except Exception:
                return False

    async def presign(self, key: str, expires_in: int = 3600) -> str:
        """Generate a presigned URL for temporary access."""
        session = self._get_session()

        async with session.client("s3", endpoint_url=self.endpoint_url) as s3:
            url = await s3.generate_presigned_url(
                "get_object",
                Params={"Bucket": self.bucket, "Key": key},
                ExpiresIn=expires_in,
            )
            return url


@lru_cache
def get_storage() -> StorageBackend:
    """
    Get the configured storage backend.

    Returns LocalStorageBackend or S3StorageBackend based on settings.
    """
    backend = settings.storage_backend.lower()

    if backend == "local":
        return LocalStorageBackend(settings.storage_local_path)

    elif backend == "s3":
        if not settings.storage_s3_bucket:
            raise ValueError("STORAGE_S3_BUCKET is required for S3 backend")

        return S3StorageBackend(
            bucket=settings.storage_s3_bucket,
            region=settings.storage_s3_region,
            endpoint_url=settings.storage_s3_endpoint_url or None,
            access_key=settings.storage_s3_access_key or None,
            secret_key=settings.storage_s3_secret_key or None,
        )

    else:
        raise ValueError(f"Unknown storage backend: {backend}")
