use async_trait::async_trait;
use axum::{
    Router as AxumRouter,
    http::{HeaderName, Method, header},
};
use loco_rs::{
    Result,
    app::{AppContext, Hooks, Initializer},
    bgworker::{BackgroundWorker, Queue},
    boot::{BootResult, StartMode, create_app},
    config::Config,
    controller::AppRoutes,
    db::{self, truncate_table},
    environment::Environment,
    task::Tasks,
};
use migration::Migrator;
use sea_orm::{
    ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, QueryOrder, Set,
};
use std::{collections::BTreeSet, path::Path};
use tower_http::cors::CorsLayer;

#[allow(unused_imports)]
use crate::{
    controllers, initializers,
    models::_entities::{
        ai_chunks, categories, comments, friend_links, posts, reviews, site_settings, tags, users,
    },
    tasks,
    workers::{
        ai_reindex::AiReindexWorker, downloader::DownloadWorker,
        notification_delivery::NotificationDeliveryWorker,
    },
};

pub struct App;

fn normalized_origin(value: &str) -> Option<String> {
    let trimmed = value.trim().trim_end_matches('/').to_string();
    if trimmed.is_empty() {
        None
    } else {
        Some(trimmed)
    }
}

fn collect_cors_origins() -> Vec<String> {
    let mut origins = BTreeSet::new();

    for origin in [
        "http://127.0.0.1:4321",
        "http://127.0.0.1:4322",
        "http://localhost:4321",
        "http://localhost:4322",
    ] {
        origins.insert(origin.to_string());
    }

    for key in [
        "APP_BASE_URL",
        "ADMIN_API_BASE_URL",
        "ADMIN_FRONTEND_BASE_URL",
        "PUBLIC_API_BASE_URL",
        "PUBLIC_FRONTEND_BASE_URL",
        "TERMI_CORS_ALLOWED_ORIGINS",
    ] {
        if let Ok(value) = std::env::var(key) {
            for origin in value.split([',', ';', ' ']) {
                if let Some(origin) = normalized_origin(origin) {
                    origins.insert(origin);
                }
            }
        }
    }

    origins.into_iter().collect()
}

#[async_trait]
impl Hooks for App {
    fn app_name() -> &'static str {
        env!("CARGO_CRATE_NAME")
    }

    fn app_version() -> String {
        format!(
            "{} ({})",
            env!("CARGO_PKG_VERSION"),
            option_env!("BUILD_SHA")
                .or(option_env!("GITHUB_SHA"))
                .unwrap_or("dev")
        )
    }

    async fn boot(
        mode: StartMode,
        environment: &Environment,
        config: Config,
    ) -> Result<BootResult> {
        create_app::<Self, Migrator>(mode, environment, config).await
    }

    async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
        Ok(vec![Box::new(
            initializers::content_sync::ContentSyncInitializer,
        )])
    }

    fn routes(_ctx: &AppContext) -> AppRoutes {
        AppRoutes::with_default_routes() // controller routes below
            .add_route(controllers::health::routes())
            .add_route(controllers::admin_api::routes())
            .add_route(controllers::admin_taxonomy::routes())
            .add_route(controllers::admin_ops::routes())
            .add_route(controllers::review::routes())
            .add_route(controllers::category::routes())
            .add_route(controllers::friend_link::routes())
            .add_route(controllers::tag::routes())
            .add_route(controllers::comment::routes())
            .add_route(controllers::post::routes())
            .add_route(controllers::search::routes())
            .add_route(controllers::content_analytics::routes())
            .add_route(controllers::site_settings::routes())
            .add_route(controllers::ai::routes())
            .add_route(controllers::auth::routes())
            .add_route(controllers::subscription::routes())
    }
    async fn after_routes(router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {
        let allowed_origins = collect_cors_origins()
            .into_iter()
            .filter_map(|origin| origin.parse().ok())
            .collect::<Vec<_>>();
        let allowed_headers = [
            header::ACCEPT,
            header::ACCEPT_LANGUAGE,
            header::AUTHORIZATION,
            header::CONTENT_LANGUAGE,
            header::CONTENT_TYPE,
            header::COOKIE,
            header::ORIGIN,
            HeaderName::from_static("x-requested-with"),
        ];
        let cors = CorsLayer::new()
            .allow_origin(allowed_origins)
            .allow_methods([
                Method::GET,
                Method::POST,
                Method::PUT,
                Method::PATCH,
                Method::DELETE,
            ])
            .allow_headers(allowed_headers)
            .allow_credentials(true);

        Ok(router.layer(cors))
    }
    async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> {
        queue.register(AiReindexWorker::build(ctx)).await?;
        queue.register(DownloadWorker::build(ctx)).await?;
        queue
            .register(NotificationDeliveryWorker::build(ctx))
            .await?;
        Ok(())
    }

    #[allow(unused_variables)]
    fn register_tasks(tasks: &mut Tasks) {
        tasks.register(tasks::retry_deliveries::RetryDeliveries);
        tasks.register(tasks::send_weekly_digest::SendWeeklyDigest);
        tasks.register(tasks::send_monthly_digest::SendMonthlyDigest);
        // tasks-inject (do not remove)
    }
    async fn seed(ctx: &AppContext, base: &Path) -> Result<()> {
        // Seed users - use loco's default seed which handles duplicates
        let users_file = base.join("users.yaml");
        if users_file.exists() {
            if let Err(e) =
                db::seed::<users::ActiveModel>(&ctx.db, &users_file.display().to_string()).await
            {
                tracing::warn!("Users seed skipped or failed: {}", e);
            }
        }

        // Seed tags first (no foreign key dependencies) - use Unchanged to ignore conflicts
        if let Ok(seed_data) = std::fs::read_to_string(base.join("tags.yaml")) {
            let tags_data: Vec<serde_json::Value> =
                serde_yaml::from_str(&seed_data).unwrap_or_default();
            for tag in tags_data {
                let id = tag["id"].as_i64().unwrap_or(0) as i32;
                let name = tag["name"].as_str().unwrap_or("").to_string();
                let slug = tag["slug"].as_str().unwrap_or("").to_string();

                let existing = tags::Entity::find_by_id(id).one(&ctx.db).await?;
                if existing.is_none() {
                    let new_tag = tags::ActiveModel {
                        id: Set(id),
                        name: Set(Some(name)),
                        slug: Set(slug),
                        ..Default::default()
                    };
                    let _ = new_tag.insert(&ctx.db).await;
                }
            }
        }

        // Seed posts
        if let Ok(seed_data) = std::fs::read_to_string(base.join("posts.yaml")) {
            let posts_data: Vec<serde_json::Value> =
                serde_yaml::from_str(&seed_data).unwrap_or_default();
            for post in posts_data {
                let pid = post["pid"].as_i64().unwrap_or(0) as i32;
                let title = post["title"].as_str().unwrap_or("").to_string();
                let slug = post["slug"].as_str().unwrap_or("").to_string();
                let content = post["content"].as_str().unwrap_or("").to_string();
                let excerpt = post["excerpt"].as_str().unwrap_or("").to_string();
                let category = post["category"].as_str().unwrap_or("").to_string();
                let pinned = post["pinned"].as_bool().unwrap_or(false);
                let post_type = post["post_type"].as_str().unwrap_or("article").to_string();
                let tags_vec = post["tags"]
                    .as_array()
                    .map(|arr| {
                        arr.iter()
                            .filter_map(|v| v.as_str())
                            .map(|s| s.to_string())
                            .collect::<Vec<_>>()
                    })
                    .unwrap_or_default();
                let tags_json = if tags_vec.is_empty() {
                    None
                } else {
                    Some(serde_json::json!(tags_vec))
                };

                let existing = posts::Entity::find_by_id(pid).one(&ctx.db).await?;
                let has_existing = existing.is_some();
                let mut post_model = existing
                    .map(|model| model.into_active_model())
                    .unwrap_or_else(|| posts::ActiveModel {
                        id: Set(pid),
                        ..Default::default()
                    });

                post_model.title = Set(Some(title));
                post_model.slug = Set(slug);
                post_model.content = Set(Some(content));
                post_model.description = Set(Some(excerpt));
                post_model.category = Set(Some(category));
                post_model.tags = Set(tags_json);
                post_model.pinned = Set(Some(pinned));
                post_model.post_type = Set(Some(post_type));

                if has_existing {
                    let _ = post_model.update(&ctx.db).await;
                } else {
                    let _ = post_model.insert(&ctx.db).await;
                }
            }
        }

        // Seed comments
        if let Ok(seed_data) = std::fs::read_to_string(base.join("comments.yaml")) {
            let comments_data: Vec<serde_json::Value> =
                serde_yaml::from_str(&seed_data).unwrap_or_default();
            for comment in comments_data {
                let id = comment["id"].as_i64().unwrap_or(0) as i32;
                let pid = comment["pid"].as_i64().unwrap_or(0) as i32;
                let author = comment["author"].as_str().unwrap_or("").to_string();
                let email = comment["email"].as_str().unwrap_or("").to_string();
                let content_text = comment["content"].as_str().unwrap_or("").to_string();
                let approved = comment["approved"].as_bool().unwrap_or(false);
                let post_slug = posts::Entity::find_by_id(pid)
                    .one(&ctx.db)
                    .await?
                    .map(|post| post.slug);

                let existing = comments::Entity::find_by_id(id).one(&ctx.db).await?;
                let has_existing = existing.is_some();
                let mut comment_model = existing
                    .map(|model| model.into_active_model())
                    .unwrap_or_else(|| comments::ActiveModel {
                        id: Set(id),
                        ..Default::default()
                    });

                comment_model.author = Set(Some(author));
                comment_model.email = Set(Some(email));
                comment_model.content = Set(Some(content_text));
                comment_model.approved = Set(Some(approved));
                comment_model.post_slug = Set(post_slug);

                if has_existing {
                    let _ = comment_model.update(&ctx.db).await;
                } else {
                    let _ = comment_model.insert(&ctx.db).await;
                }
            }
        }

        // Seed friend links
        if let Ok(seed_data) = std::fs::read_to_string(base.join("friend_links.yaml")) {
            let links_data: Vec<serde_json::Value> =
                serde_yaml::from_str(&seed_data).unwrap_or_default();
            for link in links_data {
                let site_name = link["site_name"].as_str().unwrap_or("").to_string();
                let site_url = link["site_url"].as_str().unwrap_or("").to_string();
                let avatar_url = link["avatar_url"].as_str().map(|s: &str| s.to_string());
                let description = link["description"].as_str().unwrap_or("").to_string();
                let category = link["category"].as_str().unwrap_or("").to_string();
                let status = link["status"].as_str().unwrap_or("pending").to_string();

                let existing = friend_links::Entity::find()
                    .filter(friend_links::Column::SiteUrl.eq(&site_url))
                    .one(&ctx.db)
                    .await?;
                if existing.is_none() {
                    let new_link = friend_links::ActiveModel {
                        site_name: Set(Some(site_name)),
                        site_url: Set(site_url),
                        avatar_url: Set(avatar_url),
                        description: Set(Some(description)),
                        category: Set(Some(category)),
                        status: Set(Some(status)),
                        ..Default::default()
                    };
                    let _ = new_link.insert(&ctx.db).await;
                }
            }
        }

        // Seed site settings
        if let Ok(seed_data) = std::fs::read_to_string(base.join("site_settings.yaml")) {
            let settings_data: Vec<serde_json::Value> =
                serde_yaml::from_str(&seed_data).unwrap_or_default();
            if let Some(settings) = settings_data.first() {
                let existing = site_settings::Entity::find()
                    .order_by_asc(site_settings::Column::Id)
                    .one(&ctx.db)
                    .await?;

                if existing.is_none() {
                    let comment_verification_mode = settings["comment_verification_mode"]
                        .as_str()
                        .map(ToString::to_string);
                    let subscription_verification_mode = settings["subscription_verification_mode"]
                        .as_str()
                        .map(ToString::to_string);
                    let comment_turnstile_enabled = settings["comment_turnstile_enabled"]
                        .as_bool()
                        .or(comment_verification_mode
                            .as_deref()
                            .map(|value| value.eq_ignore_ascii_case("turnstile")));
                    let subscription_turnstile_enabled = settings["subscription_turnstile_enabled"]
                        .as_bool()
                        .or(subscription_verification_mode
                            .as_deref()
                            .map(|value| value.eq_ignore_ascii_case("turnstile")));
                    let tech_stack = settings["tech_stack"]
                        .as_array()
                        .map(|items| {
                            items
                                .iter()
                                .filter_map(|item| item.as_str())
                                .map(ToString::to_string)
                                .collect::<Vec<_>>()
                        })
                        .filter(|items| !items.is_empty())
                        .map(|items| serde_json::json!(items));
                    let music_playlist = settings["music_playlist"]
                        .as_array()
                        .map(|items| {
                            items
                                .iter()
                                .filter_map(|item| {
                                    let title = item["title"].as_str()?.trim();
                                    let url = item["url"].as_str()?.trim();
                                    if title.is_empty() || url.is_empty() {
                                        None
                                    } else {
                                        Some(serde_json::json!({
                                            "title": title,
                                            "url": url,
                                        }))
                                    }
                                })
                                .collect::<Vec<_>>()
                        })
                        .filter(|items| !items.is_empty())
                        .map(serde_json::Value::Array);
                    let music_enabled = settings["music_enabled"].as_bool().or(Some(true));
                    let maintenance_mode_enabled = settings["maintenance_mode_enabled"]
                        .as_bool()
                        .or(Some(false));
                    let maintenance_access_code = settings["maintenance_access_code"]
                        .as_str()
                        .and_then(|value| {
                            let trimmed = value.trim();
                            if trimmed.is_empty() {
                                None
                            } else {
                                Some(trimmed.to_string())
                            }
                        });
                    let seo_favicon_url = settings["seo_favicon_url"].as_str().and_then(|value| {
                        let trimmed = value.trim();
                        if trimmed.is_empty() {
                            None
                        } else {
                            Some(trimmed.to_string())
                        }
                    });

                    let item = site_settings::ActiveModel {
                        id: Set(settings["id"].as_i64().unwrap_or(1) as i32),
                        site_name: Set(settings["site_name"].as_str().map(ToString::to_string)),
                        site_short_name: Set(settings["site_short_name"]
                            .as_str()
                            .map(ToString::to_string)),
                        site_url: Set(settings["site_url"].as_str().map(ToString::to_string)),
                        site_title: Set(settings["site_title"].as_str().map(ToString::to_string)),
                        site_description: Set(settings["site_description"]
                            .as_str()
                            .map(ToString::to_string)),
                        hero_title: Set(settings["hero_title"].as_str().map(ToString::to_string)),
                        hero_subtitle: Set(settings["hero_subtitle"]
                            .as_str()
                            .map(ToString::to_string)),
                        owner_name: Set(settings["owner_name"].as_str().map(ToString::to_string)),
                        owner_title: Set(settings["owner_title"].as_str().map(ToString::to_string)),
                        owner_bio: Set(settings["owner_bio"].as_str().map(ToString::to_string)),
                        owner_avatar_url: Set(settings["owner_avatar_url"].as_str().and_then(
                            |value| {
                                let trimmed = value.trim();
                                if trimmed.is_empty() {
                                    None
                                } else {
                                    Some(trimmed.to_string())
                                }
                            },
                        )),
                        social_github: Set(settings["social_github"]
                            .as_str()
                            .map(ToString::to_string)),
                        social_twitter: Set(settings["social_twitter"]
                            .as_str()
                            .map(ToString::to_string)),
                        social_email: Set(settings["social_email"]
                            .as_str()
                            .map(ToString::to_string)),
                        location: Set(settings["location"].as_str().map(ToString::to_string)),
                        tech_stack: Set(tech_stack),
                        music_playlist: Set(music_playlist),
                        music_enabled: Set(music_enabled),
                        maintenance_mode_enabled: Set(maintenance_mode_enabled),
                        maintenance_access_code: Set(maintenance_access_code),
                        seo_favicon_url: Set(seo_favicon_url),
                        ai_enabled: Set(settings["ai_enabled"].as_bool()),
                        paragraph_comments_enabled: Set(settings["paragraph_comments_enabled"]
                            .as_bool()
                            .or(Some(true))),
                        comment_verification_mode: Set(comment_verification_mode),
                        comment_turnstile_enabled: Set(comment_turnstile_enabled),
                        subscription_verification_mode: Set(subscription_verification_mode),
                        subscription_turnstile_enabled: Set(subscription_turnstile_enabled),
                        turnstile_site_key: Set(settings["turnstile_site_key"]
                            .as_str()
                            .map(ToString::to_string)),
                        turnstile_secret_key: Set(settings["turnstile_secret_key"]
                            .as_str()
                            .map(ToString::to_string)),
                        ai_provider: Set(settings["ai_provider"].as_str().map(ToString::to_string)),
                        ai_api_base: Set(settings["ai_api_base"].as_str().map(ToString::to_string)),
                        ai_api_key: Set(settings["ai_api_key"].as_str().map(ToString::to_string)),
                        ai_chat_model: Set(settings["ai_chat_model"]
                            .as_str()
                            .map(ToString::to_string)),
                        ai_embedding_model: Set(settings["ai_embedding_model"]
                            .as_str()
                            .map(ToString::to_string)),
                        ai_system_prompt: Set(settings["ai_system_prompt"]
                            .as_str()
                            .map(ToString::to_string)),
                        ai_top_k: Set(settings["ai_top_k"].as_i64().map(|value| value as i32)),
                        ai_chunk_size: Set(settings["ai_chunk_size"]
                            .as_i64()
                            .map(|value| value as i32)),
                        ..Default::default()
                    };
                    let _ = item.insert(&ctx.db).await;
                }
            }
        }

        // Seed reviews
        if let Ok(seed_data) = std::fs::read_to_string(base.join("reviews.yaml")) {
            let reviews_data: Vec<serde_json::Value> =
                serde_yaml::from_str(&seed_data).unwrap_or_default();
            for review in reviews_data {
                let title = review["title"].as_str().unwrap_or("").to_string();
                let review_type = review["review_type"].as_str().unwrap_or("").to_string();
                let rating = review["rating"].as_i64().unwrap_or(0) as i32;
                let review_date = review["review_date"].as_str().unwrap_or("").to_string();
                let status = review["status"].as_str().unwrap_or("completed").to_string();
                let description = review["description"].as_str().unwrap_or("").to_string();
                let cover = review["cover"].as_str().unwrap_or("📝").to_string();
                let link_url = review["link_url"]
                    .as_str()
                    .map(str::trim)
                    .filter(|value| !value.is_empty())
                    .map(ToString::to_string);
                let tags_vec = review["tags"]
                    .as_array()
                    .map(|arr| {
                        arr.iter()
                            .filter_map(|v| v.as_str())
                            .map(|s| s.to_string())
                            .collect::<Vec<_>>()
                    })
                    .unwrap_or_default();

                let existing = reviews::Entity::find()
                    .filter(reviews::Column::Title.eq(&title))
                    .one(&ctx.db)
                    .await?;
                if existing.is_none() {
                    let new_review = reviews::ActiveModel {
                        title: Set(Some(title)),
                        review_type: Set(Some(review_type)),
                        rating: Set(Some(rating)),
                        review_date: Set(Some(review_date)),
                        status: Set(Some(status)),
                        description: Set(Some(description)),
                        cover: Set(Some(cover)),
                        link_url: Set(link_url),
                        tags: Set(Some(serde_json::to_string(&tags_vec).unwrap_or_default())),
                        ..Default::default()
                    };
                    let _ = new_review.insert(&ctx.db).await;
                }
            }
        }

        Ok(())
    }

    async fn truncate(ctx: &AppContext) -> Result<()> {
        truncate_table(&ctx.db, ai_chunks::Entity).await?;
        truncate_table(&ctx.db, users::Entity).await?;
        Ok(())
    }
}
