use axum::body::Body;
use axum::extract::{Path, State};
use axum::http::{header, StatusCode};
use axum::response::Response;
use axum_extra::extract::cookie::SignedCookieJar;
use tokio_util::io::ReaderStream;
use uuid::Uuid;

use crate::auth::user_from_jar;
use crate::error::{ApiError, ApiResult};
use crate::models::{Photo, PhotoStatus};
use crate::routes::client::authorize_album_via_cookie;
use crate::s3;
use crate::state::AppState;

/// Allow access if the requester is the signed-in photographer, or holds a
/// share-access cookie (set when viewing/unlocking a share) covering the
/// photo's album.
async fn authorize_photo(
    state: &AppState,
    jar: &SignedCookieJar,
    photo_id: Uuid,
    need_download: bool,
) -> Result<Photo, ApiError> {
    let photo: Option<Photo> = sqlx::query_as("select * from photos where id = $1")
        .bind(photo_id)
        .fetch_optional(&state.db)
        .await?;
    let photo = photo.ok_or_else(ApiError::not_found)?;

    if user_from_jar(state, jar).is_some() {
        return Ok(photo);
    }
    authorize_album_via_cookie(state, jar, photo.album_id, need_download).await?;
    // Clients may only reach photos the share listing exposes.
    if photo.status != PhotoStatus::Ready {
        return Err(ApiError::not_found());
    }
    Ok(photo)
}

async fn stream_object(
    state: &AppState,
    key: &str,
    content_type: &str,
    attachment_name: Option<&str>,
) -> Result<Response, ApiError> {
    let object = state
        .s3
        .get_object()
        .bucket(&state.config.s3_bucket)
        .key(key)
        .send()
        .await
        .map_err(|e| {
            tracing::warn!("s3 get {key} failed: {e}");
            ApiError::not_found()
        })?;

    let mut builder = Response::builder()
        .status(StatusCode::OK)
        .header(header::CONTENT_TYPE, content_type)
        .header(
            header::CACHE_CONTROL,
            "private, max-age=31536000, immutable",
        );
    if let Some(length) = object.content_length() {
        builder = builder.header(header::CONTENT_LENGTH, length);
    }
    if let Some(name) = attachment_name {
        let safe = name.replace(['"', '\\'], "_");
        builder = builder.header(
            header::CONTENT_DISPOSITION,
            format!("attachment; filename=\"{safe}\""),
        );
    }
    let stream = ReaderStream::new(object.body.into_async_read());
    builder
        .body(Body::from_stream(stream))
        .map_err(|e| anyhow::anyhow!("building response: {e}").into())
}

pub async fn serve(
    State(state): State<AppState>,
    Path((photo_id, size)): Path<(Uuid, String)>,
    jar: SignedCookieJar,
) -> ApiResult<Response> {
    let key = match size.as_str() {
        "thumb" => s3::thumb_key(photo_id),
        "preview" => s3::preview_key(photo_id),
        _ => return Err(ApiError::bad_request("size must be thumb or preview")),
    };
    authorize_photo(&state, &jar, photo_id, false).await?;
    stream_object(&state, &key, "image/jpeg", None).await
}

pub async fn original(
    State(state): State<AppState>,
    Path(photo_id): Path<Uuid>,
    jar: SignedCookieJar,
) -> ApiResult<Response> {
    let photo = authorize_photo(&state, &jar, photo_id, true).await?;
    let key = s3::original_key(photo_id, &photo.filename);
    stream_object(&state, &key, &photo.content_type, Some(&photo.filename)).await
}
