//! Streams blobs from the `Storage` trait. Same endpoint serves manga
//! covers and chapter pages; the key embedded in the URL is whatever the
//! writer stored.
//!
//! The handler uses `Storage::get_stream` so a multi-MB page is piped to
//! the client a chunk at a time instead of buffered server-side.
//!
//! **Auth model — capability URLs by design.** This endpoint is
//! deliberately unauthenticated: reads stay public per the project
//! brief, and per-page authorisation would require either a per-request
//! ownership lookup (covers + pages are scoped by manga, not user) or a
//! signed-URL scheme. Mangalord instead relies on the keys being
//! unguessable — `mangas/{uuid}/...` and
//! `mangas/{uuid}/chapters/{uuid}/...` — so a leaked URL leaks at most
//! the one referenced file. A future feat/private-libraries branch
//! would gate this endpoint behind a `Storage::owner_of(key)` check;
//! the seam is intentional.

use axum::body::Body;
use axum::extract::{Path, State};
use axum::http::{header, HeaderName};
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use axum::Router;

use crate::app::AppState;
use crate::error::AppResult;
use crate::storage::StorageError;

pub fn routes() -> Router<AppState> {
    Router::new().route("/files/*key", get(serve))
}

async fn serve(State(state): State<AppState>, Path(key): Path<String>) -> AppResult<Response> {
    let file = match state.storage.get_stream(&key).await {
        Ok(f) => f,
        Err(StorageError::NotFound) => return Err(crate::error::AppError::NotFound),
        Err(e) => return Err(e.into()),
    };
    let ct = content_type_for(&key);
    // `nosniff` makes the contract explicit: the browser must trust the
    // Content-Type we declared (and that the magic-byte sniff at upload
    // time produced) instead of trying to detect HTML/JS in the body.
    // Belt-and-braces vs. polyglot files that survive the upload sniff.
    let headers = [
        (header::CONTENT_TYPE, ct.to_string()),
        (header::CONTENT_LENGTH, file.size_bytes.to_string()),
        (
            HeaderName::from_static("x-content-type-options"),
            "nosniff".to_string(),
        ),
        // Blobs are content-addressed by unguessable, immutable keys (a
        // re-upload mints new UUIDs), so a fetched page/cover never changes.
        // Cache it for a year and mark it `immutable` so browsers skip
        // revalidation entirely — this is what lets the reader's page and
        // next-chapter preloading hit cache instead of re-downloading (and
        // re-proxying every byte through the SvelteKit node server in prod).
        //
        // BUT under PRIVATE_MODE these blobs are auth-gated (see
        // `private_mode_guard`), so they must NOT be marked `public`: a shared
        // cache / CDN in front of the app would store the object and then serve
        // it to unauthenticated clients, defeating the gate. Use `private` so
        // only the requesting user's browser caches it.
        (
            header::CACHE_CONTROL,
            if state.auth.private_mode {
                "private, max-age=31536000, immutable".to_string()
            } else {
                "public, max-age=31536000, immutable".to_string()
            },
        ),
    ];
    Ok((headers, Body::from_stream(file.stream)).into_response())
}

fn content_type_for(key: &str) -> &'static str {
    let ext = key.rsplit('.').next().unwrap_or("").to_ascii_lowercase();
    match ext.as_str() {
        "jpg" | "jpeg" => "image/jpeg",
        "png" => "image/png",
        "webp" => "image/webp",
        "gif" => "image/gif",
        "avif" => "image/avif",
        _ => "application/octet-stream",
    }
}
