use crate::{
    controllers::{admin, auth as auth_controller, i18n::current_lang},
    models::{
        _entities::{audio_albums, audio_tracks, blog_articles, site_pages},
        users::{self, LoginParams},
    },
};
use axum_extra::extract::cookie::CookieJar;
use chrono::Utc;
use loco_rs::prelude::*;
use sea_orm::{
    sea_query::Expr, ActiveModelTrait, ColumnTrait, EntityTrait, Order, QueryFilter, QueryOrder,
    QuerySelect, Set,
};
use serde::Deserialize;
use serde_json::json;
use uuid::Uuid;

const ABOUT_SLUG: &str = "about";

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

#[derive(Debug, Deserialize)]
struct AboutForm {
    title: String,
    content: String,
}

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())
}

fn is_checked(value: &Option<String>) -> bool {
    value
        .as_deref()
        .is_some_and(|value| value == "on" || value == "true")
}

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

async fn about_page(ctx: &AppContext) -> Result<site_pages::Model> {
    site_pages::Entity::find()
        .filter(site_pages::Column::Slug.eq(ABOUT_SLUG))
        .one(&ctx.db)
        .await?
        .ok_or_else(|| Error::NotFound)
}

async fn 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)
}

async fn logged_in_admin(ctx: &AppContext, jar: &CookieJar) -> bool {
    let Some(cookie) = jar.get(auth_controller::AUTH_COOKIE) else {
        return false;
    };
    let Ok(jwt_config) = ctx.config.get_jwt_config() else {
        return false;
    };
    let Ok(claims) = loco_rs::auth::jwt::JWT::new(&jwt_config.secret).validate(cookie.value())
    else {
        return false;
    };
    let Ok(user) = users::Model::find_by_pid(&ctx.db, &claims.claims.pid).await else {
        return false;
    };

    admin::is_admin(ctx, &user)
}

#[debug_handler]
async fn home(
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    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)
        .limit(5)
        .all(&ctx.db)
        .await?;

    // A random published song to suggest on the landing page.
    let featured_track = audio_tracks::Entity::find()
        .filter(audio_tracks::Column::Published.eq(true))
        .order_by(Expr::cust("RANDOM()"), Order::Asc)
        .one(&ctx.db)
        .await?;

    // A random published album, never the one the suggested song belongs to.
    let mut album_query =
        audio_albums::Entity::find().filter(audio_albums::Column::Published.eq(true));
    if let Some(album_id) = featured_track.as_ref().and_then(|track| track.album_id) {
        album_query = album_query.filter(audio_albums::Column::Id.ne(album_id));
    }
    let featured_album = album_query
        .order_by(Expr::cust("RANDOM()"), Order::Asc)
        .one(&ctx.db)
        .await?;

    format::view(
        &v,
        "home/index.html",
        json!({
            "articles": articles,
            "featured_track": featured_track,
            "featured_album": featured_album,
            "logged_in_admin": logged_in_admin(&ctx, &jar).await,
            "lang": current_lang(&jar),
        }),
    )
}

#[debug_handler]
async fn about(
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    format::view(
        &v,
        "pages/about.html",
        json!({
            "page": about_page(&ctx).await?,
            "logged_in_admin": logged_in_admin(&ctx, &jar).await,
            "lang": current_lang(&jar),
        }),
    )
}

#[debug_handler]
async fn blog_index(
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    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?;

    format::view(
        &v,
        "blog/index.html",
        json!({
            "articles": articles,
            "logged_in_admin": logged_in_admin(&ctx, &jar).await,
            "lang": current_lang(&jar),
        }),
    )
}

#[debug_handler]
async fn blog_show(
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    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::view(
        &v,
        "blog/show.html",
        json!({
            "article": article,
            "logged_in_admin": logged_in_admin(&ctx, &jar).await,
            "lang": current_lang(&jar),
        }),
    )
}

#[debug_handler]
async fn admin_login_page(
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    if logged_in_admin(&ctx, &jar).await {
        return format::redirect("/admin/dashboard");
    }

    format::view(
        &v,
        "admin/login.html",
        json!({
            "error": null,
            "logged_in_admin": false,
            "lang": current_lang(&jar),
        }),
    )
}

#[debug_handler]
async fn admin_login(
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    State(ctx): State<AppContext>,
    Form(params): Form<LoginParams>,
) -> Result<Response> {
    let Ok(user) = users::Model::find_by_email(&ctx.db, &params.email).await else {
        return format::view(
            &v,
            "admin/login.html",
            json!({
                "error": "Invalid credentials",
                "logged_in_admin": false,
                "lang": current_lang(&jar),
            }),
        );
    };

    if !user.verify_password(&params.password) || !admin::is_admin(&ctx, &user) {
        return format::view(
            &v,
            "admin/login.html",
            json!({
                "error": "Invalid credentials",
                "logged_in_admin": false,
                "lang": current_lang(&jar),
            }),
        );
    }

    let jwt_secret = ctx.config.get_jwt_config()?;
    let token = user
        .generate_jwt(&jwt_secret.secret, jwt_secret.expiration)
        .or_else(|_| unauthorized("unauthorized!"))?;

    format::render()
        .cookies(&[auth_controller::auth_cookie(&token, jwt_secret.expiration)])?
        .redirect("/admin/dashboard")
}

#[debug_handler]
async fn admin_logout() -> Result<Response> {
    format::render()
        .cookies(&[auth_controller::clear_auth_cookie()])?
        .redirect("/admin/login")
}

#[debug_handler]
async fn admin_home(
    auth: auth::JWT,
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    let admin_user = admin::current_admin(auth, &ctx).await?;
    format::view(
        &v,
        "admin/index.html",
        json!({ "admin": admin_user, "lang": current_lang(&jar) }),
    )
}

#[debug_handler]
async fn admin_about(
    auth: auth::JWT,
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    admin::current_admin(auth, &ctx).await?;
    format::view(
        &v,
        "admin/about.html",
        json!({ "page": about_page(&ctx).await?, "lang": current_lang(&jar) }),
    )
}

#[debug_handler]
async fn admin_about_update(
    auth: auth::JWT,
    State(ctx): State<AppContext>,
    Form(params): Form<AboutForm>,
) -> Result<Response> {
    admin::current_admin(auth, &ctx).await?;
    let mut page = about_page(&ctx).await?.into_active_model();
    page.title = Set(params.title);
    page.content = Set(params.content);
    page.update(&ctx.db).await?;
    format::redirect("/admin/about")
}

#[debug_handler]
async fn admin_articles(
    auth: auth::JWT,
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    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?;
    format::view(
        &v,
        "admin/blog/index.html",
        json!({ "articles": articles, "lang": current_lang(&jar) }),
    )
}

#[debug_handler]
async fn admin_article_new(
    auth: auth::JWT,
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    admin::current_admin(auth, &ctx).await?;
    format::view(
        &v,
        "admin/blog/new.html",
        json!({ "lang": current_lang(&jar) }),
    )
}

#[debug_handler]
async fn admin_article_create(
    auth: auth::JWT,
    State(ctx): State<AppContext>,
    Form(params): Form<ArticleForm>,
) -> Result<Response> {
    let admin_user = admin::current_admin(auth, &ctx).await?;
    let published = is_checked(&params.published);

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

    format::redirect("/admin/blog/articles")
}

#[debug_handler]
async fn admin_article_edit(
    auth: auth::JWT,
    jar: CookieJar,
    ViewEngine(v): ViewEngine<TeraView>,
    Path(id): Path<Uuid>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    admin::current_admin(auth, &ctx).await?;
    format::view(
        &v,
        "admin/blog/edit.html",
        json!({ "article": article_by_id(&ctx, id).await?, "lang": current_lang(&jar) }),
    )
}

#[debug_handler]
async fn admin_article_update(
    auth: auth::JWT,
    Path(id): Path<Uuid>,
    State(ctx): State<AppContext>,
    Form(params): Form<ArticleForm>,
) -> Result<Response> {
    admin::current_admin(auth, &ctx).await?;
    let existing = article_by_id(&ctx, id).await?;
    let was_published = existing.published;
    let published = is_checked(&params.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(normalize_empty(params.excerpt));
    article.published = Set(published);
    article.featured_image_id = Set(normalize_empty(params.featured_image_id));
    if published && !was_published {
        article.published_at = Set(published_at_for(true));
    } else if !published {
        article.published_at = Set(None);
    }
    article.update(&ctx.db).await?;

    format::redirect("/admin/blog/articles")
}

#[debug_handler]
async fn admin_article_delete(
    auth: auth::JWT,
    Path(id): Path<Uuid>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    admin::current_admin(auth, &ctx).await?;
    article_by_id(&ctx, id).await?.delete(&ctx.db).await?;
    format::redirect("/admin/blog/articles")
}

pub fn routes() -> Routes {
    Routes::new()
        .add("/", get(home))
        .add("/about", get(about))
        .add("/blog", get(blog_index))
        .add("/blog/{slug}", get(blog_show))
        .add("/admin/login", get(admin_login_page))
        .add("/admin/login", post(admin_login))
        .add("/admin/logout", post(admin_logout))
        .add("/admin", get(admin_login_page))
        .add("/admin/dashboard", get(admin_home))
        .add("/admin/about", get(admin_about))
        .add("/admin/about", post(admin_about_update))
        .add("/admin/blog/articles", get(admin_articles))
        .add("/admin/blog/articles/new", get(admin_article_new))
        .add("/admin/blog/articles", post(admin_article_create))
        .add("/admin/blog/articles/{id}/edit", get(admin_article_edit))
        .add("/admin/blog/articles/{id}", post(admin_article_update))
        .add(
            "/admin/blog/articles/{id}/delete",
            post(admin_article_delete),
        )
}
