use std::collections::HashMap;

use loco_rs::prelude::*;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, Set};
use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::models::_entities::media_assets;

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct MediaAssetMetadataInput {
    pub title: Option<String>,
    pub alt_text: Option<String>,
    pub caption: Option<String>,
    pub tags: Option<Vec<String>>,
    pub notes: Option<String>,
}

fn trim_to_option(value: Option<String>) -> Option<String> {
    value.and_then(|item| {
        let trimmed = item.trim().to_string();
        if trimmed.is_empty() {
            None
        } else {
            Some(trimmed)
        }
    })
}

fn normalize_tag_list(values: Option<Vec<String>>) -> Option<Value> {
    let mut seen = std::collections::HashSet::new();
    let tags = values
        .unwrap_or_default()
        .into_iter()
        .filter_map(|item| trim_to_option(Some(item)))
        .map(|item| item.to_ascii_lowercase())
        .filter(|item| seen.insert(item.clone()))
        .map(Value::String)
        .collect::<Vec<_>>();

    (!tags.is_empty()).then_some(Value::Array(tags))
}

pub fn tag_list(model: &media_assets::Model) -> Vec<String> {
    model
        .tags
        .as_ref()
        .and_then(Value::as_array)
        .cloned()
        .unwrap_or_default()
        .into_iter()
        .filter_map(|item| item.as_str().map(ToString::to_string))
        .collect()
}

pub async fn list_by_keys(
    ctx: &AppContext,
    keys: &[String],
) -> Result<HashMap<String, media_assets::Model>> {
    if keys.is_empty() {
        return Ok(HashMap::new());
    }

    Ok(media_assets::Entity::find()
        .filter(media_assets::Column::ObjectKey.is_in(keys.iter().cloned()))
        .all(&ctx.db)
        .await?
        .into_iter()
        .map(|item| (item.object_key.clone(), item))
        .collect())
}

pub async fn get_by_key(ctx: &AppContext, object_key: &str) -> Result<Option<media_assets::Model>> {
    media_assets::Entity::find()
        .filter(media_assets::Column::ObjectKey.eq(object_key))
        .one(&ctx.db)
        .await
        .map_err(Into::into)
}

pub async fn upsert_by_key(
    ctx: &AppContext,
    object_key: &str,
    payload: MediaAssetMetadataInput,
) -> Result<media_assets::Model> {
    let normalized_key = object_key.trim();
    if normalized_key.is_empty() {
        return Err(Error::BadRequest("object key 不能为空".to_string()));
    }

    let existing = get_by_key(ctx, normalized_key).await?;
    let has_existing = existing.is_some();
    let mut active = existing
        .map(|item| item.into_active_model())
        .unwrap_or_else(|| media_assets::ActiveModel {
            object_key: Set(normalized_key.to_string()),
            ..Default::default()
        });

    active.title = Set(trim_to_option(payload.title));
    active.alt_text = Set(trim_to_option(payload.alt_text));
    active.caption = Set(trim_to_option(payload.caption));
    active.tags = Set(normalize_tag_list(payload.tags));
    active.notes = Set(trim_to_option(payload.notes));

    if has_existing {
        active.update(&ctx.db).await.map_err(Into::into)
    } else {
        active.insert(&ctx.db).await.map_err(Into::into)
    }
}

pub async fn delete_by_key(ctx: &AppContext, object_key: &str) -> Result<()> {
    if let Some(item) = get_by_key(ctx, object_key).await? {
        item.delete(&ctx.db).await?;
    }
    Ok(())
}

pub async fn delete_by_keys(ctx: &AppContext, object_keys: &[String]) -> Result<()> {
    for key in object_keys {
        delete_by_key(ctx, key).await?;
    }
    Ok(())
}
