pub mod albums;
pub mod client;
pub mod images;
pub mod photos;
pub mod shares;
pub mod xmp;
pub mod zip;

use axum::extract::{DefaultBodyLimit, Request, State};
use axum::middleware::{self, Next};
use axum::response::{IntoResponse, Response};
use axum::routing::{any, delete, get, post, put};
use axum::{Json, Router};
use axum_extra::extract::cookie::SignedCookieJar;

use crate::auth;
use crate::error::ApiError;
use crate::state::AppState;

const MAX_UPLOAD_BYTES: usize = 4 * 1024 * 1024 * 1024;

async fn health() -> Json<serde_json::Value> {
    Json(serde_json::json!({ "ok": true }))
}

/// Unknown /api paths must 404 as JSON, not fall through to the SPA's index.html.
async fn api_not_found() -> ApiError {
    ApiError::not_found()
}

/// Wrong method on a known route gets a JSON 405 (not an empty body).
async fn method_not_allowed() -> ApiError {
    ApiError(
        axum::http::StatusCode::METHOD_NOT_ALLOWED,
        "method not allowed".into(),
    )
}

/// Every route in the admin router passes through this layer, so photographer
/// auth is structural — a new admin endpoint cannot be forgotten open. The
/// authenticated user is stored in request extensions for handlers that need
/// the identity.
async fn require_photographer(
    State(state): State<AppState>,
    mut request: Request,
    next: Next,
) -> Result<Response, ApiError> {
    let jar = SignedCookieJar::from_headers(request.headers(), state.cookie_key.clone());
    let user = auth::user_from_jar(&state, &jar).ok_or_else(ApiError::unauthorized)?;
    request.extensions_mut().insert(user);
    // Slide the session: past half-life, responses carry a fresh cookie.
    let refreshed = auth::refreshed_session(&state, &jar);
    let response = next.run(request).await;
    Ok(match refreshed {
        Some(cookie) => (jar.add(cookie), response).into_response(),
        None => response,
    })
}

pub fn router(state: &AppState) -> Router<AppState> {
    // Photographer-only surface. Add new admin endpoints HERE — the auth
    // layer covers them automatically.
    let admin = Router::new()
        .route("/api/me", get(auth::me))
        .route("/api/albums", get(albums::list).post(albums::create))
        .route(
            "/api/albums/{id}",
            get(albums::get_one)
                .patch(albums::update)
                .delete(albums::delete),
        )
        .route(
            "/api/albums/{id}/photos",
            post(photos::upload).layer(DefaultBodyLimit::max(MAX_UPLOAD_BYTES)),
        )
        .route(
            "/api/albums/{id}/shares",
            get(shares::list).post(shares::create),
        )
        .route("/api/albums/{id}/zip", post(zip::album_zip))
        .route("/api/albums/{id}/xmp", get(xmp::album_xmp))
        .route("/api/albums/{id}/pending", get(albums::pending))
        .route("/api/albums/{id}/photos/by-hash/{sha256}", get(photos::by_hash))
        .route("/api/photos/delete", post(photos::delete_many))
        .route("/api/photos/{id}", delete(photos::delete))
        .route("/api/photos/{id}/rating", put(photos::set_owner_rating))
        .route("/api/photos/{id}/verdict", put(photos::set_owner_verdict))
        .route("/api/photos/{id}/reprocess", post(photos::reprocess))
        .route(
            "/api/shares/{id}",
            delete(shares::delete).patch(shares::update),
        )
        .route("/api/shares/{id}/reset-lock", post(shares::reset_lock))
        .route_layer(middleware::from_fn_with_state(
            state.clone(),
            require_photographer,
        ));

    // Public surface: health, the auth flow itself, and client-share
    // endpoints (self-authorizing via token or share-access cookie).
    Router::new()
        .route("/api/health", get(health))
        .route("/api/auth/login", get(auth::login))
        .route("/api/auth/callback", get(auth::callback))
        .route("/api/auth/logout", post(auth::logout))
        .route("/api/share/{token}", get(client::get_share))
        .route("/api/share/{token}/unlock", post(client::unlock))
        .route(
            "/api/share/{token}/photos/{photo_id}/rating",
            put(client::set_rating),
        )
        .route(
            "/api/share/{token}/photos/{photo_id}/verdict",
            put(client::set_verdict),
        )
        .route(
            "/api/share/{token}/photos/{photo_id}/tags",
            put(client::set_tags),
        )
        .route("/api/share/{token}/zip", post(zip::share_zip))
        // Dual-auth (photographer session OR share cookie), checked in-handler:
        .route("/api/img/{id}/{size}", get(images::serve))
        .route("/api/photos/{id}/original", get(images::original))
        .merge(admin)
        .route("/api", any(api_not_found))
        .route("/api/{*path}", any(api_not_found))
        .method_not_allowed_fallback(method_not_allowed)
}
