//! Props management API handlers for admin UI.

use axum::Json;
use axum::extract::{Query, State};
use axum_extra::extract::Multipart;
use chattyness_db::{
    models::{CreateServerPropRequest, ServerProp, ServerPropSummary},
    queries::props,
};
use chattyness_error::{AppError, OptionExt};
use serde::Deserialize;
use serde::Serialize;
use sha2::{Digest, Sha256};
use sqlx::PgPool;
use std::path::PathBuf;
use uuid::Uuid;

use crate::auth::AdminConn;

// =============================================================================
// API Types
// =============================================================================

/// Query parameters for prop creation.
#[derive(Debug, Deserialize)]
pub struct CreatePropQuery {
    /// If true, update existing prop with same slug instead of returning 409 Conflict.
    #[serde(default)]
    pub force: bool,
}

/// Response for prop creation.
#[derive(Debug, Serialize)]
pub struct CreatePropResponse {
    pub id: Uuid,
    pub name: String,
    pub slug: String,
    pub asset_path: String,
    pub created_at: chrono::DateTime<chrono::Utc>,
}

impl From<ServerProp> for CreatePropResponse {
    fn from(prop: ServerProp) -> Self {
        Self {
            id: prop.id,
            name: prop.name,
            slug: prop.slug,
            asset_path: prop.asset_path,
            created_at: prop.created_at,
        }
    }
}

// =============================================================================
// File Handling
// =============================================================================

/// Validate and get file extension from filename.
fn validate_file_extension(filename: &str) -> Result<&'static str, AppError> {
    let ext = filename
        .rsplit('.')
        .next()
        .map(|e| e.to_lowercase())
        .unwrap_or_default();

    match ext.as_str() {
        "svg" => Ok("svg"),
        "png" => Ok("png"),
        _ => Err(AppError::Validation("File must be SVG or PNG".to_string())),
    }
}

/// Store uploaded file and return the asset path.
async fn store_prop_file(bytes: &[u8], extension: &str) -> Result<String, AppError> {
    // Compute SHA256 hash of the file content
    let mut hasher = Sha256::new();
    hasher.update(bytes);
    let hash = hex::encode(hasher.finalize());

    // Create directory structure: /srv/chattyness/assets/server/
    let dir_path = PathBuf::from("/srv/chattyness/assets/server");

    tokio::fs::create_dir_all(&dir_path)
        .await
        .map_err(|e| AppError::Internal(format!("Failed to create directory: {}", e)))?;

    // Write the file with SHA256 hash as filename
    let filename = format!("{}.{}", hash, extension);
    let file_path = dir_path.join(&filename);

    tokio::fs::write(&file_path, bytes)
        .await
        .map_err(|e| AppError::Internal(format!("Failed to write file: {}", e)))?;

    // Return the relative path for database storage
    Ok(format!("server/{}", filename))
}

// =============================================================================
// API Handlers
// =============================================================================

/// List all server props.
pub async fn list_props(
    State(pool): State<PgPool>,
) -> Result<Json<Vec<ServerPropSummary>>, AppError> {
    let prop_list = props::list_server_props(&pool).await?;
    Ok(Json(prop_list))
}

/// Create a new server prop via multipart upload.
///
/// Expects multipart form with:
/// - `metadata`: JSON object with prop details (CreateServerPropRequest)
/// - `file`: Binary SVG or PNG file
///
/// Query parameters:
/// - `force`: If true, update existing prop with same slug instead of returning 409 Conflict.
pub async fn create_prop(
    admin_conn: AdminConn,
    Query(query): Query<CreatePropQuery>,
    mut multipart: Multipart,
) -> Result<Json<CreatePropResponse>, AppError> {
    let conn = admin_conn.0;

    let mut metadata: Option<CreateServerPropRequest> = None;
    let mut file_data: Option<(Vec<u8>, String)> = None; // (bytes, extension)

    // Parse multipart fields
    while let Some(field) = multipart
        .next_field()
        .await
        .map_err(|e| AppError::Validation(format!("Failed to read multipart field: {}", e)))?
    {
        let name = field.name().unwrap_or_default().to_string();

        match name.as_str() {
            "metadata" => {
                let text = field
                    .text()
                    .await
                    .map_err(|e| AppError::Validation(format!("Failed to read metadata: {}", e)))?;

                metadata =
                    Some(serde_json::from_str(&text).map_err(|e| {
                        AppError::Validation(format!("Invalid metadata JSON: {}", e))
                    })?);
            }
            "file" => {
                let filename = field
                    .file_name()
                    .map(|s| s.to_string())
                    .unwrap_or_else(|| "unknown.png".to_string());

                let extension = validate_file_extension(&filename)?;

                let bytes = field
                    .bytes()
                    .await
                    .map_err(|e| AppError::Validation(format!("Failed to read file: {}", e)))?;

                if bytes.is_empty() {
                    return Err(AppError::Validation("File is empty".to_string()));
                }

                file_data = Some((bytes.to_vec(), extension.to_string()));
            }
            _ => {
                // Ignore unknown fields
            }
        }
    }

    // Validate we have both required fields
    let metadata = metadata.ok_or_else(|| {
        AppError::Validation("Missing 'metadata' field in multipart form".to_string())
    })?;

    let (file_bytes, extension) = file_data.ok_or_else(|| {
        AppError::Validation("Missing 'file' field in multipart form".to_string())
    })?;

    // Validate the request
    metadata.validate()?;

    // Store the file first (SHA256-based, safe to run even if prop exists)
    let asset_path = store_prop_file(&file_bytes, &extension).await?;

    // Acquire the connection for database operations
    let mut guard = conn.acquire().await;

    let prop = if query.force {
        // Force mode: upsert (insert or update)
        props::upsert_server_prop(&mut *guard, &metadata, &asset_path, None).await?
    } else {
        // Normal mode: check availability first
        let slug = metadata.slug_or_generate();
        let available = props::is_prop_slug_available(&mut *guard, &slug).await?;
        if !available {
            return Err(AppError::Conflict(format!(
                "Prop slug '{}' is already taken",
                slug
            )));
        }
        props::create_server_prop(&mut *guard, &metadata, &asset_path, None).await?
    };

    let action = if query.force { "Upserted" } else { "Created" };
    tracing::info!("{} server prop: {} ({})", action, prop.name, prop.id);

    Ok(Json(CreatePropResponse::from(prop)))
}

/// Get a server prop by ID.
pub async fn get_prop(
    State(pool): State<PgPool>,
    axum::extract::Path(prop_id): axum::extract::Path<Uuid>,
) -> Result<Json<ServerProp>, AppError> {
    let prop = props::get_server_prop_by_id(&pool, prop_id)
        .await?
        .or_not_found("Prop")?;
    Ok(Json(prop))
}

/// Delete a server prop.
pub async fn delete_prop(
    admin_conn: AdminConn,
    axum::extract::Path(prop_id): axum::extract::Path<Uuid>,
) -> Result<Json<()>, AppError> {
    let conn = admin_conn.0;
    let mut guard = conn.acquire().await;

    // Get the prop first to get the asset path
    let prop = props::get_server_prop_by_id(&mut *guard, prop_id)
        .await?
        .or_not_found("Prop")?;

    // Delete from database
    props::delete_server_prop(&mut *guard, prop_id).await?;

    // Try to delete the file (don't fail if file doesn't exist)
    let file_path = PathBuf::from("/srv/chattyness/assets").join(&prop.asset_path);
    tokio::fs::remove_file(&file_path).await.ok();

    tracing::info!("Deleted server prop: {} ({})", prop.name, prop_id);

    Ok(Json(()))
}
