"""Validation and storage for files the client hands us.

Sources of truth: this module; app/routers/todos.py is the only caller.

Uploaded files are untrusted input, so the rules here are the paranoid ones the server's
interaction standard already sets for attachments: the type is decided by SNIFFING the
bytes, never by the filename or the declared content type; there is a hard size cap; the
stored name is random and the original name is never used as a path; JPEG and PNG are
re-encoded so metadata (EXIF, including GPS) is dropped; files live under the workspace
data/ directory, which is mounted read-write at /srv/uploads and is not a static mount;
they are served only through a route that pins Content-Type and sets nosniff.

What is accepted, and why only this: images (jpeg, png, webp, gif) for product and
airport photography; documents (pdf, docx) and text (md, txt) for articles. Nothing
executable, nothing that a browser would run.
"""

import io
import re
import secrets
import zipfile
from dataclasses import dataclass
from pathlib import Path

MAX_BYTES = 25 * 1024 * 1024  # 25 MB; a print-quality photo or a long article with images

UPLOAD_ROOT = Path("/srv/uploads")


@dataclass(frozen=True)
class Sniffed:
    kind: str          # image | document | text
    content_type: str
    ext: str


def sniff(data: bytes, original_name: str) -> Sniffed | None:
    """Decide what a file IS from its bytes. Returns None for anything not accepted."""
    head = data[:16]
    if head.startswith(b"\xff\xd8\xff"):
        return Sniffed("image", "image/jpeg", "jpg")
    if head.startswith(b"\x89PNG\r\n\x1a\n"):
        return Sniffed("image", "image/png", "png")
    if head[:4] == b"RIFF" and data[8:12] == b"WEBP":
        return Sniffed("image", "image/webp", "webp")
    if head.startswith((b"GIF87a", b"GIF89a")):
        return Sniffed("image", "image/gif", "gif")
    if head.startswith(b"%PDF-"):
        return Sniffed("document", "application/pdf", "pdf")
    if head.startswith(b"PK\x03\x04"):
        # A docx is a zip with a specific member; any other zip is refused.
        try:
            with zipfile.ZipFile(io.BytesIO(data)) as zf:
                names = set(zf.namelist())
        except zipfile.BadZipFile:
            return None
        if "[Content_Types].xml" in names and any(n.startswith("word/") for n in names):
            return Sniffed("document", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "docx")
        return None
    # Text: only if it decodes as UTF-8 and carries no control bytes other than whitespace.
    if _looks_like_text(data):
        ext = "md" if original_name.lower().endswith(".md") else "txt"
        return Sniffed("text", "text/markdown" if ext == "md" else "text/plain", ext)
    return None


def _looks_like_text(data: bytes) -> bool:
    if not data:
        return False
    try:
        s = data.decode("utf-8")
    except UnicodeDecodeError:
        return False
    return not any(ord(c) < 32 and c not in "\r\n\t" for c in s[:4096])


def strip_image_metadata(data: bytes, content_type: str) -> bytes:
    """Re-encode JPEG and PNG without metadata. Other image types pass through."""
    if content_type not in ("image/jpeg", "image/png"):
        return data
    try:
        from PIL import Image  # Pillow is a dependency for exactly this
    except ImportError:  # pragma: no cover - the image is stored as-is if Pillow is missing
        return data
    with Image.open(io.BytesIO(data)) as im:
        out = io.BytesIO()
        if content_type == "image/jpeg":
            im = im.convert("RGB")
            im.save(out, format="JPEG", quality=92, optimize=True)
        else:
            im.save(out, format="PNG", optimize=True)
        return out.getvalue()


_SAFE_NAME = re.compile(r"[^A-Za-z0-9._ -]+")


def safe_original_name(name: str) -> str:
    """The name we show back. Never a path, never longer than the column."""
    base = Path(name or "upload").name
    base = _SAFE_NAME.sub("_", base).strip(" .") or "upload"
    return base[:255]


def stored_name_for(ext: str) -> str:
    return f"{secrets.token_urlsafe(24)}.{ext}"


def store(todo_id: int, stored_name: str, data: bytes, root: Path = UPLOAD_ROOT) -> Path:
    """Write under root/<todo_id>/<stored_name>. The directory is created 0o750."""
    folder = root / str(int(todo_id))
    folder.mkdir(parents=True, exist_ok=True, mode=0o750)
    path = folder / stored_name
    # Never follow a pre-existing symlink, never overwrite.
    with open(path, "xb") as fh:
        fh.write(data)
    return path
