"""Files on disk under the data root (locked decision D13).

Postgres stores metadata plus a path RELATIVE to the data root; nothing outside
the root is ever readable or writable through this module. Every file is served
through an authed endpoint with a membership check — never via /assets or any
public path.

The root is a bind mount (./data:/app/data) so runtime state lives at the
workspace root, not in the instance (v8).
"""

import os
import re
import uuid
from pathlib import Path

from app.constants import ALLOWED_UPLOAD_EXTENSIONS, MAX_UPLOAD_BYTES


class StorageError(Exception):
    """A file the app refuses to store or serve. `code` maps to an error_code."""

    def __init__(self, code: str, summary: str, details: str = "") -> None:
        super().__init__(summary)
        self.code = code
        self.summary = summary
        self.details = details


def data_root() -> Path:
    """Resolved lazily (not at import) so the OpenAPI dump build stage, which has
    no data mount, can import the app without touching the filesystem."""
    return Path(os.environ.get("SCOUT_DATA_DIR", "/app/data")).resolve()


def resolve(relative_path: str) -> Path:
    """Turn a DB-stored relative path into an absolute one, refusing traversal.

    The DB is trusted more than user input, but defense in depth is free: a
    corrupted row must not become a read primitive.
    """
    root = data_root()
    candidate = (root / relative_path).resolve()
    if root not in candidate.parents:
        raise StorageError("PATH_OUTSIDE_DATA_ROOT", "Refusing a path outside the data root.")
    return candidate


def extension_of(filename: str) -> str:
    return Path(filename).suffix.lstrip(".").lower()


def validate_upload(filename: str, size: int) -> str:
    """Check name + size against the D13 rules; return the (lowercase) extension."""
    ext = extension_of(filename)
    if ext not in ALLOWED_UPLOAD_EXTENSIONS:
        allowed = ", ".join(sorted(ALLOWED_UPLOAD_EXTENSIONS))
        raise StorageError(
            "UPLOAD_TYPE_NOT_ALLOWED",
            f"'.{ext or '?'}' files are not accepted.",
            f"Accepted types: {allowed}.",
        )
    if size > MAX_UPLOAD_BYTES:
        raise StorageError(
            "UPLOAD_TOO_LARGE",
            f"The file is larger than the {MAX_UPLOAD_BYTES // (1024 * 1024)} MB limit.",
        )
    return ext


def store_bytes(relative_dir: str, filename_hint: str, content: bytes) -> str:
    """Write content under a UUID name inside relative_dir; return the relative
    path to store in the DB. The original filename is metadata only — it never
    becomes part of the path."""
    ext = extension_of(filename_hint)
    name = f"{uuid.uuid4().hex}.{ext}" if ext else uuid.uuid4().hex
    relative_path = f"{relative_dir}/{name}"
    absolute = resolve(relative_path)
    absolute.parent.mkdir(parents=True, exist_ok=True)
    absolute.write_bytes(content)
    return relative_path


_SCREENSHOT_SLUG = re.compile(r"^[a-z0-9][a-z0-9-]{0,78}$")


def store_screenshot(project_id: int, slug: str, kind: str, content: bytes) -> str:
    """Screenshots keep a deterministic name (<slug>-<kind>.png) so a re-import
    overwrites in place rather than leaking orphan files."""
    if not _SCREENSHOT_SLUG.match(slug):
        raise StorageError("BAD_SLUG", f"'{slug}' is not a valid option slug.")
    if kind not in ("desktop", "mobile"):
        raise StorageError("BAD_SCREENSHOT_KIND", f"Unknown screenshot kind '{kind}'.")
    relative_path = f"projects/{project_id}/screenshots/{slug}-{kind}.png"
    absolute = resolve(relative_path)
    absolute.parent.mkdir(parents=True, exist_ok=True)
    absolute.write_bytes(content)
    return relative_path


def delete_file(relative_path: str) -> None:
    """Best-effort removal; a missing file is not an error worth failing over."""
    if not relative_path:
        return
    try:
        resolve(relative_path).unlink(missing_ok=True)
    except StorageError:
        pass
