#![allow(clippy::missing_errors_doc)]
#![allow(clippy::unnecessary_struct_initialization)]
#![allow(clippy::unused_async)]

use axum::http::HeaderMap;
use loco_rs::prelude::*;
use sea_orm::{ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, QueryOrder, Set};
use serde::{Deserialize, Serialize};

use crate::{
    controllers::admin::check_auth,
    models::_entities::{categories, posts, tags},
    services::content,
};

#[derive(Clone, Debug, Deserialize)]
pub struct TaxonomyPayload {
    pub name: Option<String>,
    #[serde(default)]
    pub slug: Option<String>,
    #[serde(default)]
    pub description: Option<String>,
    #[serde(default)]
    pub cover_image: Option<String>,
    #[serde(default)]
    pub accent_color: Option<String>,
    #[serde(default)]
    pub seo_title: Option<String>,
    #[serde(default)]
    pub seo_description: Option<String>,
}

#[derive(Clone, Debug, Serialize)]
pub struct AdminCategoryRecord {
    pub id: i32,
    pub name: String,
    pub slug: String,
    pub count: usize,
    pub description: Option<String>,
    pub cover_image: Option<String>,
    pub accent_color: Option<String>,
    pub seo_title: Option<String>,
    pub seo_description: Option<String>,
    pub created_at: String,
    pub updated_at: String,
}

#[derive(Clone, Debug, Serialize)]
pub struct AdminTagRecord {
    pub id: i32,
    pub name: String,
    pub slug: String,
    pub count: usize,
    pub description: Option<String>,
    pub cover_image: Option<String>,
    pub accent_color: Option<String>,
    pub seo_title: Option<String>,
    pub seo_description: Option<String>,
    pub created_at: String,
    pub updated_at: String,
}

fn slugify(value: &str) -> String {
    let mut slug = String::new();
    let mut last_was_dash = false;

    for ch in value.trim().chars() {
        if ch.is_ascii_alphanumeric() {
            slug.push(ch.to_ascii_lowercase());
            last_was_dash = false;
        } else if (ch.is_whitespace() || ch == '-' || ch == '_') && !last_was_dash {
            slug.push('-');
            last_was_dash = true;
        }
    }

    slug.trim_matches('-').to_string()
}

fn normalized_name(params: &TaxonomyPayload, label: &str) -> Result<String> {
    params
        .name
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(ToString::to_string)
        .ok_or_else(|| Error::BadRequest(format!("{label}名称不能为空")))
}

fn normalized_slug(value: Option<&str>, fallback: &str, label: &str) -> Result<String> {
    let slug = value
        .map(str::trim)
        .filter(|item| !item.is_empty())
        .map(ToString::to_string)
        .unwrap_or_else(|| slugify(fallback));

    if slug.is_empty() {
        return Err(Error::BadRequest(format!(
            "{label} slug 不能为空，请填写英文字母 / 数字 / 连字符"
        )));
    }

    Ok(slug)
}

fn normalized_token(value: &str) -> String {
    value.trim().to_ascii_lowercase()
}

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 post_tag_values(post: &posts::Model) -> Vec<String> {
    post.tags
        .as_ref()
        .and_then(|value| serde_json::from_value::<Vec<String>>(value.clone()).ok())
        .unwrap_or_default()
        .into_iter()
        .map(|item| normalized_token(&item))
        .filter(|item| !item.is_empty())
        .collect()
}

fn category_name(item: &categories::Model) -> String {
    item.name.clone().unwrap_or_else(|| item.slug.clone())
}

fn tag_name(item: &tags::Model) -> String {
    item.name.clone().unwrap_or_else(|| item.slug.clone())
}

fn build_category_record(
    item: &categories::Model,
    post_items: &[posts::Model],
) -> AdminCategoryRecord {
    let name = category_name(item);
    let aliases = [normalized_token(&name), normalized_token(&item.slug)];
    let count = post_items
        .iter()
        .filter(|post| {
            post.category
                .as_deref()
                .map(normalized_token)
                .is_some_and(|value| aliases.iter().any(|alias| alias == &value))
        })
        .count();

    AdminCategoryRecord {
        id: item.id,
        name,
        slug: item.slug.clone(),
        count,
        description: item.description.clone(),
        cover_image: item.cover_image.clone(),
        accent_color: item.accent_color.clone(),
        seo_title: item.seo_title.clone(),
        seo_description: item.seo_description.clone(),
        created_at: item.created_at.to_rfc3339(),
        updated_at: item.updated_at.to_rfc3339(),
    }
}

fn build_tag_record(item: &tags::Model, post_items: &[posts::Model]) -> AdminTagRecord {
    let name = tag_name(item);
    let aliases = [normalized_token(&name), normalized_token(&item.slug)];
    let count = post_items
        .iter()
        .filter(|post| {
            post_tag_values(post)
                .into_iter()
                .any(|value| aliases.iter().any(|alias| alias == &value))
        })
        .count();

    AdminTagRecord {
        id: item.id,
        name,
        slug: item.slug.clone(),
        count,
        description: item.description.clone(),
        cover_image: item.cover_image.clone(),
        accent_color: item.accent_color.clone(),
        seo_title: item.seo_title.clone(),
        seo_description: item.seo_description.clone(),
        created_at: item.created_at.to_rfc3339(),
        updated_at: item.updated_at.to_rfc3339(),
    }
}

async fn load_category(ctx: &AppContext, id: i32) -> Result<categories::Model> {
    categories::Entity::find_by_id(id)
        .one(&ctx.db)
        .await?
        .ok_or(Error::NotFound)
}

async fn load_tag(ctx: &AppContext, id: i32) -> Result<tags::Model> {
    tags::Entity::find_by_id(id)
        .one(&ctx.db)
        .await?
        .ok_or(Error::NotFound)
}

async fn ensure_category_slug_unique(
    ctx: &AppContext,
    slug: &str,
    exclude_id: Option<i32>,
) -> Result<()> {
    if let Some(existing) = categories::Entity::find()
        .filter(categories::Column::Slug.eq(slug))
        .one(&ctx.db)
        .await?
    {
        if Some(existing.id) != exclude_id {
            return Err(Error::BadRequest("分类 slug 已存在".to_string()));
        }
    }

    Ok(())
}

async fn ensure_tag_slug_unique(
    ctx: &AppContext,
    slug: &str,
    exclude_id: Option<i32>,
) -> Result<()> {
    if let Some(existing) = tags::Entity::find()
        .filter(tags::Column::Slug.eq(slug))
        .one(&ctx.db)
        .await?
    {
        if Some(existing.id) != exclude_id {
            return Err(Error::BadRequest("标签 slug 已存在".to_string()));
        }
    }

    Ok(())
}

async fn load_posts(ctx: &AppContext) -> Result<Vec<posts::Model>> {
    Ok(posts::Entity::find().all(&ctx.db).await?)
}

#[debug_handler]
pub async fn list_categories(
    headers: HeaderMap,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    check_auth(&headers)?;

    let items = categories::Entity::find()
        .order_by_asc(categories::Column::Slug)
        .all(&ctx.db)
        .await?;
    let post_items = load_posts(&ctx).await?;

    format::json(
        items
            .into_iter()
            .map(|item| build_category_record(&item, &post_items))
            .collect::<Vec<_>>(),
    )
}

#[debug_handler]
pub async fn create_category(
    headers: HeaderMap,
    State(ctx): State<AppContext>,
    Json(payload): Json<TaxonomyPayload>,
) -> Result<Response> {
    check_auth(&headers)?;

    let name = normalized_name(&payload, "分类")?;
    let slug = normalized_slug(payload.slug.as_deref(), &name, "分类")?;
    ensure_category_slug_unique(&ctx, &slug, None).await?;

    let item = categories::ActiveModel {
        name: Set(Some(name)),
        slug: Set(slug),
        description: Set(trim_to_option(payload.description)),
        cover_image: Set(trim_to_option(payload.cover_image)),
        accent_color: Set(trim_to_option(payload.accent_color)),
        seo_title: Set(trim_to_option(payload.seo_title)),
        seo_description: Set(trim_to_option(payload.seo_description)),
        ..Default::default()
    }
    .insert(&ctx.db)
    .await?;
    let post_items = load_posts(&ctx).await?;

    format::json(build_category_record(&item, &post_items))
}

#[debug_handler]
pub async fn update_category(
    headers: HeaderMap,
    Path(id): Path<i32>,
    State(ctx): State<AppContext>,
    Json(payload): Json<TaxonomyPayload>,
) -> Result<Response> {
    check_auth(&headers)?;

    let name = normalized_name(&payload, "分类")?;
    let slug = normalized_slug(payload.slug.as_deref(), &name, "分类")?;
    ensure_category_slug_unique(&ctx, &slug, Some(id)).await?;

    let item = load_category(&ctx, id).await?;
    let previous_name = item.name.clone();
    let previous_slug = item.slug.clone();

    if previous_name
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
        != Some(name.as_str())
    {
        content::rewrite_category_references(
            &ctx,
            previous_name.as_deref(),
            &previous_slug,
            Some(&name),
        )
        .await?;
    }

    let mut active = item.into_active_model();
    active.name = Set(Some(name));
    active.slug = Set(slug);
    active.description = Set(trim_to_option(payload.description));
    active.cover_image = Set(trim_to_option(payload.cover_image));
    active.accent_color = Set(trim_to_option(payload.accent_color));
    active.seo_title = Set(trim_to_option(payload.seo_title));
    active.seo_description = Set(trim_to_option(payload.seo_description));
    let updated = active.update(&ctx.db).await?;
    let post_items = load_posts(&ctx).await?;

    format::json(build_category_record(&updated, &post_items))
}

#[debug_handler]
pub async fn delete_category(
    headers: HeaderMap,
    Path(id): Path<i32>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    check_auth(&headers)?;

    let item = load_category(&ctx, id).await?;
    content::rewrite_category_references(&ctx, item.name.as_deref(), &item.slug, None).await?;
    item.delete(&ctx.db).await?;

    format::empty()
}

#[debug_handler]
pub async fn list_tags(headers: HeaderMap, State(ctx): State<AppContext>) -> Result<Response> {
    check_auth(&headers)?;

    let items = tags::Entity::find()
        .order_by_asc(tags::Column::Slug)
        .all(&ctx.db)
        .await?;
    let post_items = load_posts(&ctx).await?;

    format::json(
        items
            .into_iter()
            .map(|item| build_tag_record(&item, &post_items))
            .collect::<Vec<_>>(),
    )
}

#[debug_handler]
pub async fn create_tag(
    headers: HeaderMap,
    State(ctx): State<AppContext>,
    Json(payload): Json<TaxonomyPayload>,
) -> Result<Response> {
    check_auth(&headers)?;

    let name = normalized_name(&payload, "标签")?;
    let slug = normalized_slug(payload.slug.as_deref(), &name, "标签")?;
    ensure_tag_slug_unique(&ctx, &slug, None).await?;

    let item = tags::ActiveModel {
        name: Set(Some(name)),
        slug: Set(slug),
        description: Set(trim_to_option(payload.description)),
        cover_image: Set(trim_to_option(payload.cover_image)),
        accent_color: Set(trim_to_option(payload.accent_color)),
        seo_title: Set(trim_to_option(payload.seo_title)),
        seo_description: Set(trim_to_option(payload.seo_description)),
        ..Default::default()
    }
    .insert(&ctx.db)
    .await?;
    let post_items = load_posts(&ctx).await?;

    format::json(build_tag_record(&item, &post_items))
}

#[debug_handler]
pub async fn update_tag(
    headers: HeaderMap,
    Path(id): Path<i32>,
    State(ctx): State<AppContext>,
    Json(payload): Json<TaxonomyPayload>,
) -> Result<Response> {
    check_auth(&headers)?;

    let name = normalized_name(&payload, "标签")?;
    let slug = normalized_slug(payload.slug.as_deref(), &name, "标签")?;
    ensure_tag_slug_unique(&ctx, &slug, Some(id)).await?;

    let item = load_tag(&ctx, id).await?;
    let previous_name = item.name.clone();
    let previous_slug = item.slug.clone();

    if previous_name
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
        != Some(name.as_str())
    {
        content::rewrite_tag_references(
            &ctx,
            previous_name.as_deref(),
            &previous_slug,
            Some(&name),
        )
        .await?;
    }

    let mut active = item.into_active_model();
    active.name = Set(Some(name));
    active.slug = Set(slug);
    active.description = Set(trim_to_option(payload.description));
    active.cover_image = Set(trim_to_option(payload.cover_image));
    active.accent_color = Set(trim_to_option(payload.accent_color));
    active.seo_title = Set(trim_to_option(payload.seo_title));
    active.seo_description = Set(trim_to_option(payload.seo_description));
    let updated = active.update(&ctx.db).await?;
    let post_items = load_posts(&ctx).await?;

    format::json(build_tag_record(&updated, &post_items))
}

#[debug_handler]
pub async fn delete_tag(
    headers: HeaderMap,
    Path(id): Path<i32>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    check_auth(&headers)?;

    let item = load_tag(&ctx, id).await?;
    content::rewrite_tag_references(&ctx, item.name.as_deref(), &item.slug, None).await?;
    item.delete(&ctx.db).await?;

    format::empty()
}

pub fn routes() -> Routes {
    Routes::new()
        .add(
            "/api/admin/categories",
            get(list_categories).post(create_category),
        )
        .add(
            "/api/admin/categories/{id}",
            patch(update_category).delete(delete_category),
        )
        .add("/api/admin/tags", get(list_tags).post(create_tag))
        .add("/api/admin/tags/{id}", patch(update_tag).delete(delete_tag))
}
