use async_trait::async_trait;
use loco_rs::{
    app::{AppContext, Hooks, Initializer},
    bgworker::{BackgroundWorker, Queue},
    boot::{create_app, BootResult, StartMode},
    config::Config,
    controller::AppRoutes,
    db::{self, truncate_table},
    environment::Environment,
    storage::{self, Storage},
    task::Tasks,
    Result,
};
use migration::Migrator;
use std::{path::Path, sync::Arc};

#[allow(unused_imports)]
use crate::{
    controllers::{
        account, admin_categories, admin_currencies, admin_customers, admin_dashboard,
        admin_discount_profiles, admin_form, admin_orders, admin_payments, admin_products, admin_shipping,
        auth, auth_pages, cart, checkout, currency, home, i18n, media, oauth2,
        pages, shop,
    },
    initializers,
    models::_entities::users,
    tasks,
    workers::downloader::DownloadWorker,
};

pub struct App;
#[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 load_config(environment: &Environment) -> Result<Config> {
        dotenvy::dotenv().ok();
        environment.load()
    }

    /// Attach the Casbin authorization layer on top of all routes. Order
    /// matters: `inject_subject` is the outermost layer so it runs first and
    /// stamps the JWT-derived role onto the request before the inner
    /// `CasbinAxumLayer` enforces the policy. See `shared::rbac`.
    async fn after_routes(router: axum::Router, ctx: &AppContext) -> Result<axum::Router> {
        let casbin = crate::shared::rbac::layer().await?;
        Ok(router
            // Stamp a long-lived Cache-Control on /static/* responses only.
            .layer(axum::middleware::from_fn(
                crate::shared::cache::static_cache,
            ))
            .layer(casbin)
            .layer(axum::middleware::from_fn_with_state(
                ctx.clone(),
                crate::shared::rbac::inject_subject,
            ))
            // CSRF runs outermost so it validates the double-submit token before
            // any handler sees the request and stamps the cookie on safe ones.
            .layer(axum::middleware::from_fn_with_state(
                ctx.clone(),
                crate::shared::csrf::protect,
            )))
    }

    async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
        Ok(vec![
            Box::new(initializers::view_engine::ViewEngineInitializer),
            Box::new(initializers::admin_seeder::AdminSeeder),
            Box::new(initializers::shipping_seeder::ShippingSeeder),
            Box::new(initializers::payment_seeder::PaymentSeeder),
            Box::new(initializers::currency_seeder::CurrencySeeder),
            Box::new(initializers::oauth2::OAuth2StoreInitializer),
            Box::new(initializers::oauth2_session::OAuth2SessionInitializer),
        ])
    }

    fn routes(_ctx: &AppContext) -> AppRoutes {
        AppRoutes::with_default_routes() // feature routes below
            // public
            .add_route(home::routes())
            .add_route(shop::routes())
            .add_route(cart::routes())
            .add_route(checkout::routes())
            .add_route(currency::routes())
            .add_route(pages::routes())
            // cross-cutting
            .add_route(auth::routes())
            .add_route(auth_pages::routes())
            .add_route(account::routes())
            .add_route(oauth2::routes())
            .add_route(i18n::routes())
            .add_route(media::routes())
            // admin
            .add_route(admin_dashboard::routes())
            .add_route(admin_products::routes())
            .add_route(admin_discount_profiles::routes())
            .add_route(admin_categories::routes())
            .add_route(admin_orders::routes())
            .add_route(admin_payments::routes())
            .add_route(admin_customers::routes())
            .add_route(admin_shipping::routes())
            .add_route(admin_currencies::routes())
    }

    async fn after_context(ctx: AppContext) -> Result<AppContext> {
        let upload_root = media::uploads_root(&ctx.config)?;
        tokio::fs::create_dir_all(upload_root.join(media::IMAGE_STORAGE_DIR)).await?;

        let driver = storage::drivers::local::new_with_prefix(&upload_root)?;
        Ok(AppContext {
            storage: Arc::new(Storage::single(driver)),
            ..ctx
        })
    }

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

    #[allow(unused_variables)]
    fn register_tasks(tasks: &mut Tasks) {
        // tasks-inject (do not remove)
    }
    async fn truncate(ctx: &AppContext) -> Result<()> {
        truncate_table(&ctx.db, users::Entity).await?;
        Ok(())
    }
    async fn seed(ctx: &AppContext, base: &Path) -> Result<()> {
        db::seed::<users::ActiveModel>(&ctx.db, &base.join("users.yaml").display().to_string())
            .await?;
        crate::seed::seed_catalog(ctx).await?;
        Ok(())
    }
}
