"""StorageProvider Protocol + LocalFileSystem implementation.

Future providers (MinIO, S3) implement the same Protocol — call sites use
`get_default_provider()` and never import a concrete class directly.
"""

from __future__ import annotations

import hashlib
import os
import shutil
from pathlib import Path
from typing import BinaryIO, Protocol

from django.conf import settings


class StorageError(Exception):
    """Raised for any provider operation failure."""


class StorageProvider(Protocol):
    name: str

    def save(self, path: str, content: BinaryIO) -> str: ...

    def open(self, path: str) -> BinaryIO: ...

    def delete(self, path: str) -> None: ...

    def url(self, path: str) -> str: ...

    def exists(self, path: str) -> bool: ...

    def sha256(self, path: str) -> str: ...


class LocalFileSystemProvider:
    """cPanel-friendly provider rooted at `settings.STORAGE_ROOT`."""

    name = "local"

    def __init__(self, root: Path | None = None) -> None:
        self.root = Path(root or getattr(settings, "STORAGE_ROOT", settings.MEDIA_ROOT))
        self.root.mkdir(parents=True, exist_ok=True)

    def _abs(self, path: str) -> Path:
        # Block traversal — final path must remain inside root.
        candidate = (self.root / path).resolve()
        try:
            candidate.relative_to(self.root.resolve())
        except ValueError as exc:
            raise StorageError(f"path escapes storage root: {path!r}") from exc
        return candidate

    def save(self, path: str, content: BinaryIO) -> str:
        target = self._abs(path)
        target.parent.mkdir(parents=True, exist_ok=True)
        with target.open("wb") as fh:
            shutil.copyfileobj(content, fh)
        return path

    def open(self, path: str) -> BinaryIO:
        target = self._abs(path)
        if not target.exists():
            raise StorageError(f"not found: {path!r}")
        return target.open("rb")

    def delete(self, path: str) -> None:
        target = self._abs(path)
        if target.exists():
            target.unlink()

    def url(self, path: str) -> str:
        base = getattr(settings, "MEDIA_URL", "/media/")
        return f"{base.rstrip('/')}/{path.lstrip('/')}"

    def exists(self, path: str) -> bool:
        return self._abs(path).exists()

    def sha256(self, path: str) -> str:
        h = hashlib.sha256()
        with self.open(path) as fh:
            for chunk in iter(lambda: fh.read(64 * 1024), b""):
                h.update(chunk)
        return h.hexdigest()


class MinIOProvider:
    """Placeholder — Phase 8 wires this to the `minio` SDK."""

    name = "minio"

    def __init__(self, *_a, **_kw) -> None:
        raise StorageError("MinIO provider is not implemented yet")


_DEFAULT: StorageProvider | None = None


def get_default_provider() -> StorageProvider:
    global _DEFAULT
    if _DEFAULT is None:
        _DEFAULT = LocalFileSystemProvider()
    return _DEFAULT


def reset_default_provider() -> None:
    """Test helper — drop the cached provider so a new MEDIA_ROOT is picked up."""
    global _DEFAULT
    _DEFAULT = None


def safe_tenant_path(tenant_id: int, filename: str, *, sub_key: str | None = None) -> str:
    """Compose a deterministic, traversal-safe, collision-free per-tenant storage path.

    Format: ``tenants/{tenant_id}/{sub_key}/{filename}``

    ``sub_key`` defaults to a fresh UUID4 hex string so that two uploads of
    the same filename from the same tenant never share a path.  Pass an
    explicit ``sub_key`` (e.g. the UploadSession public_id) when you need a
    stable, pre-computed path (chunk reassembly, resumable upload).

    The filename is sanitised: directory separators and leading dots are
    stripped so path traversal and hidden-file tricks are not possible.
    """
    import uuid as _uuid

    clean = os.path.basename(filename).strip().replace("\\", "_")
    if not clean or clean.startswith("."):
        raise StorageError(f"invalid filename: {filename!r}")
    key = sub_key if sub_key is not None else _uuid.uuid4().hex
    return f"tenants/{tenant_id}/{key}/{clean}"
