"""Attachments in comments: a picture or a PDF handed over mid-conversation.

Sources of truth: this module, `app/services/uploads.py` (the sniff-and-strip seam; the one
place a file's type is decided), `app/services/discussion.py` (`valid_subject`,
`permission_of`), `docs/CLIENT-SURFACES.md`, `tests/test_discussion_attachments.py`.

The Interaction Standard's addendum, followed to the letter because this is a client-facing
upload surface: the type is decided by the BYTES (images and PDF only for a comment; nothing
script-capable, so no SVG and no HTML), the stored name is ours (random, extension from the
sniff), JPEG and PNG are re-encoded so metadata goes, a hard size cap and a per-subject count
cap, the served content type comes from our table and never from the upload, `nosniff` on every
answer, and visibility is re-checked through the OWNING SUBJECT on every read (a running-list
subject needs `plan.view`; an unknown or unseen subject is 404, never 403). Storage is DB-free:
`uploads/attachments/<type>/<id>/<name>` under the mounted workspace `uploads/` directory,
never the instance and never a static mount; the comment body carries the markdown the composer
appends on Send, and the pack's renderer loads images only from this prefix.
"""

from __future__ import annotations

import re
from pathlib import Path

from fastapi import APIRouter, File, HTTPException, Request, UploadFile
from fastapi.responses import FileResponse

from app.services import accounts, identity
from app.services import discussion as disc
from app.services import uploads as up

router = APIRouter(prefix="/api/attachments", tags=["attachments"])

MAX_BYTES = 10 * 1024 * 1024
MAX_FILES_PER_SUBJECT = 50
ROOT = up.UPLOAD_ROOT / "attachments"
#: The stored-name shape is the traversal guard: nothing outside it resolves to a file.
NAME = re.compile(r"^[A-Za-z0-9_-]{20,64}\.(png|jpg|webp|gif|pdf)$")
CONTENT_TYPES = {"png": "image/png", "jpg": "image/jpeg", "webp": "image/webp", "gif": "image/gif",
                 "pdf": "application/pdf"}


def _subject(request: Request, subject_type: str, subject_id: str) -> None:
    """A subject the caller may see, or 404: a place you cannot see does not exist."""
    if not disc.valid_subject(subject_type, subject_id):
        raise HTTPException(status_code=404, detail={"error_code": "NOT_FOUND", "summary": "No such place."})
    if not accounts.can(identity.optional_user(request), disc.permission_of(subject_type)):
        raise HTTPException(status_code=404, detail={"error_code": "NOT_FOUND", "summary": "No such place."})


def folder_for(subject_type: str, subject_id: str, root: Path | None = None) -> Path:
    return (root or ROOT) / subject_type / subject_id


@router.post("/{subject_type}/{subject_id}", status_code=201)
async def upload(subject_type: str, subject_id: str, request: Request, file: UploadFile = File(...)) -> dict:
    """One file for this subject's conversation. The answer's `markdown` is what the composer
    appends on Send, so a comment carries its file."""
    _subject(request, subject_type, subject_id)
    data = await file.read(MAX_BYTES + 1)
    if not data:
        raise HTTPException(status_code=422, detail={"error_code": "EMPTY", "summary": "The file arrived empty."})
    if len(data) > MAX_BYTES:
        raise HTTPException(status_code=413, detail={"error_code": "TOO_LARGE",
                                                    "summary": "That file is over 10 MB. A link to a shared drive works for big files."})
    sniffed = up.sniff(data, file.filename or "")
    if sniffed is None or sniffed.ext not in CONTENT_TYPES:
        raise HTTPException(status_code=415, detail={"error_code": "UNSUPPORTED_TYPE",
                                                    "summary": "Images (JPEG, PNG, WebP, GIF) and PDF work here."})
    if sniffed.kind == "image":
        data = up.strip_image_metadata(data, sniffed.content_type)
    folder = folder_for(subject_type, subject_id)
    folder.mkdir(parents=True, exist_ok=True, mode=0o750)
    if sum(1 for _ in folder.iterdir()) >= MAX_FILES_PER_SUBJECT:
        raise HTTPException(status_code=409, detail={"error_code": "SUBJECT_FULL",
                                                    "summary": "This conversation already holds a lot of files."})
    name = up.stored_name_for(sniffed.ext)
    with open(folder / name, "xb") as fh:  # never follows a pre-existing symlink, never overwrites
        fh.write(data)
    url = f"/api/attachments/{subject_type}/{subject_id}/{name}"
    display = up.safe_original_name(file.filename or "")[:60]
    is_image = sniffed.ext != "pdf"
    return {"ok": True, "url": url, "name": name, "display": display, "is_image": is_image,
            "markdown": f"![{display}]({url})" if is_image else f"[{display}]({url})"}


@router.get("/{subject_type}/{subject_id}/{name}")
def serve(subject_type: str, subject_id: str, name: str, request: Request) -> FileResponse:
    _subject(request, subject_type, subject_id)
    if not NAME.match(name):
        raise HTTPException(status_code=404, detail={"error_code": "NOT_FOUND", "summary": "No such attachment."})
    path = folder_for(subject_type, subject_id) / name
    if not path.is_file():
        raise HTTPException(status_code=404, detail={"error_code": "NOT_FOUND", "summary": "No such attachment."})
    return FileResponse(path, media_type=CONTENT_TYPES[name.rsplit(".", 1)[-1]], headers={
        "X-Content-Type-Options": "nosniff",
        "Content-Disposition": f'inline; filename="{name}"',
        "Cache-Control": "private, max-age=3600",
    })
