from __future__ import annotations

import re
import uuid
from functools import lru_cache
from urllib.parse import quote, urlsplit

import boto3
from botocore.exceptions import ClientError

from app.core.config import settings


def _safe_file_name(file_name: str) -> str:
    raw = str(file_name or "").strip() or "file.bin"
    return re.sub(r"[^A-Za-z0-9._-]+", "_", raw)


def build_object_key(prefix: str, file_name: str) -> str:
    safe_name = _safe_file_name(file_name)
    return f"{prefix.strip('/')}/{uuid.uuid4().hex}-{safe_name}"


class S3Storage:
    def __init__(self):
        self.bucket = settings.S3_BUCKET
        verify_ssl: bool | str = bool(settings.S3_VERIFY_SSL)
        ca_bundle = str(settings.S3_CA_CERT_PATH or "").strip()
        if verify_ssl and ca_bundle:
            verify_ssl = ca_bundle
        self.client = boto3.client(
            "s3",
            endpoint_url=settings.S3_ENDPOINT,
            aws_access_key_id=settings.S3_ACCESS_KEY,
            aws_secret_access_key=settings.S3_SECRET_KEY,
            region_name=settings.S3_REGION,
            use_ssl=settings.S3_USE_SSL,
            verify=verify_ssl,
        )
        self._bucket_checked = False

    def ensure_bucket(self) -> None:
        if self._bucket_checked:
            return
        try:
            self.client.head_bucket(Bucket=self.bucket)
        except ClientError as exc:
            code = str(exc.response.get("Error", {}).get("Code", ""))
            # In production setups credentials may be scoped to object operations only.
            # If bucket-level HeadBucket is forbidden, continue and let object-level calls decide.
            if code in {"403", "AccessDenied", "Forbidden"}:
                self._bucket_checked = True
                return
            if code not in {"404", "NoSuchBucket", "NotFound"}:
                raise
            kwargs: dict = {"Bucket": self.bucket}
            if settings.S3_REGION and settings.S3_REGION != "us-east-1":
                kwargs["CreateBucketConfiguration"] = {"LocationConstraint": settings.S3_REGION}
            try:
                self.client.create_bucket(**kwargs)
            except ClientError as create_exc:
                create_code = str(create_exc.response.get("Error", {}).get("Code", ""))
                if create_code not in {"BucketAlreadyOwnedByYou", "BucketAlreadyExists"}:
                    raise
        self._bucket_checked = True

    @staticmethod
    def _proxy_presigned_url(raw_url: str) -> str:
        # Route pre-signed requests through frontend `/s3/*` proxy to avoid browser cross-origin issues.
        parts = urlsplit(str(raw_url or ""))
        if not parts.path:
            return raw_url
        query = ("?" + parts.query) if parts.query else ""
        return "/s3" + parts.path + query

    def create_presigned_put_url(self, key: str, mime_type: str, expires_sec: int = 900) -> str:
        self.ensure_bucket()
        url = self.client.generate_presigned_url(
            "put_object",
            Params={"Bucket": self.bucket, "Key": key, "ContentType": mime_type},
            ExpiresIn=expires_sec,
            HttpMethod="PUT",
        )
        return self._proxy_presigned_url(url)

    def head_object(self, key: str) -> dict:
        self.ensure_bucket()
        return self.client.head_object(Bucket=self.bucket, Key=key)

    def get_object(self, key: str) -> dict:
        self.ensure_bucket()
        return self.client.get_object(Bucket=self.bucket, Key=key)

    def get_avatar_proxy_path(self, key: str, token: str) -> str:
        return "/api/admin/uploads/object/" + quote(key, safe="") + "?token=" + quote(token, safe="")


@lru_cache(maxsize=1)
def get_s3_storage() -> S3Storage:
    return S3Storage()
