use argon2::password_hash::rand_core::OsRng;
use argon2::password_hash::SaltString;
use argon2::{Argon2, PasswordHasher};
use axum::extract::{Path, State};
use axum::Json;
use chrono::{DateTime, Utc};
use serde::Deserialize;
use uuid::Uuid;

use crate::auth::random_token;
use crate::error::{ApiError, ApiResult};
use crate::state::AppState;

#[derive(sqlx::FromRow)]
struct ShareAdminRow {
    id: Uuid,
    token: String,
    label: String,
    password_hash: Option<String>,
    allow_download: bool,
    expires_at: Option<DateTime<Utc>>,
    locked_until: Option<DateTime<Utc>>,
    created_at: DateTime<Utc>,
    rating_count: i64,
    tag_count: i64,
    accept_count: i64,
    reject_count: i64,
}

fn share_json(state: &AppState, row: &ShareAdminRow) -> serde_json::Value {
    serde_json::json!({
        "id": row.id,
        "token": row.token,
        "url": format!("{}/s/{}", state.config.public_url, row.token),
        "label": row.label,
        "has_password": row.password_hash.is_some(),
        "allow_download": row.allow_download,
        "expires_at": row.expires_at,
        "locked": row.locked_until.map(|t| t > Utc::now()).unwrap_or(false),
        "created_at": row.created_at,
        "rating_count": row.rating_count,
        "tag_count": row.tag_count,
        "accept_count": row.accept_count,
        "reject_count": row.reject_count,
    })
}

// Aggregates run as laterals so each feedback table is scanned once per
// share (the verdict lateral yields both counts from a single pass).
const SHARE_SELECT: &str = "select s.id, s.token, s.label, s.password_hash, s.allow_download,
    s.expires_at, s.locked_until, s.created_at,
    rc.rating_count, tc.tag_count, vc.accept_count, vc.reject_count
    from shares s
    cross join lateral (select count(*) as rating_count from ratings r where r.share_id = s.id) rc
    cross join lateral (select count(*) as tag_count from tags t where t.share_id = s.id) tc
    cross join lateral (
        select count(*) filter (where v.verdict = 'accept') as accept_count,
               count(*) filter (where v.verdict = 'reject') as reject_count
        from verdicts v where v.share_id = s.id) vc";

pub async fn list(
    State(state): State<AppState>,
    Path(album_id): Path<Uuid>,
) -> ApiResult<Json<Vec<serde_json::Value>>> {
    let rows: Vec<ShareAdminRow> = sqlx::query_as(&format!(
        "{SHARE_SELECT} where s.album_id = $1 order by s.created_at desc"
    ))
    .bind(album_id)
    .fetch_all(&state.db)
    .await?;
    Ok(Json(rows.iter().map(|r| share_json(&state, r)).collect()))
}

#[derive(Deserialize)]
pub struct CreateShare {
    #[serde(default)]
    label: String,
    password: Option<String>,
    #[serde(default = "default_true")]
    allow_download: bool,
    expires_at: Option<DateTime<Utc>>,
}

fn default_true() -> bool {
    true
}

pub async fn create(
    State(state): State<AppState>,
    Path(album_id): Path<Uuid>,
    Json(body): Json<CreateShare>,
) -> ApiResult<Json<serde_json::Value>> {
    let album_exists: Option<(Uuid,)> = sqlx::query_as("select id from albums where id = $1")
        .bind(album_id)
        .fetch_optional(&state.db)
        .await?;
    if album_exists.is_none() {
        return Err(ApiError::not_found());
    }

    let password_hash = match body.password.as_deref().map(str::trim) {
        Some(pw) if !pw.is_empty() => {
            // Argon2 is deliberately slow; keep it off the async runtime threads.
            let pw = pw.to_string();
            let hash = tokio::task::spawn_blocking(move || {
                let salt = SaltString::generate(&mut OsRng);
                Argon2::default()
                    .hash_password(pw.as_bytes(), &salt)
                    .map(|h| h.to_string())
                    .map_err(|e| anyhow::anyhow!("hashing password: {e}"))
            })
            .await
            .map_err(|e| anyhow::anyhow!("hash task failed: {e}"))??;
            Some(hash)
        }
        _ => None,
    };

    let token = random_token(24);
    let (share_id,): (Uuid,) = sqlx::query_as(
        "insert into shares (album_id, token, label, password_hash, allow_download, expires_at)
         values ($1, $2, $3, $4, $5, $6)
         returning id",
    )
    .bind(album_id)
    .bind(&token)
    .bind(body.label.trim())
    .bind(&password_hash)
    .bind(body.allow_download)
    .bind(body.expires_at)
    .fetch_one(&state.db)
    .await?;

    let row: ShareAdminRow = sqlx::query_as(&format!("{SHARE_SELECT} where s.id = $1"))
        .bind(share_id)
        .fetch_one(&state.db)
        .await?;
    Ok(Json(share_json(&state, &row)))
}

/// Distinguishes an absent JSON field (keep current value) from an explicit
/// null (clear the expiry): absent → None, present → Some(inner).
fn double_option<'de, D>(de: D) -> Result<Option<Option<DateTime<Utc>>>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    serde::Deserialize::deserialize(de).map(Some)
}

#[derive(Deserialize)]
pub struct UpdateShare {
    allow_download: Option<bool>,
    #[serde(default, deserialize_with = "double_option")]
    expires_at: Option<Option<DateTime<Utc>>>,
}

pub async fn update(
    State(state): State<AppState>,
    Path(share_id): Path<Uuid>,
    Json(body): Json<UpdateShare>,
) -> ApiResult<Json<serde_json::Value>> {
    let updated = sqlx::query(
        "update shares set
             allow_download = coalesce($2, allow_download),
             expires_at = case when $3 then $4 else expires_at end
         where id = $1",
    )
    .bind(share_id)
    .bind(body.allow_download)
    .bind(body.expires_at.is_some())
    .bind(body.expires_at.flatten())
    .execute(&state.db)
    .await?;
    if updated.rows_affected() == 0 {
        return Err(ApiError::not_found());
    }
    let row: ShareAdminRow = sqlx::query_as(&format!("{SHARE_SELECT} where s.id = $1"))
        .bind(share_id)
        .fetch_one(&state.db)
        .await?;
    Ok(Json(share_json(&state, &row)))
}

/// Clear a share's password-lockout state (e.g. after a client fat-fingered
/// their way into the 15-minute lock).
pub async fn reset_lock(
    State(state): State<AppState>,
    Path(share_id): Path<Uuid>,
) -> ApiResult<Json<serde_json::Value>> {
    let updated =
        sqlx::query("update shares set failed_attempts = 0, locked_until = null where id = $1")
            .bind(share_id)
            .execute(&state.db)
            .await?;
    if updated.rows_affected() == 0 {
        return Err(ApiError::not_found());
    }
    Ok(Json(serde_json::json!({ "ok": true })))
}

pub async fn delete(
    State(state): State<AppState>,
    Path(share_id): Path<Uuid>,
) -> ApiResult<Json<serde_json::Value>> {
    let deleted = sqlx::query("delete from shares where id = $1")
        .bind(share_id)
        .execute(&state.db)
        .await?;
    if deleted.rows_affected() == 0 {
        return Err(ApiError::not_found());
    }
    Ok(Json(serde_json::json!({ "ok": true })))
}
