import os
import shutil
from abc import ABC, abstractmethod
from typing import Union, BinaryIO, Optional
import logging
import oss2
from src.config.settings import PROJECTS_DIR, STORAGE_TYPE
from src.utils import oss_utils

logger = logging.getLogger(__name__)

class StorageProvider(ABC):
    @abstractmethod
    def save(self, path: str, data: Union[bytes, str, BinaryIO]) -> str:
        """
        Save data to storage.
        Args:
            path: Relative path (e.g., 'projects/123/image.png')
            data: Data to save (bytes, string, or file-like object)
        Returns:
            The public or accessible URL/Path of the saved file.
        """
        pass

    @abstractmethod
    def delete(self, path: str) -> bool:
        """ 删除 file at path."""
        pass

    @abstractmethod
    def exists(self, path: str) -> bool:
        """Check if file exists."""
        pass

    @abstractmethod
    def get_url(self, path: str) -> str:
        """Get accessible URL for the file."""
        pass

    @abstractmethod
    def ensure_dir(self, path: str):
        """Ensure directory exists (mostly for local storage)."""
        pass
    
    @abstractmethod
    def list_files(self, path: str) -> list[str]:
        """ 列表 files in a directory."""
        pass

class LocalStorageProvider(StorageProvider):
    def __init__(self, root_dir: str):
        self.root_dir = root_dir
        os.makedirs(self.root_dir, exist_ok=True)

    def _get_full_path(self, path: str) -> str:
        # Remove leading slash if present to join correctly
        if path.startswith('/'):
            path = path[1:]
        return os.path.join(self.root_dir, path)

    def save(self, path: str, data: Union[bytes, str, BinaryIO]) -> str:
        full_path = self._get_full_path(path)
        os.makedirs(os.path.dirname(full_path), exist_ok=True)
        
        mode = 'wb' if isinstance(data, (bytes, BinaryIO)) else 'w'
        
        try:
            with open(full_path, mode) as f:
                if hasattr(data, 'read'):
                    shutil.copyfileobj(data, f)
                else:
                    f.write(data)
            return self.get_url(path)
        except Exception as e:
            logger.error(f"Failed to save local file {full_path}: {e}")
            raise

    def delete(self, path: str) -> bool:
        full_path = self._get_full_path(path)
        
        # Safety check: do not delete root dir
        if os.path.abspath(full_path) == os.path.abspath(self.root_dir):
            logger.warning("Attempted to delete storage root directory. Ignored.")
            return False

        if not os.path.exists(full_path):
            return True

        try:
            if os.path.isdir(full_path):
                shutil.rmtree(full_path)
            else:
                os.remove(full_path)
            return True
        except Exception as e:
            logger.error(f"Failed to delete local file {full_path}: {e}")
            return False

    def exists(self, path: str) -> bool:
        return os.path.exists(self._get_full_path(path))

    def get_url(self, path: str) -> str:
        # For local storage, we might return a file path or a served URL.
        # Assuming we serve 'data' directory via static mount or similar.
        # But for now, let's return the relative path or absolute path.
        # Ideally, we should have a base URL config.
        # For this task, let's return the absolute path for internal use, 
        # but the frontend might need a http URL.
        # The backend mounts /uploads, but PROJECTS_DIR is in /data.
        # We might need to mount /data/projects in api.py
        
        # Let's assume we will mount /data/projects as /projects static route.
        if path.startswith('/'):
            path = path[1:]
        return f"/files/{path}" 

    def ensure_dir(self, path: str):
        full_path = self._get_full_path(path)
        os.makedirs(full_path, exist_ok=True)

    def list_files(self, path: str) -> list[str]:
        full_path = self._get_full_path(path)
        if not os.path.exists(full_path):
            return []
        if os.path.isdir(full_path):
            return os.listdir(full_path)
        return []

class OSSStorageProvider(StorageProvider):
    def save(self, path: str, data: Union[bytes, str, BinaryIO]) -> str:
        if path.startswith('/'):
            path = path[1:]
            
        # Determine if data is bytes or file-like
        if isinstance(data, str):
            data_bytes = data.encode('utf-8')
        elif hasattr(data, 'read'):
            data_bytes = data.read()
        else:
            data_bytes = data

        success = oss_utils.upload_bytes(path, data_bytes)
        if not success:
            raise Exception(f"Failed to upload to OSS: {path}")
            
        return self.get_url(path)

    def delete(self, path: str) -> bool:
        if path.startswith('/'):
            path = path[1:]
        
        # Check if it looks like a directory (ends with / or we treat it as prefix if generic delete)
        # But 'path' might be 'projects/123'. 
        # For OSS, we should try to list objects with this prefix. 
        # If we find any, we delete them (simulating directory delete).
        # If not, we try to delete it as a single object.
        
        bucket = oss_utils.OSSClient.get_bucket()
        if not bucket:
            return False

        try:
            # 1. Try to list objects with prefix (add slash to ensure it matches directory structure)
            prefix = path if path.endswith('/') else f"{path}/"
            
            # 简单 list loop to get all keys
            keys_to_delete = []
            for obj in oss2.ObjectIterator(bucket, prefix=prefix):
                keys_to_delete.append(obj.key)
            
            if keys_to_delete:
                # 批处理 delete (max 1000 per call)
                for i in range(0, len(keys_to_delete), 1000):
                    batch = keys_to_delete[i:i+1000]
                    bucket.batch_delete_objects(batch)
                return True
            else:
                # 2. If no objects found with prefix/, maybe it's a single file
                return oss_utils.delete_object(path)
                
        except Exception as e:
            logger.error(f"Failed to delete OSS path {path}: {e}")
            return False

    def exists(self, path: str) -> bool:
        if path.startswith('/'):
            path = path[1:]
        return oss_utils.object_exists(path)

    def get_url(self, path: str) -> str:
        if path.startswith('/'):
            path = path[1:]
        # Use signed URL
        return oss_utils.sign_oss_url(path)

    def ensure_dir(self, path: str):
        # OSS is flat, no need to create directories
        pass
        
    def list_files(self, path: str) -> list[str]:
        # OSS listing is more complex (list_objects with prefix), not implemented in oss_utils yet.
        # now return empty or implement if needed.
        return []

class StorageManager:
    _instance = None
    _provider: StorageProvider = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super(StorageManager, cls).__new__(cls)
            cls._instance._init_provider()
        return cls._instance

    def _init_provider(self):
        # Use DATA_DIR as root for local storage (parent of projects)
        # So path 'projects/123/img.png' maps to '.../data/projects/123/img.png'
        # PROJECTS_DIR is .../data/projects
        # 配置.DATA_DIR is .../data
        from src.config.settings import DATA_DIR
        
        if STORAGE_TYPE == 'oss':
            self._provider = OSSStorageProvider()
            logger.info("StorageManager initialized with OSS provider")
        else:
            self._provider = LocalStorageProvider(root_dir=DATA_DIR)
            logger.info(f"StorageManager initialized with Local provider (root: {DATA_DIR})")

    @property
    def provider(self) -> StorageProvider:
        return self._provider

    def save(self, path: str, data: Union[bytes, str, BinaryIO]) -> str:
        return self._provider.save(path, data)

    def delete(self, path: str) -> bool:
        return self._provider.delete(path)

    def exists(self, path: str) -> bool:
        return self._provider.exists(path)

    def get_url(self, path: str) -> str:
        return self._provider.get_url(path)

    def ensure_dir(self, path: str):
        self._provider.ensure_dir(path)
        
    def list_files(self, path: str) -> list[str]:
        return self._provider.list_files(path)

    def save_from_url(self, url: str, key: str) -> Optional[str]:
        """ 下载 file from URL and save to storage.
        If URL is already a local file path (starts with /files/), returns it as is.
        """
        # If it's already a local file path (starts with /files/), skip
        if url.startswith('/files/'):
             return url
             
        try:
            import requests
            response = requests.get(url, timeout=60)
            if response.status_code != 200:
                logger.error(f"Failed to download file from {url}: {response.status_code}")
                return None
            
            return self.save(key, response.content)
        except Exception as e:
            logger.error(f"Failed to save file from {url}: {e}")
            return None

    def sign_url(self, url: str) -> str:
        """
        Sign a URL if needed (for private OSS).
        If local storage, returns URL as is.
        """
        # If it's local URL, return as is
        if url.startswith('/files/') or url.startswith('http://localhost'):
            return url
            
        # OSS provider, delegate to utils
        if isinstance(self._provider, OSSStorageProvider):
             return oss_utils.sign_oss_url(url)
        
        return url

# 全局的 instance
storage_manager = StorageManager()
