#![allow(clippy::missing_errors_doc)]
#![allow(clippy::unnecessary_struct_initialization)]
#![allow(clippy::unused_async)]
use loco_rs::prelude::*;
use sea_orm::{ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, QueryOrder, Set};
use serde::{Deserialize, Serialize};

use crate::models::_entities::{categories, posts};
use crate::services::content;

#[derive(Clone, Debug, Serialize)]
pub struct CategorySummary {
    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>,
}

#[derive(Clone, Debug, Serialize)]
pub struct CategoryRecord {
    pub id: i32,
    pub name: Option<String>,
    pub slug: String,
    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, Deserialize)]
pub struct Params {
    pub name: Option<String>,
    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>,
}

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 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 normalized_name(params: &Params) -> Result<String> {
    let name = params
        .name
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .ok_or_else(|| Error::BadRequest("category name is required".to_string()))?;

    Ok(name.to_string())
}

fn normalized_slug(params: &Params, fallback: &str) -> String {
    params
        .slug
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(ToString::to_string)
        .unwrap_or_else(|| slugify(fallback))
}

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

fn build_summary(item: &categories::Model, post_items: &[posts::Model]) -> CategorySummary {
    let name = category_name(item);
    let count = post_items
        .iter()
        .filter(|post| {
            post.category
                .as_deref()
                .map(str::trim)
                .is_some_and(|value| {
                    value.eq_ignore_ascii_case(&name) || value.eq_ignore_ascii_case(&item.slug)
                })
        })
        .count();

    CategorySummary {
        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(),
    }
}

fn build_record(item: categories::Model) -> CategoryRecord {
    CategoryRecord {
        id: item.id,
        name: item.name,
        slug: item.slug,
        description: item.description,
        cover_image: item.cover_image,
        accent_color: item.accent_color,
        seo_title: item.seo_title,
        seo_description: item.seo_description,
        created_at: item.created_at.to_rfc3339(),
        updated_at: item.updated_at.to_rfc3339(),
    }
}

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

#[debug_handler]
pub async fn list(State(ctx): State<AppContext>) -> Result<Response> {
    let category_items = categories::Entity::find()
        .order_by_asc(categories::Column::Slug)
        .all(&ctx.db)
        .await?;
    let post_items = posts::Entity::find().all(&ctx.db).await?;

    let categories = category_items
        .into_iter()
        .map(|category| build_summary(&category, &post_items))
        .collect::<Vec<_>>();

    format::json(categories)
}

#[debug_handler]
pub async fn add(State(ctx): State<AppContext>, Json(params): Json<Params>) -> Result<Response> {
    let name = normalized_name(&params)?;
    let slug = normalized_slug(&params, &name);

    let existing = categories::Entity::find()
        .filter(categories::Column::Slug.eq(&slug))
        .one(&ctx.db)
        .await?;

    let item = if let Some(existing_category) = existing {
        let mut model = existing_category.into_active_model();
        model.name = Set(Some(name));
        model.slug = Set(slug);
        model.description = Set(trim_to_option(params.description));
        model.cover_image = Set(trim_to_option(params.cover_image));
        model.accent_color = Set(trim_to_option(params.accent_color));
        model.seo_title = Set(trim_to_option(params.seo_title));
        model.seo_description = Set(trim_to_option(params.seo_description));
        model.update(&ctx.db).await?
    } else {
        categories::ActiveModel {
            name: Set(Some(name)),
            slug: Set(slug),
            description: Set(trim_to_option(params.description)),
            cover_image: Set(trim_to_option(params.cover_image)),
            accent_color: Set(trim_to_option(params.accent_color)),
            seo_title: Set(trim_to_option(params.seo_title)),
            seo_description: Set(trim_to_option(params.seo_description)),
            ..Default::default()
        }
        .insert(&ctx.db)
        .await?
    };

    format::json(build_record(item))
}

#[debug_handler]
pub async fn update(
    Path(id): Path<i32>,
    State(ctx): State<AppContext>,
    Json(params): Json<Params>,
) -> Result<Response> {
    let name = normalized_name(&params)?;
    let slug = normalized_slug(&params, &name);
    let item = load_item(&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 item = item.into_active_model();
    item.name = Set(Some(name));
    item.slug = Set(slug);
    item.description = Set(trim_to_option(params.description));
    item.cover_image = Set(trim_to_option(params.cover_image));
    item.accent_color = Set(trim_to_option(params.accent_color));
    item.seo_title = Set(trim_to_option(params.seo_title));
    item.seo_description = Set(trim_to_option(params.seo_description));
    let item = item.update(&ctx.db).await?;
    format::json(build_record(item))
}

#[debug_handler]
pub async fn remove(Path(id): Path<i32>, State(ctx): State<AppContext>) -> Result<Response> {
    let item = load_item(&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 get_one(Path(id): Path<i32>, State(ctx): State<AppContext>) -> Result<Response> {
    format::json(build_record(load_item(&ctx, id).await?))
}

pub fn routes() -> Routes {
    Routes::new()
        .prefix("/api/categories")
        .add("/", get(list))
        .add("/", post(add))
        .add("/{id}", get(get_one))
        .add("/{id}", delete(remove))
        .add("/{id}", put(update))
        .add("/{id}", patch(update))
}
