"""可插拔存储后端 — Local / AWS S3 / Cloudflare R2 / 阿里云 OSS

使用方式：
    from app.core.storage import get_storage
    storage = get_storage()
    url = await storage.save(filename, content_bytes)
"""
import io
import os
from abc import ABC, abstractmethod
from typing import Optional

from app.config import settings


class BaseStorage(ABC):
    @abstractmethod
    async def save(self, filename: str, data: bytes, content_type: str = "image/webp") -> str:
        """保存文件，返回可公开访问的 URL。"""

    @abstractmethod
    async def delete(self, filename: str) -> None:
        """删除文件（可选实现，忽略不存在的文件）。"""

    @abstractmethod
    async def list_files(self, prefix: str = "") -> list[str]:
        """列出存储中所有文件名（仅文件名，不含路径/前缀）。"""


# ── 本地文件系统 ────────────────────────────────────────────────────────

class LocalStorage(BaseStorage):
    def __init__(self, upload_dir: str, url_prefix: str):
        self._dir = upload_dir
        self._prefix = url_prefix.rstrip("/")
        os.makedirs(self._dir, exist_ok=True)

    async def save(self, filename: str, data: bytes, content_type: str = "image/webp") -> str:
        path = os.path.join(self._dir, filename)
        os.makedirs(os.path.dirname(path), exist_ok=True)
        with open(path, "wb") as f:
            f.write(data)
        return f"{self._prefix}/{filename}"

    async def delete(self, filename: str) -> None:
        path = os.path.join(self._dir, filename)
        try:
            os.remove(path)
        except FileNotFoundError:
            pass

    async def list_files(self, prefix: str = "") -> list[str]:
        if not os.path.isdir(self._dir):
            return []
        base = os.path.join(self._dir, prefix) if prefix else self._dir
        if prefix and os.path.isdir(base):
            return [f for f in os.listdir(base) if os.path.isfile(os.path.join(base, f))]
        return [
            f for f in os.listdir(self._dir)
            if os.path.isfile(os.path.join(self._dir, f)) and f.startswith(prefix)
        ]


# ── AWS S3 / Cloudflare R2（S3 兼容）──────────────────────────────────

class S3Storage(BaseStorage):
    def __init__(
        self,
        bucket: str,
        region: str,
        access_key: str,
        secret_key: str,
        endpoint_url: Optional[str],   # R2 需要，S3 留 None
        public_url_base: Optional[str],  # 自定义域名或 R2 public URL
        use_acl: bool = True,          # Lightsail / R2 不支持对象 ACL，设为 False
    ):
        self._bucket = bucket
        self._region = region
        self._endpoint = endpoint_url
        self._public_base = (public_url_base or "").rstrip("/")
        self._use_acl = use_acl

        # 延迟导入，避免在未安装 boto3 时出错
        try:
            import aioboto3  # type: ignore
            self._session = aioboto3.Session(
                aws_access_key_id=access_key,
                aws_secret_access_key=secret_key,
                region_name=region,
            )
            self._ok = True
        except ImportError:
            self._ok = False

    async def save(self, filename: str, data: bytes, content_type: str = "image/webp") -> str:
        if not self._ok:
            raise RuntimeError("aioboto3 not installed — run: pip install aioboto3")
        client_kwargs: dict = {}
        if self._endpoint:
            client_kwargs["endpoint_url"] = self._endpoint
        put_kwargs: dict = {
            "Bucket": self._bucket,
            "Key": filename,
            "Body": data,
            "ContentType": content_type,
        }
        if self._use_acl:
            put_kwargs["ACL"] = "public-read"
        async with self._session.client("s3", **client_kwargs) as s3:
            await s3.put_object(**put_kwargs)
        if self._public_base:
            return f"{self._public_base}/{filename}"
        return f"https://{self._bucket}.s3.{self._region}.amazonaws.com/{filename}"

    async def delete(self, filename: str) -> None:
        if not self._ok:
            return
        kwargs: dict = {}
        if self._endpoint:
            kwargs["endpoint_url"] = self._endpoint
        async with self._session.client("s3", **kwargs) as s3:
            try:
                await s3.delete_object(Bucket=self._bucket, Key=filename)
            except Exception:
                pass

    async def list_files(self, prefix: str = "") -> list[str]:
        if not self._ok:
            return []
        client_kwargs: dict = {}
        if self._endpoint:
            client_kwargs["endpoint_url"] = self._endpoint
        files: list[str] = []
        async with self._session.client("s3", **client_kwargs) as s3:
            paginator = s3.get_paginator("list_objects_v2")
            async for page in paginator.paginate(Bucket=self._bucket, Prefix=prefix):
                for obj in page.get("Contents", []):
                    files.append(os.path.basename(obj["Key"]))
        return files


# ── 阿里云 OSS ────────────────────────────────────────────────────────

class OSSStorage(BaseStorage):
    def __init__(
        self,
        bucket: str,
        endpoint: str,           # e.g. oss-cn-shanghai.aliyuncs.com
        access_key_id: str,
        access_key_secret: str,
        public_url_base: Optional[str],
    ):
        self._bucket_name = bucket
        self._endpoint = endpoint
        self._public_base = (public_url_base or "").rstrip("/")
        try:
            import oss2  # type: ignore
            auth = oss2.Auth(access_key_id, access_key_secret)
            self._bucket = oss2.Bucket(auth, f"https://{endpoint}", bucket)
            self._ok = True
        except ImportError:
            self._ok = False

    async def save(self, filename: str, data: bytes, content_type: str = "image/webp") -> str:
        if not self._ok:
            raise RuntimeError("oss2 not installed — run: pip install oss2")
        import asyncio
        loop = asyncio.get_event_loop()
        headers = {"Content-Type": content_type}
        await loop.run_in_executor(
            None,
            lambda: self._bucket.put_object(filename, io.BytesIO(data), headers=headers),
        )
        if self._public_base:
            return f"{self._public_base}/{filename}"
        return f"https://{self._bucket_name}.{self._endpoint}/{filename}"

    async def delete(self, filename: str) -> None:
        if not self._ok:
            return
        import asyncio
        loop = asyncio.get_event_loop()
        try:
            await loop.run_in_executor(None, lambda: self._bucket.delete_object(filename))
        except Exception:
            pass

    async def list_files(self, prefix: str = "") -> list[str]:
        if not self._ok:
            return []
        import asyncio
        loop = asyncio.get_event_loop()
        files: list[str] = []
        marker = ""
        while True:
            result = await loop.run_in_executor(
                None,
                lambda m=marker: self._bucket.list_objects(
                    prefix=prefix, marker=m, max_keys=1000
                ),
            )
            for obj in result.object_list:
                files.append(os.path.basename(obj.key))
            if not result.is_truncated:
                break
            marker = result.next_marker
        return files


# ── 工厂函数 ───────────────────────────────────────────────────────────

_HERE = os.path.dirname(os.path.abspath(__file__))
_ROOT = os.path.abspath(os.path.join(_HERE, "../../.."))
_DEFAULT_UPLOAD_DIR = os.path.join(_ROOT, "static", "uploads")


def get_storage(cfg: Optional[dict] = None) -> BaseStorage:
    """构建存储后端。

    cfg 优先于 .env：先从 cfg（DB extra 字段）取值，缺失时回退到 settings。
    upload.py 传入 DB 中的 extra dict，使 Admin 页面修改后无需重启即生效。
    """
    def v(key: str, default: str = "") -> str:
        env_key = key.upper()
        if cfg and key in cfg and cfg[key]:
            return str(cfg[key])
        return str(getattr(settings, env_key, default) or default)

    provider = v("cdn_provider", "local").lower()

    if provider == "s3":
        use_acl_raw = v("cdn_s3_use_acl", "false")
        use_acl = use_acl_raw.lower() not in ("false", "0", "no", "")
        return S3Storage(
            bucket=v("cdn_s3_bucket"),
            region=v("cdn_s3_region", "us-east-1"),
            access_key=v("cdn_s3_access_key"),
            secret_key=v("cdn_s3_secret_key"),
            endpoint_url=v("cdn_s3_endpoint_url") or None,
            public_url_base=v("cdn_public_url") or None,
            use_acl=use_acl,
        )

    if provider == "r2":
        return S3Storage(
            bucket=v("cdn_s3_bucket"),
            region="auto",
            access_key=v("cdn_s3_access_key"),
            secret_key=v("cdn_s3_secret_key"),
            endpoint_url=v("cdn_r2_endpoint") or None,
            public_url_base=v("cdn_public_url") or None,
            use_acl=False,
        )

    if provider == "oss":
        return OSSStorage(
            bucket=v("cdn_oss_bucket"),
            endpoint=v("cdn_oss_endpoint"),
            access_key_id=v("cdn_oss_access_key"),
            access_key_secret=v("cdn_oss_secret_key"),
            public_url_base=v("cdn_public_url") or None,
        )

    return LocalStorage(
        upload_dir=_DEFAULT_UPLOAD_DIR,
        url_prefix="/api/static/uploads",
    )


def get_storage_for_profile(profile) -> BaseStorage:
    """根据 StorageProfile ORM 对象构建存储后端；profile 为 None 时回退本地存储。"""
    if profile is None:
        return LocalStorage(upload_dir=_DEFAULT_UPLOAD_DIR, url_prefix="/api/static/uploads")
    return get_storage(profile.to_storage_cfg())


def check_storage_deps(provider: str) -> dict:
    """检查指定存储后端的可选依赖是否已安装。

    Returns:
        {"ok": bool, "missing": str | None, "install_cmd": str | None}
    """
    p = (provider or "local").lower()
    if p in ("s3", "r2"):
        try:
            import aioboto3  # noqa: F401
            return {"ok": True, "missing": None, "install_cmd": None}
        except ImportError:
            return {"ok": False, "missing": "aioboto3",
                    "install_cmd": "pip install aioboto3"}
    if p == "oss":
        try:
            import oss2  # noqa: F401
            return {"ok": True, "missing": None, "install_cmd": None}
        except ImportError:
            return {"ok": False, "missing": "oss2",
                    "install_cmd": "pip install oss2"}
    return {"ok": True, "missing": None, "install_cmd": None}
