//! asm.famzheng.me — 汇编教学小游戏。
//!
//! 4 个 endpoint：
//! - `GET  /api/health`            前端 ping 用
//! - `POST /api/players`           按 name upsert，返回 id + 已有进度 map
//! - `POST /api/progress`          单关卡 upsert（stars 只能增加，code 覆盖）
//! - `GET  /api/leaderboard`       top 50（按 total_stars desc, levels_completed desc）
//!
//! 静态前端 + SPA fallback 由 cube-core::base 处理。

use std::collections::HashMap;
use std::sync::{Arc, Mutex};

use axum::{
    extract::State,
    http::StatusCode,
    response::{IntoResponse, Json as JsonResp},
    routing::{get, post},
    Json, Router,
};
use rusqlite::{params, Connection, OptionalExtension};
use serde::{Deserialize, Serialize};
use serde_json::json;

type Db = Arc<Mutex<Connection>>;

#[tokio::main]
async fn main() -> std::io::Result<()> {
    cube_core::init_tracing();

    let db_path = std::env::var("DB_PATH").unwrap_or_else(|_| "/data/app.db".into());
    let dist = std::env::var("SIMPLEASM_DIST_DIR").unwrap_or_else(|_| "/dist".into());

    let conn = Connection::open(&db_path).expect("open sqlite");
    conn.execute_batch(
        "PRAGMA journal_mode=WAL;
         CREATE TABLE IF NOT EXISTS players (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT UNIQUE NOT NULL,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
         );
         CREATE TABLE IF NOT EXISTS progress (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            player_id INTEGER NOT NULL,
            level_id INTEGER NOT NULL,
            stars INTEGER NOT NULL DEFAULT 0,
            code TEXT,
            completed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            FOREIGN KEY (player_id) REFERENCES players(id),
            UNIQUE (player_id, level_id)
         );",
    )
    .expect("init schema");
    tracing::info!(%db_path, "sqlite ready");

    let db: Db = Arc::new(Mutex::new(conn));

    let api = Router::new()
        .route("/health", get(|| async { "ok" }))
        .route("/players", post(create_or_get_player))
        .route("/progress", post(save_progress))
        .route("/leaderboard", get(leaderboard))
        .with_state(db);

    let app = cube_core::base(dist).nest("/api", api);
    cube_core::serve(app, 8080).await
}

#[derive(Deserialize)]
struct PlayerCreate {
    name: String,
}

#[derive(Deserialize)]
struct ProgressSave {
    player_id: i64,
    level_id: i64,
    stars: i64,
    #[serde(default)]
    code: String,
}

#[derive(Serialize)]
struct ProgressItem {
    stars: i64,
    code: String,
    completed: bool,
}

#[derive(Serialize)]
struct LeaderboardRow {
    name: String,
    total_stars: i64,
    levels_completed: i64,
}

/// `POST /api/players` — 按 name upsert。返回 `{id, name, progress: {level_id: {...}}}`。
async fn create_or_get_player(
    State(db): State<Db>,
    Json(data): Json<PlayerCreate>,
) -> Result<impl IntoResponse, AppError> {
    let name = data.name.trim().to_string();
    if name.is_empty() {
        return Err(AppError::bad_request("Name cannot be empty"));
    }

    let conn = db.lock().unwrap();
    let player_id: i64 = match conn
        .query_row(
            "SELECT id FROM players WHERE name = ?1",
            params![name],
            |r| r.get(0),
        )
        .optional()?
    {
        Some(id) => id,
        None => {
            conn.execute("INSERT INTO players (name) VALUES (?1)", params![name])?;
            conn.last_insert_rowid()
        }
    };

    let mut stmt = conn.prepare(
        "SELECT level_id, stars, code FROM progress WHERE player_id = ?1",
    )?;
    let mut rows = stmt.query(params![player_id])?;
    let mut progress: HashMap<String, ProgressItem> = HashMap::new();
    while let Some(r) = rows.next()? {
        let level_id: i64 = r.get(0)?;
        let stars: i64 = r.get(1)?;
        let code: String = r.get::<_, Option<String>>(2)?.unwrap_or_default();
        progress.insert(
            level_id.to_string(),
            ProgressItem { stars, code, completed: true },
        );
    }

    Ok(JsonResp(json!({
        "id": player_id,
        "name": name,
        "progress": progress,
    })))
}

/// `POST /api/progress` — 单关 upsert。stars 取 max(old, new)，code 永远覆盖。
async fn save_progress(
    State(db): State<Db>,
    Json(data): Json<ProgressSave>,
) -> Result<impl IntoResponse, AppError> {
    let conn = db.lock().unwrap();
    conn.execute(
        "INSERT INTO progress (player_id, level_id, stars, code)
         VALUES (?1, ?2, ?3, ?4)
         ON CONFLICT (player_id, level_id)
         DO UPDATE SET stars = MAX(stars, excluded.stars),
                       code = excluded.code,
                       completed_at = CURRENT_TIMESTAMP",
        params![data.player_id, data.level_id, data.stars, data.code],
    )?;
    Ok(JsonResp(json!({ "success": true })))
}

/// `GET /api/leaderboard` — top 50 by (total_stars desc, levels_completed desc)。
async fn leaderboard(State(db): State<Db>) -> Result<impl IntoResponse, AppError> {
    let conn = db.lock().unwrap();
    let mut stmt = conn.prepare(
        "SELECT p.name, COALESCE(SUM(pr.stars), 0) AS total_stars,
                COUNT(pr.id) AS levels_completed
         FROM players p
         LEFT JOIN progress pr ON p.id = pr.player_id
         GROUP BY p.id
         ORDER BY total_stars DESC, levels_completed DESC
         LIMIT 50",
    )?;
    let rows = stmt
        .query_map([], |r| {
            Ok(LeaderboardRow {
                name: r.get(0)?,
                total_stars: r.get(1)?,
                levels_completed: r.get(2)?,
            })
        })?
        .collect::<Result<Vec<_>, _>>()?;
    Ok(JsonResp(json!(rows)))
}

enum AppError {
    BadRequest(String),
    Db(rusqlite::Error),
}

impl AppError {
    fn bad_request(msg: impl Into<String>) -> Self {
        Self::BadRequest(msg.into())
    }
}

impl From<rusqlite::Error> for AppError {
    fn from(e: rusqlite::Error) -> Self {
        Self::Db(e)
    }
}

impl IntoResponse for AppError {
    fn into_response(self) -> axum::response::Response {
        match self {
            Self::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg).into_response(),
            Self::Db(e) => {
                tracing::error!(error = %e, "sqlite error");
                (StatusCode::INTERNAL_SERVER_ERROR, "db error").into_response()
            }
        }
    }
}
