use std::num::NonZeroU32;

use axum::{Router, routing};
use axum_governor::{GovernorConfigBuilder, GovernorLayer, Quota, extractor::PeerIp};
use tower_http::{
    cors::CorsLayer,
    services::{ServeDir, ServeFile},
    trace::TraceLayer,
};

use crate::{handlers, state::AppState};

pub fn build_router(state: AppState, ap_router: Router) -> Router {
    let rate_limit = state.app_ctx.config.rate_limit;

    let ap_cfg = GovernorConfigBuilder::default()
        .with_extractor(PeerIp::default())
        .expect_connect_info()
        .quota_default(per_minute(rate_limit / 2))
        .finish()
        .unwrap();
    let ap_router = ap_router.layer(GovernorLayer::new(ap_cfg));

    Router::new()
        .route("/health", routing::get(health_handler))
        .merge(html_routes(rate_limit))
        .merge(api_routes(rate_limit))
        .nest_service("/static", ServeDir::new("static"))
        .nest_service(
            "/app",
            ServeDir::new("spa/dist").fallback(ServeFile::new("spa/dist/index.html")),
        )
        .layer(TraceLayer::new_for_http())
        .with_state(state)
        .merge(ap_router)
}

async fn health_handler() -> axum::Json<serde_json::Value> {
    axum::Json(serde_json::json!({ "status": "ok" }))
}

fn per_minute(n: u64) -> Quota {
    let n = NonZeroU32::new(n.clamp(1, u32::MAX as u64) as u32).unwrap();
    Quota::requests_per_minute(n)
}

fn html_routes(rate_limit: u64) -> Router<AppState> {
    let auth = Router::new()
        .route(
            "/login",
            routing::get(handlers::auth::get_login_page).post(handlers::auth::post_login),
        )
        .route("/logout", routing::get(handlers::auth::get_logout))
        .route(
            "/register",
            routing::get(handlers::auth::get_register_page).post(handlers::auth::post_register),
        )
        .layer({
            let cfg = GovernorConfigBuilder::default()
                .with_extractor(PeerIp::default())
                .expect_connect_info()
                .quota_default(per_minute(rate_limit))
                .finish()
                .unwrap();
            GovernorLayer::new(cfg)
        });

    let base = Router::new()
        .route("/", routing::get(handlers::diary::get_activity_feed_html))
        .route("/users", routing::get(handlers::users::get_users_list))
        .route(
            "/u/{username}",
            routing::get(handlers::users::get_user_by_username),
        )
        .route(
            "/users/{id}",
            routing::get(handlers::users::get_user_profile_html),
        )
        .route(
            "/movies/{movie_id}",
            routing::get(handlers::movies::get_movie_detail_html),
        )
        .merge(auth)
        .route(
            "/reviews/new",
            routing::get(handlers::diary::get_new_review_page),
        )
        .route("/reviews", routing::post(handlers::diary::post_review_html))
        .route(
            "/reviews/{id}/delete",
            routing::post(handlers::diary::post_delete_review_html),
        )
        .route("/images/{*key}", routing::get(handlers::images::get_image))
        .route(
            "/posters/{path}",
            routing::get(
                |axum::extract::Path(p): axum::extract::Path<String>| async move {
                    axum::response::Redirect::permanent(&format!("/images/{}", p))
                },
            ),
        )
        .route(
            "/diary/export",
            routing::get(handlers::diary::get_export_html),
        )
        .route("/import", routing::get(handlers::import::get_import_page))
        .route(
            "/import/upload",
            routing::post(handlers::import::post_upload),
        )
        .route(
            "/import/{id}/mapping",
            routing::get(handlers::import::get_mapping_page).post(handlers::import::post_mapping),
        )
        .route(
            "/import/{id}/preview",
            routing::get(handlers::import::get_preview_page),
        )
        .route(
            "/import/{id}/confirm",
            routing::post(handlers::import::post_confirm),
        )
        .route(
            "/import/done",
            routing::get(handlers::import::get_import_done),
        )
        .route(
            "/import/profiles/{profile_id}/delete",
            routing::post(handlers::import::post_delete_profile),
        )
        .route("/feed.rss", routing::get(handlers::rss::get_feed))
        .route(
            "/users/{id}/feed.rss",
            routing::get(handlers::rss::get_user_feed),
        )
        .route(
            "/settings/profile",
            routing::get(handlers::users::get_profile_settings)
                .post(handlers::users::post_profile_settings),
        )
        .route("/tags/{tag}", routing::get(handlers::search::get_tag))
        .route(
            "/users/{id}/watchlist",
            routing::get(handlers::watchlist::get_watchlist_page),
        )
        .route(
            "/watchlist/add",
            routing::post(handlers::watchlist::post_watchlist_add_html),
        )
        .route(
            "/watchlist/{movie_id}/remove",
            routing::post(handlers::watchlist::post_watchlist_remove_html),
        )
        .route(
            "/settings/integrations",
            routing::get(handlers::integrations::get_integrations_page),
        )
        .route(
            "/settings/integrations/generate",
            routing::post(handlers::integrations::post_generate_token),
        )
        .route(
            "/settings/integrations/{id}/revoke",
            routing::post(handlers::integrations::post_revoke_token),
        )
        .route(
            "/watch-queue",
            routing::get(handlers::integrations::get_watch_queue_page),
        )
        .route(
            "/watch-queue/{id}/confirm",
            routing::post(handlers::integrations::post_confirm_single),
        )
        .route(
            "/watch-queue/{id}/dismiss",
            routing::post(handlers::integrations::post_dismiss_single),
        )
        .route(
            "/wrapups/{user_id}/{year}",
            routing::get(handlers::wrapup::get_user_wrapup_html),
        )
        .route(
            "/wrapups/global/{year}",
            routing::get(handlers::wrapup::get_global_wrapup_html),
        );

    let base = base.merge(social_html_routes());
    #[cfg(feature = "federation")]
    let base = base.merge(federation_html_routes());

    base.layer(axum::middleware::from_fn(crate::csrf::csrf_middleware))
}

fn social_html_routes() -> Router<AppState> {
    let base = Router::new()
        .route(
            "/users/{id}/follow",
            routing::post(handlers::social::follow_user),
        )
        .route(
            "/users/{id}/unfollow",
            routing::post(handlers::social::unfollow_user),
        )
        .route(
            "/users/{id}/followers/accept",
            routing::post(handlers::social::accept_follower_html),
        )
        .route(
            "/users/{id}/followers/reject",
            routing::post(handlers::social::reject_follower_html),
        )
        .route(
            "/users/{id}/followers/remove",
            routing::post(handlers::social::remove_follower_html),
        )
        .route(
            "/users/{id}/following-list",
            routing::get(handlers::social::get_following_page),
        )
        .route(
            "/users/{id}/followers-list",
            routing::get(handlers::social::get_followers_page),
        );

    #[cfg(not(feature = "federation"))]
    let base = base
        .route(
            "/users/{id}/followers",
            routing::get(
                |axum::extract::Path(id): axum::extract::Path<uuid::Uuid>| async move {
                    axum::response::Redirect::permanent(&format!("/users/{}/followers-list", id))
                },
            ),
        )
        .route(
            "/users/{id}/following",
            routing::get(
                |axum::extract::Path(id): axum::extract::Path<uuid::Uuid>| async move {
                    axum::response::Redirect::permanent(&format!("/users/{}/following-list", id))
                },
            ),
        );

    base
}

#[cfg(feature = "federation")]
fn federation_html_routes() -> Router<AppState> {
    Router::new()
        .route(
            "/users/{id}/followers",
            routing::get(handlers::social::get_followers_collection),
        )
        .route(
            "/users/{id}/following",
            routing::get(handlers::social::get_following_collection),
        )
        .route(
            "/admin/blocked-domains",
            routing::get(handlers::social::get_blocked_domains_page)
                .post(handlers::social::post_blocked_domain),
        )
        .route(
            "/admin/blocked-domains/remove",
            routing::post(handlers::social::post_remove_blocked_domain),
        )
        .route(
            "/social/blocked",
            routing::get(handlers::social::get_blocked_actors_page),
        )
        .route(
            "/social/block",
            routing::post(handlers::social::post_block_actor_html),
        )
        .route(
            "/social/unblock",
            routing::post(handlers::social::post_unblock_actor),
        )
}

fn cors_layer() -> CorsLayer {
    use axum::http::{HeaderName, Method};
    use tower_http::cors::AllowOrigin;

    let origins = std::env::var("CORS_ORIGINS").unwrap_or_default();
    let layer = CorsLayer::new()
        .allow_methods([
            Method::GET,
            Method::POST,
            Method::PUT,
            Method::PATCH,
            Method::DELETE,
            Method::OPTIONS,
        ])
        .allow_headers([
            HeaderName::from_static("content-type"),
            HeaderName::from_static("authorization"),
        ]);

    if origins.is_empty() || origins == "*" {
        layer.allow_origin(AllowOrigin::any())
    } else {
        let parsed: Vec<_> = origins
            .split(',')
            .filter_map(|s| {
                let trimmed = s.trim();
                match trimmed.parse() {
                    Ok(v) => Some(v),
                    Err(e) => {
                        tracing::warn!("ignoring invalid CORS origin {trimmed:?}: {e}");
                        None
                    }
                }
            })
            .collect();
        layer
            .allow_origin(AllowOrigin::list(parsed))
            .allow_credentials(true)
    }
}

fn api_routes(rate_limit: u64) -> Router<AppState> {
    let cfg = GovernorConfigBuilder::default()
        .with_extractor(PeerIp::default())
        .expect_connect_info()
        .quota_default(per_minute(rate_limit))
        .finish()
        .unwrap();

    let base = Router::new()
        .route("/diary", routing::get(handlers::diary::get_diary))
        .route(
            "/movies/{id}/history",
            routing::get(handlers::movies::get_review_history),
        )
        .route("/movies", routing::get(handlers::movies::list_movies))
        .route(
            "/movies/{id}",
            routing::get(handlers::movies::get_movie_detail),
        )
        .route(
            "/movies/{id}/profile",
            routing::get(handlers::movies::get_movie_profile),
        )
        .route("/reviews", routing::post(handlers::diary::post_review))
        .route(
            "/reviews/{id}",
            routing::delete(handlers::diary::delete_review).patch(handlers::diary::patch_review),
        )
        .route(
            "/movies/{id}/sync-poster",
            routing::post(handlers::movies::sync_poster),
        )
        .route("/auth/login", routing::post(handlers::auth::login))
        .route("/auth/register", routing::post(handlers::auth::register))
        .route("/auth/refresh", routing::post(handlers::auth::refresh))
        .route("/auth/logout", routing::post(handlers::auth::api_logout))
        .route("/diary/export", routing::get(handlers::diary::export_diary))
        .route(
            "/activity-feed",
            routing::get(handlers::diary::get_activity_feed),
        )
        .route("/users", routing::get(handlers::users::list_users))
        .route(
            "/users/{id}",
            routing::get(handlers::users::get_user_profile),
        )
        .route(
            "/import/sessions",
            routing::post(handlers::import::api_post_session),
        )
        .route(
            "/import/sessions/{id}",
            routing::get(handlers::import::api_get_session),
        )
        .route(
            "/import/sessions/{id}/mapping",
            routing::put(handlers::import::api_put_mapping),
        )
        .route(
            "/import/sessions/{id}/preview",
            routing::get(handlers::import::api_get_preview),
        )
        .route(
            "/import/sessions/{id}/confirm",
            routing::post(handlers::import::api_post_confirm),
        )
        .route(
            "/import/profiles",
            routing::get(handlers::import::api_get_profiles)
                .post(handlers::import::api_post_profile),
        )
        .route(
            "/import/profiles/{id}",
            routing::delete(handlers::import::api_delete_profile),
        )
        .route(
            "/import/sessions/{id}/profile/{profile_id}",
            routing::put(handlers::import::api_apply_profile),
        )
        .route(
            "/profile",
            routing::get(handlers::users::get_profile).put(handlers::users::update_profile_handler),
        )
        .route(
            "/profile/fields",
            routing::put(handlers::users::update_profile_fields_handler),
        )
        .route("/search", routing::get(handlers::search::get_search))
        .route(
            "/people/{id}",
            routing::get(handlers::search::get_person_handler),
        )
        .route(
            "/people/{id}/credits",
            routing::get(handlers::search::get_person_credits_handler),
        )
        .route(
            "/watchlist",
            routing::get(handlers::watchlist::get_watchlist_handler)
                .post(handlers::watchlist::post_watchlist_add),
        )
        .route(
            "/watchlist/{movie_id}",
            routing::get(handlers::watchlist::get_watchlist_status)
                .delete(handlers::watchlist::delete_watchlist_entry),
        )
        .route(
            "/settings/webhook-tokens",
            routing::get(handlers::webhook::get_webhook_tokens)
                .post(handlers::webhook::post_generate_webhook_token),
        )
        .route(
            "/settings/webhook-tokens/{id}",
            routing::delete(handlers::webhook::delete_webhook_token),
        )
        .route(
            "/watch-queue",
            routing::get(handlers::webhook::get_watch_queue),
        )
        .route(
            "/watch-queue/confirm",
            routing::post(handlers::webhook::post_confirm_watch_events),
        )
        .route(
            "/watch-queue/dismiss",
            routing::post(handlers::webhook::post_dismiss_watch_events),
        )
        .route(
            "/wrapups/generate",
            routing::post(handlers::wrapup::post_generate),
        )
        .route("/wrapups", routing::get(handlers::wrapup::get_list))
        .route(
            "/wrapups/{id}",
            routing::get(handlers::wrapup::get_status)
                .delete(handlers::wrapup::delete_wrapup_handler),
        )
        .route(
            "/wrapups/{id}/report",
            routing::get(handlers::wrapup::get_report),
        )
        .route(
            "/admin/reindex-search",
            routing::post(handlers::search::post_reindex_search),
        )
        .route(
            "/goals",
            routing::get(handlers::goals::list_goals).post(handlers::goals::create_goal),
        )
        .route(
            "/goals/{year}",
            routing::put(handlers::goals::update_goal).delete(handlers::goals::delete_goal),
        )
        .route(
            "/users/{id}/goals",
            routing::get(handlers::goals::get_user_goals),
        )
        .route(
            "/settings",
            routing::get(handlers::goals::get_settings).put(handlers::goals::update_settings),
        );

    let base = base.merge(social_api_routes());
    #[cfg(feature = "federation")]
    let base = base.merge(federation_api_routes());

    let webhook_cfg = GovernorConfigBuilder::default()
        .with_extractor(PeerIp::default())
        .expect_connect_info()
        .quota_default(per_minute(rate_limit / 4))
        .finish()
        .unwrap();
    let webhook_routes = Router::new()
        .route(
            "/webhooks/jellyfin",
            routing::post(handlers::webhook::post_jellyfin_webhook),
        )
        .route(
            "/webhooks/plex",
            routing::post(handlers::webhook::post_plex_webhook),
        )
        .layer(GovernorLayer::new(webhook_cfg));

    Router::new()
        .nest("/api/v1", base.layer(GovernorLayer::new(cfg)))
        .nest("/api/v1", webhook_routes)
        .layer(cors_layer())
}

fn social_api_routes() -> Router<AppState> {
    Router::new()
        .route(
            "/social/following",
            routing::get(handlers::social::get_following),
        )
        .route(
            "/social/followers",
            routing::get(handlers::social::get_followers),
        )
        .route(
            "/social/followers/pending",
            routing::get(handlers::social::get_pending_followers),
        )
        .route(
            "/social/followers/pending/count",
            routing::get(handlers::social::get_pending_follower_count),
        )
        .route(
            "/social/following/pending",
            routing::get(handlers::social::get_pending_following),
        )
        .route(
            "/social/relationship",
            routing::get(handlers::social::get_relationship),
        )
        .route("/social/follow", routing::post(handlers::social::follow))
        .route(
            "/social/unfollow",
            routing::post(handlers::social::unfollow),
        )
        .route(
            "/social/followers/accept",
            routing::post(handlers::social::accept_follower),
        )
        .route(
            "/social/followers/reject",
            routing::post(handlers::social::reject_follower),
        )
        .route(
            "/social/followers/remove",
            routing::post(handlers::social::remove_follower),
        )
        .route(
            "/users/{id}/following",
            routing::get(handlers::social::get_user_following),
        )
        .route(
            "/users/{id}/followers",
            routing::get(handlers::social::get_user_followers),
        )
}

#[cfg(feature = "federation")]
fn federation_api_routes() -> Router<AppState> {
    Router::new()
        .route(
            "/admin/blocked-domains",
            routing::get(handlers::social::get_blocked_domains_admin)
                .post(handlers::social::add_blocked_domain_admin),
        )
        .route(
            "/admin/blocked-domains/{domain}",
            routing::delete(handlers::social::remove_blocked_domain_admin),
        )
        .route(
            "/social/block",
            routing::post(handlers::social::block_actor_api),
        )
        .route(
            "/social/unblock",
            routing::post(handlers::social::unblock_actor_api),
        )
        .route(
            "/social/blocked",
            routing::get(handlers::social::get_blocked_actors_api),
        )
}
