from __future__ import annotations

import hashlib
import json
from pathlib import Path
from typing import Any, Dict, List, Optional

from mockmesh.core.logger import get_logger
from mockmesh.storage.base import BaseStorage
from mockmesh.storage.utils import route_subdir, utc_now

_log = get_logger("storage.blob")
_META_SUFFIX = ".mockmesh-meta.json"


def _etag(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()[:32]


_BLOB_ROUTES: list = [
    ("s3-", "aws"),
    ("gcs-", "gcp"),
    ("az-", "azure"),
]


class BlobStorage(BaseStorage):

    backend_type = "blob"

    def __init__(self, root: Path) -> None:
        super().__init__(root)
        self._blob_root = root / "blob"

    # ------------------------------------------------------------------
    # Lifecycle
    # ------------------------------------------------------------------

    def initialize(self) -> None:
        self._blob_root.mkdir(parents=True, exist_ok=True)
        self._ready = True
        _log.info("BlobStorage ready", extra={"mm_dir": str(self._blob_root)})

    # ------------------------------------------------------------------
    # Path helpers
    # ------------------------------------------------------------------

    @staticmethod
    def _route_subdir(bucket: str) -> str:
        return route_subdir(bucket, _BLOB_ROUTES)

    def _object_path(self, bucket: str, key: str) -> Path:
        safe_key = key.lstrip("/")
        subdir = self._route_subdir(bucket)
        if subdir:
            target = self._blob_root / subdir
            target.mkdir(parents=True, exist_ok=True)
            return target / bucket / safe_key
        return self._blob_root / bucket / safe_key

    def _meta_path(self, obj_path: Path) -> Path:
        return obj_path.parent / (obj_path.name + _META_SUFFIX)

    # ------------------------------------------------------------------
    # Core blob operations
    # ------------------------------------------------------------------

    def put_object(
        self,
        bucket: str,
        key: str,
        data: bytes,
        content_type: str = "application/octet-stream",
        metadata: Optional[Dict[str, str]] = None,
    ) -> Dict[str, Any]:
        obj_path = self._object_path(bucket, key)
        obj_path.parent.mkdir(parents=True, exist_ok=True)
        obj_path.write_bytes(data)

        tag = _etag(data)
        meta: Dict[str, Any] = {
            "bucket":       bucket,
            "key":          key,
            "size":         len(data),
            "content_type": content_type,
            "etag":         tag,
            "last_modified": utc_now(),
            "metadata":     metadata or {},
        }
        self._meta_path(obj_path).write_text(json.dumps(meta, indent=2), encoding="utf-8")
        _log.info("Blob put_object", extra={"mm_bucket": bucket, "mm_key": key, "mm_size": len(data)})
        return {"ETag": f'"{tag}"', "size": len(data), "key": key, "bucket": bucket}

    def get_object(self, bucket: str, key: str) -> Optional[Dict[str, Any]]:
        obj_path = self._object_path(bucket, key)
        if not obj_path.exists() or obj_path.name.endswith(_META_SUFFIX):
            return None
        data = obj_path.read_bytes()
        meta = self._read_meta(obj_path)
        _log.info("Blob get_object", extra={"mm_bucket": bucket, "mm_key": key})
        return {
            "Body":          data,
            "ContentType":   meta.get("content_type", "application/octet-stream"),
            "ContentLength": len(data),
            "ETag":          f"\"{meta.get('etag', _etag(data))}\"",
            "LastModified":  meta.get("last_modified", utc_now()),
            "Metadata":      meta.get("metadata", {}),
        }

    def delete_object(self, bucket: str, key: str) -> bool:
        obj_path = self._object_path(bucket, key)
        if not obj_path.exists():
            return False
        obj_path.unlink()
        meta_path = self._meta_path(obj_path)
        if meta_path.exists():
            meta_path.unlink()
        _log.info("Blob delete_object", extra={"mm_bucket": bucket, "mm_key": key})
        return True

    def list_objects(self, bucket: str, prefix: str = "") -> List[Dict[str, Any]]:
        subdir = self._route_subdir(bucket)
        bucket_dir = (self._blob_root / subdir / bucket) if subdir else (self._blob_root / bucket)
        if not bucket_dir.exists():
            return []
        objects = []
        for path in bucket_dir.rglob("*"):
            if path.is_dir() or path.name.endswith(_META_SUFFIX):
                continue
            rel_key = str(path.relative_to(bucket_dir))
            if prefix and not rel_key.startswith(prefix):
                continue
            meta = self._read_meta(path)
            objects.append({
                "Key":          rel_key,
                "Size":         path.stat().st_size,
                "ETag":         f"\"{meta.get('etag', '')}\"",
                "LastModified": meta.get("last_modified", ""),
            })
        return objects

    # ------------------------------------------------------------------
    # Bucket management
    # ------------------------------------------------------------------

    def list_buckets(self) -> List[str]:
        if not self._blob_root.exists():
            return []
        buckets = []
        provider_dirs = {subdir for _, subdir in _BLOB_ROUTES}
        for d in self._blob_root.iterdir():
            if not d.is_dir():
                continue
            if d.name in provider_dirs:
                buckets.extend(sd.name for sd in d.iterdir() if sd.is_dir())
            else:
                buckets.append(d.name)
        return buckets

    # ------------------------------------------------------------------
    # BaseStorage CRUD bridge
    # ------------------------------------------------------------------

    def put(self, collection: str, key: str, value: Dict[str, Any]) -> None:
        data = json.dumps(value).encode("utf-8")
        self.put_object(collection, key, data, content_type="application/json")

    def get(self, collection: str, key: str) -> Optional[Dict[str, Any]]:
        result = self.get_object(collection, key)
        if result is None:
            return None
        try:
            return json.loads(result["Body"])
        except (json.JSONDecodeError, KeyError):
            return {"raw": result["Body"].decode("latin-1")}

    def delete(self, collection: str, key: str) -> bool:
        return self.delete_object(collection, key)

    def list(self, collection: str, prefix: str = "") -> List[Dict[str, Any]]:
        return [
            {"key": o["Key"], "value": o}
            for o in self.list_objects(collection, prefix=prefix)
        ]

    def query(self, collection: str, **filters: Any) -> List[Dict[str, Any]]:
        all_objs = self.list(collection)
        if not filters:
            return all_objs
        return [
            o for o in all_objs
            if all(o["value"].get(k) == v for k, v in filters.items())
        ]

    # ------------------------------------------------------------------
    # Private
    # ------------------------------------------------------------------

    def _read_meta(self, obj_path: Path) -> Dict[str, Any]:
        meta_path = self._meta_path(obj_path)
        if not meta_path.exists():
            return {}
        try:
            return json.loads(meta_path.read_text(encoding="utf-8"))
        except (json.JSONDecodeError, OSError):
            return {}
