use crate::{controllers::admin, models::_entities::blog_articles};
use chrono::Utc;
use loco_rs::prelude::*;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[derive(Debug, Deserialize)]
struct ArticleParams {
    title: String,
    content: String,
    excerpt: Option<String>,
    published: Option<bool>,
    featured_image_id: Option<String>,
}

#[derive(Debug, Serialize)]
struct ArticleResponse {
    id: Uuid,
    title: String,
    slug: String,
    content: String,
    excerpt: Option<String>,
    published: bool,
    author_id: i32,
    featured_image_id: Option<String>,
    view_count: i32,
    created_at: chrono::DateTime<chrono::FixedOffset>,
    updated_at: chrono::DateTime<chrono::FixedOffset>,
    published_at: Option<chrono::DateTime<chrono::FixedOffset>>,
}

#[derive(Debug, Serialize)]
struct ArticleListResponse {
    articles: Vec<ArticleResponse>,
}

impl From<blog_articles::Model> for ArticleResponse {
    fn from(article: blog_articles::Model) -> Self {
        Self {
            id: article.id,
            title: article.title,
            slug: article.slug,
            content: article.content,
            excerpt: article.excerpt,
            published: article.published,
            author_id: article.author_id,
            featured_image_id: article.featured_image_id,
            view_count: article.view_count,
            created_at: article.created_at,
            updated_at: article.updated_at,
            published_at: article.published_at,
        }
    }
}

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

    for ch in title.chars().flat_map(char::to_lowercase) {
        if ch.is_ascii_alphanumeric() {
            slug.push(ch);
            last_was_dash = false;
        } else if !last_was_dash && !slug.is_empty() {
            slug.push('-');
            last_was_dash = true;
        }
    }

    let slug = slug.trim_matches('-').to_string();
    if slug.is_empty() {
        Uuid::new_v4().to_string()
    } else {
        slug
    }
}

fn published_at_for(published: bool) -> Option<chrono::DateTime<chrono::FixedOffset>> {
    published.then(|| Utc::now().into())
}

async fn find_article_by_id(ctx: &AppContext, id: Uuid) -> Result<blog_articles::Model> {
    blog_articles::Entity::find_by_id(id)
        .one(&ctx.db)
        .await?
        .ok_or_else(|| Error::NotFound)
}

#[debug_handler]
async fn public_index(State(ctx): State<AppContext>) -> Result<Response> {
    let articles = blog_articles::Entity::find()
        .filter(blog_articles::Column::Published.eq(true))
        .order_by_desc(blog_articles::Column::PublishedAt)
        .all(&ctx.db)
        .await?
        .into_iter()
        .map(ArticleResponse::from)
        .collect();

    format::json(ArticleListResponse { articles })
}

#[debug_handler]
async fn public_show(Path(slug): Path<String>, State(ctx): State<AppContext>) -> Result<Response> {
    let article = blog_articles::Entity::find()
        .filter(blog_articles::Column::Slug.eq(slug))
        .filter(blog_articles::Column::Published.eq(true))
        .one(&ctx.db)
        .await?
        .ok_or_else(|| Error::NotFound)?;

    let mut active = article.into_active_model();
    let next_count = active.view_count.as_ref().to_owned() + 1;
    active.view_count = Set(next_count);
    let article = active.update(&ctx.db).await?;

    format::json(ArticleResponse::from(article))
}

#[debug_handler]
async fn admin_index(auth: auth::JWT, State(ctx): State<AppContext>) -> Result<Response> {
    admin::current_admin(auth, &ctx).await?;

    let articles = blog_articles::Entity::find()
        .order_by_desc(blog_articles::Column::CreatedAt)
        .all(&ctx.db)
        .await?
        .into_iter()
        .map(ArticleResponse::from)
        .collect();

    format::json(ArticleListResponse { articles })
}

#[debug_handler]
async fn admin_create(
    auth: auth::JWT,
    State(ctx): State<AppContext>,
    Json(params): Json<ArticleParams>,
) -> Result<Response> {
    let admin_user = admin::current_admin(auth, &ctx).await?;
    let published = params.published.unwrap_or(false);

    let article = blog_articles::ActiveModel {
        id: Set(Uuid::new_v4()),
        title: Set(params.title.clone()),
        slug: Set(slugify(&params.title)),
        content: Set(params.content),
        excerpt: Set(params.excerpt),
        published: Set(published),
        author_id: Set(admin_user.id),
        featured_image_id: Set(params.featured_image_id),
        view_count: Set(0),
        published_at: Set(published_at_for(published)),
        ..Default::default()
    }
    .insert(&ctx.db)
    .await?;

    format::json(ArticleResponse::from(article))
}

#[debug_handler]
async fn admin_update(
    auth: auth::JWT,
    Path(id): Path<Uuid>,
    State(ctx): State<AppContext>,
    Json(params): Json<ArticleParams>,
) -> Result<Response> {
    admin::current_admin(auth, &ctx).await?;

    let existing = find_article_by_id(&ctx, id).await?;
    let was_published = existing.published;
    let published = params.published.unwrap_or(was_published);

    let mut article = existing.into_active_model();
    article.title = Set(params.title.clone());
    article.slug = Set(slugify(&params.title));
    article.content = Set(params.content);
    article.excerpt = Set(params.excerpt);
    article.published = Set(published);
    article.featured_image_id = Set(params.featured_image_id);
    if published && !was_published {
        article.published_at = Set(published_at_for(true));
    } else if !published {
        article.published_at = Set(None);
    }

    let article = article.update(&ctx.db).await?;
    format::json(ArticleResponse::from(article))
}

#[debug_handler]
async fn admin_delete(
    auth: auth::JWT,
    Path(id): Path<Uuid>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    admin::current_admin(auth, &ctx).await?;
    let article = find_article_by_id(&ctx, id).await?;
    article.delete(&ctx.db).await?;
    format::json(())
}

#[debug_handler]
async fn admin_publish(
    auth: auth::JWT,
    Path(id): Path<Uuid>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    admin::current_admin(auth, &ctx).await?;
    let mut article = find_article_by_id(&ctx, id).await?.into_active_model();
    article.published = Set(true);
    article.published_at = Set(published_at_for(true));
    let article = article.update(&ctx.db).await?;
    format::json(ArticleResponse::from(article))
}

#[debug_handler]
async fn admin_unpublish(
    auth: auth::JWT,
    Path(id): Path<Uuid>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    admin::current_admin(auth, &ctx).await?;
    let mut article = find_article_by_id(&ctx, id).await?.into_active_model();
    article.published = Set(false);
    article.published_at = Set(None);
    let article = article.update(&ctx.db).await?;
    format::json(ArticleResponse::from(article))
}

pub fn routes() -> Routes {
    Routes::new()
        .prefix("/api")
        .add("/blog", get(public_index))
        .add("/blog/{slug}", get(public_show))
        .add("/admin/blog/articles", get(admin_index))
        .add("/admin/blog/articles", post(admin_create))
        .add("/admin/blog/articles/{id}", put(admin_update))
        .add("/admin/blog/articles/{id}", delete(admin_delete))
        .add("/admin/blog/articles/{id}/publish", post(admin_publish))
        .add("/admin/blog/articles/{id}/unpublish", post(admin_unpublish))
}
