# app/modules/core/services/storage_service.py
"""
Storage abstraction service for file uploads.

Provides a unified interface for file storage with support for:
- Local filesystem (default, development)
- Cloudflare R2 (production, S3-compatible)

Usage:
    from app.modules.core.services import get_storage_backend

    storage = get_storage_backend()
    url = await storage.upload("path/to/file.jpg", file_bytes, "image/jpeg")
    await storage.delete("path/to/file.jpg")
"""

import logging
from abc import ABC, abstractmethod
from pathlib import Path

from botocore.exceptions import ClientError

from app.core.config import settings

logger = logging.getLogger(__name__)


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

    @abstractmethod
    async def upload(self, file_path: str, content: bytes, content_type: str) -> str:
        """
        Upload a file to storage.

        Args:
            file_path: Relative path where file should be stored
            content: File content as bytes
            content_type: MIME type of the file

        Returns:
            Public URL to access the file
        """

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

        Args:
            file_path: Relative path of file to delete

        Returns:
            True if file was deleted, False if not found
        """

    @abstractmethod
    def get_url(self, file_path: str) -> str:
        """
        Get the public URL for a file.

        Args:
            file_path: Relative path of the file

        Returns:
            Public URL to access the file
        """

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

        Args:
            file_path: Relative path of the file

        Returns:
            True if file exists
        """


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

    def __init__(self, base_dir: str = "uploads"):
        """
        Initialize local storage backend.

        Args:
            base_dir: Base directory for file storage (relative to project root)
        """
        self.base_dir = Path(base_dir)
        self.base_dir.mkdir(parents=True, exist_ok=True)
        logger.info(f"LocalStorageBackend initialized with base_dir: {self.base_dir}")

    async def upload(self, file_path: str, content: bytes, content_type: str) -> str:
        """Upload file to local filesystem."""
        full_path = self.base_dir / file_path

        # Ensure parent directory exists
        full_path.parent.mkdir(parents=True, exist_ok=True)

        # Write file
        full_path.write_bytes(content)

        logger.debug(f"Uploaded to local: {file_path} ({len(content)} bytes)")

        return self.get_url(file_path)

    async def delete(self, file_path: str) -> bool:
        """Delete file from local filesystem."""
        full_path = self.base_dir / file_path

        if full_path.exists():
            full_path.unlink()
            logger.debug(f"Deleted from local: {file_path}")

            # Clean up empty parent directories
            self._cleanup_empty_dirs(full_path.parent)
            return True

        return False

    def get_url(self, file_path: str) -> str:
        """Get URL for local file (served via /uploads mount)."""
        return f"/uploads/{file_path}"

    async def exists(self, file_path: str) -> bool:
        """Check if file exists locally."""
        return (self.base_dir / file_path).exists()

    def _cleanup_empty_dirs(self, dir_path: Path) -> None:
        """Remove empty directories up to base_dir."""
        try:
            while dir_path != self.base_dir and dir_path.exists():
                if not any(dir_path.iterdir()):
                    dir_path.rmdir()
                    dir_path = dir_path.parent
                else:
                    break
        except OSError:
            pass


class R2StorageBackend(StorageBackend):
    """Cloudflare R2 storage backend (S3-compatible)."""

    def __init__(self):
        """Initialize R2 storage backend."""
        import boto3
        from botocore.config import Config

        if not all([
            settings.r2_account_id,
            settings.r2_access_key_id,
            settings.r2_secret_access_key,
        ]):
            raise ValueError(
                "R2 storage requires R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, "
                "and R2_SECRET_ACCESS_KEY environment variables"
            )

        # R2 endpoint URL
        endpoint_url = f"https://{settings.r2_account_id}.r2.cloudflarestorage.com"

        # Configure boto3 client for R2
        self.client = boto3.client(
            "s3",
            endpoint_url=endpoint_url,
            aws_access_key_id=settings.r2_access_key_id,
            aws_secret_access_key=settings.r2_secret_access_key,
            config=Config(
                signature_version="s3v4",
                retries={"max_attempts": 3, "mode": "adaptive"},
            ),
        )

        self.bucket_name = settings.r2_bucket_name
        self.public_url = settings.r2_public_url

        logger.info(
            f"R2StorageBackend initialized: bucket={self.bucket_name}, "
            f"public_url={self.public_url or 'default'}"
        )

    async def upload(self, file_path: str, content: bytes, content_type: str) -> str:
        """Upload file to R2."""
        try:
            self.client.put_object(
                Bucket=self.bucket_name,
                Key=file_path,
                Body=content,
                ContentType=content_type,
            )

            logger.debug(f"Uploaded to R2: {file_path} ({len(content)} bytes)")

            return self.get_url(file_path)

        except ClientError as e:
            logger.error(f"R2 upload failed for {file_path}: {e}")
            raise

    async def delete(self, file_path: str) -> bool:
        """Delete file from R2."""
        try:
            # Check if file exists first
            if not await self.exists(file_path):
                return False

            self.client.delete_object(
                Bucket=self.bucket_name,
                Key=file_path,
            )

            logger.debug(f"Deleted from R2: {file_path}")
            return True

        except ClientError as e:
            logger.error(f"R2 delete failed for {file_path}: {e}")
            return False

    def get_url(self, file_path: str) -> str:
        """Get public URL for R2 file."""
        if self.public_url:
            # Use custom domain
            return f"{self.public_url.rstrip('/')}/{file_path}"
        # Use default R2 public URL pattern
        # Note: Bucket must have public access enabled
        return f"https://{self.bucket_name}.{settings.r2_account_id}.r2.dev/{file_path}"

    async def exists(self, file_path: str) -> bool:
        """Check if file exists in R2."""
        try:
            self.client.head_object(Bucket=self.bucket_name, Key=file_path)
            return True
        except self.client.exceptions.ClientError as e:
            if e.response.get("Error", {}).get("Code") == "404":
                return False
            raise


# =============================================================================
# STORAGE BACKEND FACTORY
# =============================================================================

_storage_backend: StorageBackend | None = None


def get_storage_backend() -> StorageBackend:
    """
    Get the configured storage backend instance.

    Returns:
        Storage backend based on STORAGE_BACKEND setting

    Raises:
        ValueError: If storage backend is misconfigured
    """
    global _storage_backend

    if _storage_backend is not None:
        return _storage_backend

    backend_type = settings.storage_backend.lower()

    if backend_type == "r2":
        _storage_backend = R2StorageBackend()
    elif backend_type == "local":
        _storage_backend = LocalStorageBackend()
    else:
        raise ValueError(f"Unknown storage backend: {backend_type}")

    return _storage_backend


def reset_storage_backend() -> None:
    """Reset the storage backend (useful for testing)."""
    global _storage_backend
    _storage_backend = None


# =============================================================================
# PUBLIC API
# =============================================================================
__all__ = [
    "StorageBackend",
    "LocalStorageBackend",
    "R2StorageBackend",
    "get_storage_backend",
    "reset_storage_backend",
]
