Skip to content
pub mod battler;
pub mod debug;
pub mod guard;
pub mod layout;
pub mod pokedex;

use actix_web::{HttpRequest, HttpResponse, Responder, get, post, web};
use serde::{Deserialize, Serialize};

use crate::auth;
use crate::config::Config;
use crate::db::{DuckDb, SqliteDb};
use crate::session::{SESSION_COOKIE_NAME, SessionStore};
use debug::DbTableSummary;
use layout::page_shell;

pub fn is_htmx(req: &HttpRequest) -> bool {
    req.headers().contains_key("hx-request")
}

const PAGE_SIZE: usize = 24;

#[derive(Deserialize, Serialize)]
pub struct SearchQuery {
    #[serde(default)]
    pub q: String,
}

#[derive(Deserialize, Serialize)]
pub struct PageQuery {
    #[serde(default)]
    pub offset: usize,
    #[serde(default)]
    pub q: String,
}

#[derive(Deserialize, Serialize)]
pub struct PinForm {
    pub pin: String,
}

#[get("/")]
pub async fn index(duck_db: web::Data<DuckDb>) -> impl Responder {
    let pokemon_list = duck_db
        .search_pokemon("", PAGE_SIZE, 0)
        .await
        .unwrap_or_default();
    let total = duck_db.total_pokemon_count("").await.unwrap_or(0);
    let has_more = total > PAGE_SIZE;

    let content = pokedex::pokedex_tab_view(&pokemon_list, "", total, has_more, PAGE_SIZE);
    HttpResponse::Ok()
        .content_type("text/html; charset=utf-8")
        .body(page_shell("Pokédex • FireRed & LeafGreen", "pokedex", &content).into_string())
}

#[get("/pokedex")]
pub async fn pokedex_tab(req: HttpRequest, duck_db: web::Data<DuckDb>) -> impl Responder {
    let pokemon_list = duck_db
        .search_pokemon("", PAGE_SIZE, 0)
        .await
        .unwrap_or_default();
    let total = duck_db.total_pokemon_count("").await.unwrap_or(0);
    let has_more = total > PAGE_SIZE;

    let content = pokedex::pokedex_tab_view(&pokemon_list, "", total, has_more, PAGE_SIZE);
    if is_htmx(&req) {
        HttpResponse::Ok()
            .content_type("text/html; charset=utf-8")
            .body(content.into_string())
    } else {
        HttpResponse::Ok()
            .content_type("text/html; charset=utf-8")
            .body(page_shell("Pokédex • FireRed & LeafGreen", "pokedex", &content).into_string())
    }
}

#[get("/pokedex/search")]
pub async fn pokedex_search(
    query: web::Query<SearchQuery>,
    duck_db: web::Data<DuckDb>,
) -> impl Responder {
    let q = query.q.trim();
    let pokemon_list = duck_db
        .search_pokemon(q, PAGE_SIZE, 0)
        .await
        .unwrap_or_default();
    let total = duck_db.total_pokemon_count(q).await.unwrap_or(0);
    let has_more = total > PAGE_SIZE;

    let content = pokedex::pokemon_grid_items(&pokemon_list, q, has_more, PAGE_SIZE);
    HttpResponse::Ok()
        .content_type("text/html; charset=utf-8")
        .body(content.into_string())
}

#[get("/pokedex/page")]
pub async fn pokedex_page(
    query: web::Query<PageQuery>,
    duck_db: web::Data<DuckDb>,
) -> impl Responder {
    let q = query.q.trim();
    let offset = query.offset;
    let pokemon_list = duck_db
        .search_pokemon(q, PAGE_SIZE, offset)
        .await
        .unwrap_or_default();
    let total = duck_db.total_pokemon_count(q).await.unwrap_or(0);
    let next_offset = offset + pokemon_list.len();
    let has_more = total > next_offset;

    let content = pokedex::pokemon_grid_items(&pokemon_list, q, has_more, next_offset);
    HttpResponse::Ok()
        .content_type("text/html; charset=utf-8")
        .body(content.into_string())
}

#[get("/pokemon/{id}")]
pub async fn pokemon_detail(
    req: HttpRequest,
    path: web::Path<i32>,
    duck_db: web::Data<DuckDb>,
) -> impl Responder {
    let id = path.into_inner();
    match duck_db.get_pokemon_by_id(id).await {
        Ok(Some(p)) => {
            let content = pokedex::pokemon_detail_page(&p);
            if is_htmx(&req) {
                HttpResponse::Ok()
                    .content_type("text/html; charset=utf-8")
                    .body(content.into_string())
            } else {
                HttpResponse::Ok()
                    .content_type("text/html; charset=utf-8")
                    .body(
                        page_shell(
                            &format!("{} • Pokédex", p.display_name),
                            "pokedex",
                            &content,
                        )
                        .into_string(),
                    )
            }
        }
        Ok(None) => HttpResponse::NotFound().body("Pokémon not found"),
        Err(e) => {
            log::error!("Failed to get pokemon {id}: {e}");
            HttpResponse::InternalServerError().finish()
        }
    }
}

#[get("/battler")]
pub async fn battler_tab(req: HttpRequest) -> impl Responder {
    let content = battler::battler_view();
    if is_htmx(&req) {
        HttpResponse::Ok()
            .content_type("text/html; charset=utf-8")
            .body(content.into_string())
    } else {
        HttpResponse::Ok()
            .content_type("text/html; charset=utf-8")
            .body(page_shell("AutoBattler Arena • Coming Soon", "battler", &content).into_string())
    }
}

#[get("/battler/ticket")]
pub async fn battler_ticket() -> impl Responder {
    let pin = crate::battle_ticket::generate_battle_pin();
    let qr_svg =
        crate::battle_ticket::generate_qr_svg(&format!("/battle?pin={pin}")).unwrap_or_default();
    let content = battler::battle_ticket_card(&pin, &qr_svg);
    HttpResponse::Ok()
        .content_type("text/html; charset=utf-8")
        .body(content.into_string())
}

#[derive(Deserialize, Serialize)]
pub struct BattleQuery {
    pub pin: Option<String>,
}

#[get("/battle")]
pub async fn battle_entry(req: HttpRequest, query: web::Query<BattleQuery>) -> impl Responder {
    let content = battler::battle_coming_soon_view(query.pin.as_deref());
    if is_htmx(&req) {
        HttpResponse::Ok()
            .content_type("text/html; charset=utf-8")
            .body(content.into_string())
    } else {
        HttpResponse::Ok()
            .content_type("text/html; charset=utf-8")
            .body(page_shell("Match Lobby • Coming Soon", "battler", &content).into_string())
    }
}

#[get("/debug")]
pub async fn debug_tab(
    req: HttpRequest,
    config: web::Data<Config>,
    session_store: web::Data<SessionStore>,
    duck_db: web::Data<DuckDb>,
    sqlite_db: web::Data<SqliteDb>,
) -> impl Responder {
    if !auth::is_admin(&req, &config, &session_store) {
        let guard_html = guard::admin_pin_guard(None);
        return if is_htmx(&req) {
            HttpResponse::Ok()
                .content_type("text/html; charset=utf-8")
                .body(guard_html.into_string())
        } else {
            HttpResponse::Ok()
                .content_type("text/html; charset=utf-8")
                .body(page_shell("Admin Access • Pokédex", "debug", &guard_html).into_string())
        };
    }

    let duck_summary = fetch_duck_summary(&duck_db).await;
    let sqlite_summary = fetch_sqlite_summary(&sqlite_db).await;

    let content = debug::debug_view(
        duck_db.db_path(),
        sqlite_db.db_path(),
        &duck_summary,
        &sqlite_summary,
        None,
    );

    if is_htmx(&req) {
        HttpResponse::Ok()
            .content_type("text/html; charset=utf-8")
            .body(content.into_string())
    } else {
        HttpResponse::Ok()
            .content_type("text/html; charset=utf-8")
            .body(page_shell("Debug Diagnostics • Pokédex", "debug", &content).into_string())
    }
}

#[post("/pin")]
pub async fn verify_pin(
    form: web::Form<PinForm>,
    config: web::Data<Config>,
    session_store: web::Data<SessionStore>,
    duck_db: web::Data<DuckDb>,
    sqlite_db: web::Data<SqliteDb>,
) -> impl Responder {
    if config.is_valid_pin(&form.pin) {
        let token = session_store.create_session();
        let cookie = actix_web::cookie::Cookie::build(SESSION_COOKIE_NAME, token)
            .path("/")
            .http_only(true)
            .same_site(actix_web::cookie::SameSite::Lax)
            .finish();

        let duck_summary = fetch_duck_summary(&duck_db).await;
        let sqlite_summary = fetch_sqlite_summary(&sqlite_db).await;

        let content = debug::debug_view(
            duck_db.db_path(),
            sqlite_db.db_path(),
            &duck_summary,
            &sqlite_summary,
            None,
        );

        HttpResponse::Ok()
            .cookie(cookie)
            .content_type("text/html; charset=utf-8")
            .body(content.into_string())
    } else {
        let guard_html = guard::admin_pin_guard(Some("Invalid PIN code. Please try again."));
        HttpResponse::Ok()
            .content_type("text/html; charset=utf-8")
            .body(guard_html.into_string())
    }
}

#[post("/logout")]
pub async fn logout(req: HttpRequest, session_store: web::Data<SessionStore>) -> impl Responder {
    session_store.destroy_session(&req);
    let cookie = actix_web::cookie::Cookie::build(SESSION_COOKIE_NAME, "")
        .path("/")
        .max_age(actix_web::cookie::time::Duration::seconds(0))
        .finish();

    let guard_html = guard::admin_pin_guard(None);
    HttpResponse::Ok()
        .cookie(cookie)
        .content_type("text/html; charset=utf-8")
        .body(guard_html.into_string())
}

#[post("/debug/sync")]
pub async fn debug_sync(
    req: HttpRequest,
    config: web::Data<Config>,
    session_store: web::Data<SessionStore>,
    duck_db: web::Data<DuckDb>,
) -> impl Responder {
    if !auth::is_admin(&req, &config, &session_store) {
        return HttpResponse::Unauthorized().finish();
    }

    match duck_db.seed_if_empty().await {
        Ok(count) => {
            let msg = format!("✓ DuckDB verified ({count} Pokémon active)");
            HttpResponse::Ok()
                .content_type("text/html; charset=utf-8")
                .body(format!("<span class='text-xs font-semibold px-2 py-1 bg-green-100 text-green-800 rounded'>{msg}</span>"))
        }
        Err(e) => HttpResponse::InternalServerError().body(format!(
            "<span class='text-xs text-red-600'>Sync error: {e}</span>"
        )),
    }
}

async fn fetch_duck_summary(duck_db: &DuckDb) -> Vec<DbTableSummary> {
    let mut list = Vec::new();
    if let Ok(tables) = duck_db.list_tables().await {
        for table in tables {
            let row_count = duck_db.table_row_count(&table).await.unwrap_or(0);
            let columns = duck_db.describe_table(&table).await.unwrap_or_default();
            let preview = duck_db.preview_table(&table, 10).await.unwrap_or_else(|_| {
                crate::db::TablePreview {
                    table_name: table.clone(),
                    total_rows: 0,
                    columns: Vec::new(),
                    rows: Vec::new(),
                }
            });
            list.push(DbTableSummary {
                name: table,
                row_count,
                columns,
                preview,
            });
        }
    }
    list
}

async fn fetch_sqlite_summary(sqlite_db: &SqliteDb) -> Vec<DbTableSummary> {
    let mut list = Vec::new();
    if let Ok(tables) = sqlite_db.list_tables().await {
        for table in tables {
            let row_count = sqlite_db.table_row_count(&table).await.unwrap_or(0);
            let columns = sqlite_db.describe_table(&table).await.unwrap_or_default();
            let preview = sqlite_db
                .preview_table(&table, 10)
                .await
                .unwrap_or_else(|_| crate::db::TablePreview {
                    table_name: table.clone(),
                    total_rows: 0,
                    columns: Vec::new(),
                    rows: Vec::new(),
                });
            list.push(DbTableSummary {
                name: table,
                row_count,
                columns,
                preview,
            });
        }
    }
    list
}