#![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 serde_json::Value;

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

#[derive(Clone, Debug, Serialize)]
pub struct TagSummary {
    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 TagRecord {
    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 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 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: &Params) -> Result<String> {
    params
        .name
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(ToString::to_string)
        .ok_or_else(|| Error::BadRequest("tag name is required".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 tag_name(item: &tags::Model) -> String {
    item.name.clone().unwrap_or_else(|| item.slug.clone())
}

fn tag_values(post: &posts::Model) -> Vec<String> {
    post.tags
        .as_ref()
        .and_then(Value::as_array)
        .cloned()
        .unwrap_or_default()
        .into_iter()
        .filter_map(|item| item.as_str().map(|value| value.trim().to_ascii_lowercase()))
        .filter(|item| !item.is_empty())
        .collect()
}

fn build_summary(item: &tags::Model, post_items: &[posts::Model]) -> TagSummary {
    let name = tag_name(item);
    let aliases = [
        name.trim().to_ascii_lowercase(),
        item.slug.trim().to_ascii_lowercase(),
    ];
    let count = post_items
        .iter()
        .filter(|post| {
            tag_values(post)
                .into_iter()
                .any(|value| aliases.iter().any(|alias| alias == &value))
        })
        .count();

    TagSummary {
        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: tags::Model) -> TagRecord {
    TagRecord {
        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<tags::Model> {
    let item = tags::Entity::find_by_id(id).one(&ctx.db).await?;
    item.ok_or_else(|| Error::NotFound)
}

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

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

#[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 = tags::Entity::find()
        .filter(tags::Column::Slug.eq(&slug))
        .one(&ctx.db)
        .await?;

    let item = if let Some(existing_tag) = existing {
        let mut item = existing_tag.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));
        item.update(&ctx.db).await?
    } else {
        tags::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_tag_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_tag_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/tags/")
        .add("/", get(list))
        .add("/", post(add))
        .add("{id}", get(get_one))
        .add("{id}", delete(remove))
        .add("{id}", put(update))
        .add("{id}", patch(update))
}
