"""Measured tenant storage with safe, atomic writes."""

from __future__ import annotations

import os
import tempfile
from contextlib import contextmanager
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterator

from sqlalchemy import case, or_, select

from config.control_plane import get_control_plane_session
from bridge_platform.tenants.models import Tenant, TenantLimit


STORAGE_ROOT = Path(
    os.getenv("PLATFORM_TENANT_STORAGE_ROOT", "/var/www/html/flask_server/storage/tenants")
)


@dataclass(frozen=True)
class StorageSnapshot:
    tenant_id: str
    storage_key: str
    root: str
    used_bytes: int
    limit_bytes: int | None
    remaining_bytes: int | None
    percentage: float
    status: str

    def to_dict(self) -> dict[str, Any]:
        return {
            "metric": "storage_bytes",
            "tenant_id": self.tenant_id,
            "storage_key": self.storage_key,
            "used": self.used_bytes,
            "used_mb": round(self.used_bytes / 1024 / 1024, 2),
            "limit": self.limit_bytes,
            "limit_mb": None if self.limit_bytes is None else round(self.limit_bytes / 1024 / 1024, 2),
            "remaining": self.remaining_bytes,
            "remaining_mb": None if self.remaining_bytes is None else round(self.remaining_bytes / 1024 / 1024, 2),
            "percentage": self.percentage,
            "status": self.status,
        }


class StorageQuotaExceeded(PermissionError):
    def __init__(self, snapshot: StorageSnapshot, requested_bytes: int) -> None:
        self.snapshot = snapshot
        self.requested_bytes = requested_bytes
        super().__init__(
            f"Storage quota exceeded: {snapshot.used_bytes} bytes used, "
            f"{requested_bytes} bytes requested"
        )


def get_storage_snapshot(tenant_id: str, app_id: str | None = None) -> StorageSnapshot:
    storage_key, limit_mb, policy = _storage_settings(tenant_id, app_id)
    root = _tenant_root(storage_key)
    used = _directory_size(root)
    limit_bytes = None if limit_mb is None else int(limit_mb) * 1024 * 1024
    remaining = None if limit_bytes is None else max(0, limit_bytes - used)
    percentage = 0 if not limit_bytes else min(100.0, round(used / limit_bytes * 100, 2))
    blocked = limit_bytes is not None and used >= limit_bytes and policy == "block"
    return StorageSnapshot(
        tenant_id=tenant_id,
        storage_key=storage_key,
        root=str(root),
        used_bytes=used,
        limit_bytes=limit_bytes,
        remaining_bytes=remaining,
        percentage=percentage,
        status="blocked" if blocked else "available",
    )


def write_bytes(
    *,
    tenant_id: str,
    app_id: str,
    relative_path: str | Path,
    data: bytes,
) -> Path:
    """Quota-check and atomically write one tenant file.

    A filesystem lock prevents competing workers on the same shared volume
    from both accepting the same remaining capacity.
    """
    storage_key, limit_mb, policy = _storage_settings(tenant_id, app_id)
    tenant_root = _tenant_root(storage_key)
    app_root = _safe_child(tenant_root, app_id)
    destination = _safe_child(app_root, relative_path)
    tenant_root.mkdir(parents=True, exist_ok=True)

    with _tenant_lock(tenant_root):
        used = _directory_size(tenant_root)
        existing = destination.stat().st_size if destination.is_file() else 0
        projected = used - existing + len(data)
        limit_bytes = None if limit_mb is None else int(limit_mb) * 1024 * 1024
        if limit_bytes is not None and projected > limit_bytes and policy == "block":
            snapshot = get_storage_snapshot(tenant_id, app_id)
            raise StorageQuotaExceeded(snapshot, len(data))

        destination.parent.mkdir(parents=True, exist_ok=True)
        fd, temp_name = tempfile.mkstemp(prefix=".upload-", dir=destination.parent)
        try:
            with os.fdopen(fd, "wb") as stream:
                stream.write(data)
                stream.flush()
                os.fsync(stream.fileno())
            os.replace(temp_name, destination)
        except Exception:
            try:
                os.unlink(temp_name)
            except FileNotFoundError:
                pass
            raise
    return destination


def read_bytes(*, tenant_id: str, app_id: str, relative_path: str | Path) -> bytes:
    """Read one file from the resolved tenant/app namespace."""
    storage_key, _, _ = _storage_settings(tenant_id, app_id)
    tenant_root = _tenant_root(storage_key)
    app_root = _safe_child(tenant_root, app_id)
    source = _safe_child(app_root, relative_path)
    if not source.is_file():
        raise FileNotFoundError(str(relative_path))
    return source.read_bytes()


def _storage_settings(tenant_id: str, app_id: str | None) -> tuple[str, int | None, str]:
    with get_control_plane_session() as session:
        tenant = session.get(Tenant, tenant_id)
        if tenant is None:
            raise LookupError(f"Tenant not found: {tenant_id}")
        scope = (
            or_(TenantLimit.app_id == app_id, TenantLimit.app_id.is_(None))
            if app_id else TenantLimit.app_id.is_(None)
        )
        limit = session.execute(
            select(TenantLimit)
            .where(
                TenantLimit.tenant_id == tenant_id,
                TenantLimit.limit_name == "storage_mb",
                scope,
            )
            .order_by(case((TenantLimit.app_id == app_id, 0), else_=1))
            .limit(1)
        ).scalar_one_or_none()
        return (
            tenant.storage_key or tenant.slug or tenant.tenant_id,
            limit.limit_value if limit else None,
            (limit.overage_policy or "block") if limit else "unlimited",
        )


def _tenant_root(storage_key: str) -> Path:
    return _safe_child(STORAGE_ROOT, storage_key)


def _safe_child(root: Path, child: str | Path) -> Path:
    resolved_root = root.resolve()
    candidate = (resolved_root / child).resolve()
    if candidate != resolved_root and resolved_root not in candidate.parents:
        raise ValueError("Storage path escapes the tenant root")
    return candidate


def _directory_size(root: Path) -> int:
    if not root.exists():
        return 0
    total = 0
    for path in root.rglob("*"):
        try:
            if path.is_file() and path.name != ".quota.lock":
                total += path.stat().st_size
        except FileNotFoundError:
            continue
    return total


@contextmanager
def _tenant_lock(root: Path) -> Iterator[None]:
    import fcntl

    lock_path = root / ".quota.lock"
    with lock_path.open("a+b") as lock:
        fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
        try:
            yield
        finally:
            fcntl.flock(lock.fileno(), fcntl.LOCK_UN)
