use std::collections::HashMap;

use axum::extract::{Path, State};
use axum::Json;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::error::{ApiError, ApiResult};
use crate::models::{Album, FeedbackAggregate, Photo, PhotoStatus};
use crate::state::AppState;

#[derive(Serialize, sqlx::FromRow)]
pub struct AlbumListItem {
    pub id: Uuid,
    pub name: String,
    pub description: String,
    pub created_at: DateTime<Utc>,
    pub photo_count: i64,
    pub cover_photo_id: Option<Uuid>,
    pub cover_processed_at: Option<DateTime<Utc>>,
}

pub async fn list(
    State(state): State<AppState>,
) -> ApiResult<Json<Vec<AlbumListItem>>> {
    let albums: Vec<AlbumListItem> = sqlx::query_as(
        "select a.id, a.name, a.description, a.created_at,
                (select count(*) from photos p where p.album_id = a.id) as photo_count,
                c.id as cover_photo_id, c.processed_at as cover_processed_at
         from albums a
         left join lateral (
             select p.id, p.processed_at from photos p
             where p.album_id = a.id and p.status = $1
             order by coalesce(p.taken_at, p.created_at), p.filename
             limit 1
         ) c on true
         order by a.created_at desc",
    )
    .bind(PhotoStatus::Ready.as_str())
    .fetch_all(&state.db)
    .await?;
    Ok(Json(albums))
}

#[derive(Deserialize)]
pub struct CreateAlbum {
    name: String,
    #[serde(default)]
    description: String,
}

pub async fn create(
    State(state): State<AppState>,
    Json(body): Json<CreateAlbum>,
) -> ApiResult<Json<Album>> {
    let name = body.name.trim();
    if name.is_empty() {
        return Err(ApiError::bad_request("album name is required"));
    }
    let album: Album =
        sqlx::query_as("insert into albums (name, description) values ($1, $2) returning *")
            .bind(name)
            .bind(body.description.trim())
            .fetch_one(&state.db)
            .await?;
    Ok(Json(album))
}

#[derive(Serialize)]
pub struct ShareRating {
    pub share_id: Uuid,
    pub share_label: String,
    pub rating: i32,
}

#[derive(Serialize)]
pub struct ShareTag {
    pub share_id: Uuid,
    pub share_label: String,
    pub tag: String,
}

#[derive(Serialize)]
pub struct ShareVerdict {
    pub share_id: Uuid,
    pub share_label: String,
    pub verdict: String,
}

#[derive(Serialize, Default)]
pub struct PhotoFeedback {
    pub ratings: Vec<ShareRating>,
    pub verdicts: Vec<ShareVerdict>,
    pub tags: Vec<ShareTag>,
}

pub(super) const RATING_ROWS: &str = "select r.photo_id, s.id, s.label, r.rating
    from ratings r join shares s on s.id = r.share_id
    where s.album_id = $1 and ($2::uuid[] is null or r.share_id = any($2))";
pub(super) const VERDICT_ROWS: &str = "select v.photo_id, s.id, s.label, v.verdict
    from verdicts v join shares s on s.id = v.share_id
    where s.album_id = $1 and ($2::uuid[] is null or v.share_id = any($2))";
pub(super) const TAG_ROWS: &str = "select t.photo_id, s.id, s.label, t.tag
    from tags t join shares s on s.id = t.share_id
    where s.album_id = $1 and ($2::uuid[] is null or t.share_id = any($2))
    order by t.created_at";

/// One feedback query: (photo_id, share id, share label, value) rows for an
/// album, optionally narrowed to a single share ($2).
pub(super) async fn feedback_rows<T>(
    db: &sqlx::PgPool,
    sql: &str,
    album_id: Uuid,
    shares: Option<&[Uuid]>,
) -> Result<Vec<(Uuid, Uuid, String, T)>, sqlx::Error>
where
    (Uuid, Uuid, String, T): for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow> + Send + Unpin,
{
    sqlx::query_as(sql)
        .bind(album_id)
        .bind(shares)
        .fetch_all(db)
        .await
}

fn fold_feedback<T>(
    feedback: &mut HashMap<Uuid, PhotoFeedback>,
    rows: Vec<(Uuid, Uuid, String, T)>,
    push: impl Fn(&mut PhotoFeedback, Uuid, String, T),
) {
    for (photo_id, share_id, share_label, value) in rows {
        push(feedback.entry(photo_id).or_default(), share_id, share_label, value);
    }
}

#[derive(Serialize)]
pub struct AlbumDetail {
    pub album: Album,
    pub photos: Vec<Photo>,
    pub feedback: HashMap<Uuid, PhotoFeedback>,
    pub aggregates: HashMap<Uuid, FeedbackAggregate>,
}

pub async fn get_one(
    State(state): State<AppState>,
    Path(album_id): Path<Uuid>,
) -> ApiResult<Json<AlbumDetail>> {
    let album: Album = sqlx::query_as("select * from albums where id = $1")
        .bind(album_id)
        .fetch_one(&state.db)
        .await?;
    let photos: Vec<Photo> = sqlx::query_as(
        "select * from photos where album_id = $1
         order by coalesce(taken_at, created_at), filename",
    )
    .bind(album_id)
    .fetch_all(&state.db)
    .await?;

    // The three feedback kinds are independent (photo_id, share, value)
    // queries — run them concurrently and fold with one shared shape.
    let (ratings, verdicts, tags) = tokio::try_join!(
        feedback_rows::<i32>(&state.db, RATING_ROWS, album_id, None),
        feedback_rows::<String>(&state.db, VERDICT_ROWS, album_id, None),
        feedback_rows::<String>(&state.db, TAG_ROWS, album_id, None),
    )?;

    let mut aggregates: HashMap<Uuid, FeedbackAggregate> = HashMap::new();
    for (photo_id, _, _, rating) in &ratings {
        aggregates.entry(*photo_id).or_default().add_rating(*rating);
    }
    for (photo_id, _, _, verdict) in &verdicts {
        aggregates.entry(*photo_id).or_default().add_verdict(verdict);
    }
    for photo in &photos {
        if photo.owner_rating.is_some() || photo.owner_verdict.is_some() {
            aggregates
                .entry(photo.id)
                .or_default()
                .add_owner(photo.owner_rating, photo.owner_verdict.as_deref());
        }
    }

    let mut feedback: HashMap<Uuid, PhotoFeedback> = HashMap::new();
    fold_feedback(&mut feedback, ratings, |f, share_id, share_label, rating| {
        f.ratings.push(ShareRating {
            share_id,
            share_label,
            rating,
        })
    });
    fold_feedback(&mut feedback, verdicts, |f, share_id, share_label, verdict| {
        f.verdicts.push(ShareVerdict {
            share_id,
            share_label,
            verdict,
        })
    });
    fold_feedback(&mut feedback, tags, |f, share_id, share_label, tag| {
        f.tags.push(ShareTag {
            share_id,
            share_label,
            tag,
        })
    });

    Ok(Json(AlbumDetail {
        album,
        photos,
        feedback,
        aggregates,
    }))
}

#[derive(Serialize, sqlx::FromRow)]
pub struct PendingPhoto {
    pub id: Uuid,
    pub filename: String,
    pub status: String,
    pub error: Option<String>,
}

pub async fn pending(
    State(state): State<AppState>,
    Path(album_id): Path<Uuid>,
) -> ApiResult<Json<serde_json::Value>> {
    let pending: Vec<PendingPhoto> = sqlx::query_as(
        "select id, filename, status, error from photos
         where album_id = $1 and status != $2
         order by created_at",
    )
    .bind(album_id)
    .bind(PhotoStatus::Ready.as_str())
    .fetch_all(&state.db)
    .await?;
    Ok(Json(serde_json::json!({ "pending": pending })))
}

#[derive(Deserialize)]
pub struct UpdateAlbum {
    name: Option<String>,
    description: Option<String>,
}

pub async fn update(
    State(state): State<AppState>,
    Path(album_id): Path<Uuid>,
    Json(body): Json<UpdateAlbum>,
) -> ApiResult<Json<Album>> {
    if let Some(name) = &body.name {
        if name.trim().is_empty() {
            return Err(ApiError::bad_request("album name cannot be empty"));
        }
    }
    let album: Album = sqlx::query_as(
        "update albums
         set name = coalesce($2, name), description = coalesce($3, description)
         where id = $1
         returning *",
    )
    .bind(album_id)
    .bind(body.name.as_deref().map(str::trim))
    .bind(body.description.as_deref().map(str::trim))
    .fetch_one(&state.db)
    .await?;
    Ok(Json(album))
}

pub async fn delete(
    State(state): State<AppState>,
    Path(album_id): Path<Uuid>,
) -> ApiResult<Json<serde_json::Value>> {
    let mut tx = state.db.begin().await?;
    // Lock the album row: concurrent uploads block on their FK check against
    // it, then fail once it's gone and clean up their own S3 objects — so no
    // photo can slip in between the cleanup enqueue and the cascade delete.
    let locked: Option<(Uuid,)> = sqlx::query_as("select id from albums where id = $1 for update")
        .bind(album_id)
        .fetch_optional(&mut *tx)
        .await?;
    if locked.is_none() {
        return Err(ApiError::not_found());
    }
    // Delete photos through the shared path so the S3 cleanup convention
    // lives in one place; the album lock above keeps this set complete.
    let photo_ids: Vec<Uuid> = sqlx::query_scalar("select id from photos where album_id = $1")
        .bind(album_id)
        .fetch_all(&mut *tx)
        .await?;
    super::photos::delete_with_cleanup(&mut tx, &photo_ids).await?;
    sqlx::query("delete from albums where id = $1")
        .bind(album_id)
        .execute(&mut *tx)
        .await?;
    tx.commit().await?;
    Ok(Json(serde_json::json!({ "ok": true })))
}
