use super::models::{
    infer_library_item_kind, GameCandidate, GameCustomizationRequest, GameMetadata, GameSource,
    LibraryGame, LibraryItemKind, MetadataStatus,
};
use rusqlite::{params, Connection, OptionalExtension};
use std::time::{SystemTime, UNIX_EPOCH};

pub fn now_unix_ms() -> i64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_millis() as i64)
        .unwrap_or(0)
}

pub fn ensure_library_schema(conn: &Connection) -> Result<(), String> {
    conn.execute_batch(
        "PRAGMA foreign_keys = ON;

        CREATE TABLE IF NOT EXISTS games (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL,
            platform_source TEXT NOT NULL,
            source_store TEXT NOT NULL,
            source_app_id TEXT,
            install_path TEXT NOT NULL,
            executable_path TEXT,
            launch_command TEXT,
            description TEXT,
            app_kind TEXT NOT NULL DEFAULT 'game',
            steam_app_type TEXT,
            genres_json TEXT NOT NULL DEFAULT '[]',
            steam_categories_json TEXT NOT NULL DEFAULT '[]',
            release_date TEXT,
            developer TEXT,
            publisher TEXT,
            cover_image TEXT,
            hero_image TEXT,
            icon_image TEXT,
            metadata_status TEXT NOT NULL DEFAULT 'pending',
            last_scanned INTEGER NOT NULL,
            user_hidden INTEGER NOT NULL DEFAULT 0,
            user_favourite INTEGER NOT NULL DEFAULT 0,
            user_title TEXT,
            manual_metadata_json TEXT,
            created_at INTEGER NOT NULL,
            updated_at INTEGER NOT NULL
        );

        CREATE UNIQUE INDEX IF NOT EXISTS idx_games_source_app
            ON games (platform_source, source_app_id)
            WHERE source_app_id IS NOT NULL;

        CREATE UNIQUE INDEX IF NOT EXISTS idx_games_install_path
            ON games (install_path);

        CREATE INDEX IF NOT EXISTS idx_games_hidden ON games (user_hidden);
        CREATE INDEX IF NOT EXISTS idx_games_metadata_status ON games (metadata_status);

        CREATE TABLE IF NOT EXISTS scan_runs (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            started_at INTEGER NOT NULL,
            completed_at INTEGER,
            status TEXT NOT NULL,
            discovered_count INTEGER NOT NULL DEFAULT 0,
            metadata_matched_count INTEGER NOT NULL DEFAULT 0,
            unmatched_count INTEGER NOT NULL DEFAULT 0,
            error TEXT
        );

        CREATE TABLE IF NOT EXISTS metadata_cache (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            provider TEXT NOT NULL,
            lookup_key TEXT NOT NULL,
            metadata_json TEXT NOT NULL,
            created_at INTEGER NOT NULL,
            updated_at INTEGER NOT NULL,
            UNIQUE(provider, lookup_key)
        );",
    )
    .map_err(|err| format!("Failed to prepare library schema: {err}"))?;

    ensure_column(
        conn,
        "games",
        "app_kind",
        "app_kind TEXT NOT NULL DEFAULT 'game'",
    )?;
    ensure_column(conn, "games", "steam_app_type", "steam_app_type TEXT")?;
    ensure_column(
        conn,
        "games",
        "steam_categories_json",
        "steam_categories_json TEXT NOT NULL DEFAULT '[]'",
    )?;

    Ok(())
}

pub fn upsert_candidate(
    conn: &Connection,
    candidate: &GameCandidate,
    metadata: Option<&GameMetadata>,
) -> Result<(), String> {
    let now = now_unix_ms();
    let metadata_status = metadata
        .map(|value| value.status.as_str())
        .unwrap_or(MetadataStatus::NeedsReview.as_str());
    let title = metadata
        .and_then(|value| value.title.as_deref())
        .unwrap_or(&candidate.title);
    let source = candidate.source.as_str();
    let app_kind = metadata
        .map(|value| value.app_kind.clone())
        .unwrap_or_else(|| infer_library_item_kind(None, Some(&candidate.title), &[], &[]));
    let genres_json = metadata
        .map(|value| serde_json::to_string(&value.genres))
        .transpose()
        .map_err(|err| format!("Failed to encode genres: {err}"))?
        .unwrap_or_else(|| "[]".to_string());
    let steam_categories_json = metadata
        .map(|value| serde_json::to_string(&value.steam_categories))
        .transpose()
        .map_err(|err| format!("Failed to encode Steam categories: {err}"))?
        .unwrap_or_else(|| "[]".to_string());

    conn.execute(
        "INSERT INTO games (
            title,
            platform_source,
            source_store,
            source_app_id,
            install_path,
            executable_path,
            launch_command,
            description,
            app_kind,
            steam_app_type,
            genres_json,
            steam_categories_json,
            release_date,
            developer,
            publisher,
            cover_image,
            hero_image,
            icon_image,
            metadata_status,
            last_scanned,
            created_at,
            updated_at
        )
        VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22)
        ON CONFLICT(platform_source, source_app_id) WHERE source_app_id IS NOT NULL DO UPDATE SET
            title = excluded.title,
            source_store = excluded.source_store,
            install_path = excluded.install_path,
            executable_path = excluded.executable_path,
            launch_command = excluded.launch_command,
            description = COALESCE(excluded.description, games.description),
            app_kind = excluded.app_kind,
            steam_app_type = COALESCE(excluded.steam_app_type, games.steam_app_type),
            genres_json = excluded.genres_json,
            steam_categories_json = excluded.steam_categories_json,
            release_date = COALESCE(excluded.release_date, games.release_date),
            developer = COALESCE(excluded.developer, games.developer),
            publisher = COALESCE(excluded.publisher, games.publisher),
            cover_image = excluded.cover_image,
            hero_image = excluded.hero_image,
            icon_image = excluded.icon_image,
            metadata_status = excluded.metadata_status,
            last_scanned = excluded.last_scanned,
            updated_at = excluded.updated_at
        ON CONFLICT(install_path) DO UPDATE SET
            title = excluded.title,
            platform_source = excluded.platform_source,
            source_store = excluded.source_store,
            source_app_id = COALESCE(excluded.source_app_id, games.source_app_id),
            executable_path = excluded.executable_path,
            launch_command = excluded.launch_command,
            description = COALESCE(excluded.description, games.description),
            app_kind = excluded.app_kind,
            steam_app_type = COALESCE(excluded.steam_app_type, games.steam_app_type),
            genres_json = excluded.genres_json,
            steam_categories_json = excluded.steam_categories_json,
            release_date = COALESCE(excluded.release_date, games.release_date),
            developer = COALESCE(excluded.developer, games.developer),
            publisher = COALESCE(excluded.publisher, games.publisher),
            cover_image = excluded.cover_image,
            hero_image = excluded.hero_image,
            icon_image = excluded.icon_image,
            metadata_status = excluded.metadata_status,
            last_scanned = excluded.last_scanned,
            updated_at = excluded.updated_at",
        params![
            title,
            source,
            source,
            candidate.app_id,
            candidate.install_path.to_string_lossy(),
            candidate
                .executable_path
                .as_ref()
                .map(|path| path.to_string_lossy().to_string()),
            candidate.launch_command,
            metadata.and_then(|value| value.description.as_deref()),
            app_kind.as_str(),
            metadata.and_then(|value| value.steam_app_type.as_deref()),
            genres_json,
            steam_categories_json,
            metadata.and_then(|value| value.release_date.as_deref()),
            metadata.and_then(|value| value.developer.as_deref()),
            metadata.and_then(|value| value.publisher.as_deref()),
            metadata
                .and_then(|value| value.cover_image.as_ref())
                .map(|path| path.to_string_lossy().to_string()),
            metadata
                .and_then(|value| value.hero_image.as_ref())
                .map(|path| path.to_string_lossy().to_string()),
            metadata
                .and_then(|value| value.icon_image.as_ref())
                .map(|path| path.to_string_lossy().to_string()),
            metadata_status,
            now,
            now,
            now,
        ],
    )
    .map_err(|err| format!("Failed to upsert library game: {err}"))?;

    Ok(())
}

pub fn list_games(conn: &Connection, include_hidden: bool) -> Result<Vec<LibraryGame>, String> {
    let where_clause = if include_hidden {
        ""
    } else {
        "WHERE user_hidden = 0"
    };
    let sql = format!(
        "SELECT
            id,
            title,
            platform_source,
            source_store,
            source_app_id,
            install_path,
            executable_path,
            launch_command,
            description,
            app_kind,
            steam_app_type,
            genres_json,
            steam_categories_json,
            release_date,
            developer,
            publisher,
            cover_image,
            hero_image,
            icon_image,
            metadata_status,
            last_scanned,
            user_hidden,
            user_favourite,
            user_title
        FROM games
        {where_clause}
        ORDER BY COALESCE(user_title, title) COLLATE NOCASE ASC"
    );

    let mut stmt = conn
        .prepare(&sql)
        .map_err(|err| format!("Failed to load library games: {err}"))?;

    let rows = stmt
        .query_map([], |row| {
            let genres_json: String = row.get(11)?;
            let genres = parse_string_list(&genres_json);
            let steam_categories_json: String = row.get(12)?;
            let steam_categories = parse_string_list(&steam_categories_json);
            let platform_source: String = row.get(2)?;
            let stored_app_kind: String = row.get(9)?;
            let metadata_status: String = row.get(19)?;
            let steam_app_type: Option<String> = row.get(10)?;
            let title: String = row.get(1)?;
            let inferred_app_kind = infer_library_item_kind(
                steam_app_type.as_deref(),
                Some(&title),
                &genres,
                &steam_categories,
            );
            let app_kind = if inferred_app_kind == LibraryItemKind::Unknown {
                LibraryItemKind::from_str(&stored_app_kind)
            } else {
                inferred_app_kind
            };
            Ok(LibraryGame {
                id: row.get(0)?,
                title,
                platform_source: GameSource::from_str(&platform_source),
                source_store: row.get(3)?,
                source_app_id: row.get(4)?,
                install_path: row.get(5)?,
                executable_path: row.get(6)?,
                launch_command: row.get(7)?,
                description: row.get(8)?,
                app_kind,
                steam_app_type,
                genres,
                steam_categories,
                release_date: row.get(13)?,
                developer: row.get(14)?,
                publisher: row.get(15)?,
                cover_image: row.get(16)?,
                hero_image: row.get(17)?,
                icon_image: row.get(18)?,
                metadata_status: MetadataStatus::from_str(&metadata_status),
                last_scanned: row.get(20)?,
                user_hidden: row.get::<_, i64>(21)? != 0,
                user_favourite: row.get::<_, i64>(22)? != 0,
                user_title: row.get(23)?,
            })
        })
        .map_err(|err| format!("Failed to read library games: {err}"))?;

    rows.collect::<Result<Vec<_>, _>>()
        .map_err(|err| format!("Failed to parse library games: {err}"))
}

pub fn update_customization(
    conn: &Connection,
    request: GameCustomizationRequest,
) -> Result<Option<LibraryGame>, String> {
    let now = now_unix_ms();
    let current = conn
        .query_row(
            "SELECT id FROM games WHERE id = ?1",
            params![request.game_id],
            |row| row.get::<_, i64>(0),
        )
        .optional()
        .map_err(|err| format!("Failed to find library game: {err}"))?;

    if current.is_none() {
        return Ok(None);
    }

    conn.execute(
        "UPDATE games SET
            user_title = COALESCE(?1, user_title),
            executable_path = COALESCE(?2, executable_path),
            user_hidden = COALESCE(?3, user_hidden),
            user_favourite = COALESCE(?4, user_favourite),
            metadata_status = CASE WHEN ?1 IS NULL THEN metadata_status ELSE 'manual' END,
            updated_at = ?5
         WHERE id = ?6",
        params![
            request.title.and_then(|value| {
                let trimmed = value.trim().to_string();
                if trimmed.is_empty() {
                    None
                } else {
                    Some(trimmed)
                }
            }),
            request.executable_path,
            request.hidden.map(|value| if value { 1 } else { 0 }),
            request.favourite.map(|value| if value { 1 } else { 0 }),
            now,
            request.game_id,
        ],
    )
    .map_err(|err| format!("Failed to update library game: {err}"))?;

    conn.query_row(
        "SELECT
            id,
            title,
            platform_source,
            source_store,
            source_app_id,
            install_path,
            executable_path,
            launch_command,
            description,
            app_kind,
            steam_app_type,
            genres_json,
            steam_categories_json,
            release_date,
            developer,
            publisher,
            cover_image,
            hero_image,
            icon_image,
            metadata_status,
            last_scanned,
            user_hidden,
            user_favourite,
            user_title
         FROM games
         WHERE id = ?1",
        params![request.game_id],
        |row| {
            let genres_json: String = row.get(11)?;
            let steam_categories_json: String = row.get(12)?;
            let platform_source: String = row.get(2)?;
            let stored_app_kind: String = row.get(9)?;
            let metadata_status: String = row.get(19)?;
            let steam_app_type: Option<String> = row.get(10)?;
            let title: String = row.get(1)?;
            let genres = parse_string_list(&genres_json);
            let steam_categories = parse_string_list(&steam_categories_json);
            let inferred_app_kind = infer_library_item_kind(
                steam_app_type.as_deref(),
                Some(&title),
                &genres,
                &steam_categories,
            );
            let app_kind = if inferred_app_kind == LibraryItemKind::Unknown {
                LibraryItemKind::from_str(&stored_app_kind)
            } else {
                inferred_app_kind
            };
            Ok(LibraryGame {
                id: row.get(0)?,
                title,
                platform_source: GameSource::from_str(&platform_source),
                source_store: row.get(3)?,
                source_app_id: row.get(4)?,
                install_path: row.get(5)?,
                executable_path: row.get(6)?,
                launch_command: row.get(7)?,
                description: row.get(8)?,
                app_kind,
                steam_app_type,
                genres,
                steam_categories,
                release_date: row.get(13)?,
                developer: row.get(14)?,
                publisher: row.get(15)?,
                cover_image: row.get(16)?,
                hero_image: row.get(17)?,
                icon_image: row.get(18)?,
                metadata_status: MetadataStatus::from_str(&metadata_status),
                last_scanned: row.get(20)?,
                user_hidden: row.get::<_, i64>(21)? != 0,
                user_favourite: row.get::<_, i64>(22)? != 0,
                user_title: row.get(23)?,
            })
        },
    )
    .optional()
    .map_err(|err| format!("Failed to reload library game: {err}"))
}

fn ensure_column(
    conn: &Connection,
    table: &str,
    column: &str,
    definition: &str,
) -> Result<(), String> {
    let mut stmt = conn
        .prepare(&format!("PRAGMA table_info({table})"))
        .map_err(|err| format!("Failed to inspect {table} schema: {err}"))?;
    let columns = stmt
        .query_map([], |row| row.get::<_, String>(1))
        .map_err(|err| format!("Failed to read {table} schema: {err}"))?
        .collect::<Result<Vec<_>, _>>()
        .map_err(|err| format!("Failed to parse {table} schema: {err}"))?;

    if !columns.iter().any(|name| name == column) {
        conn.execute(&format!("ALTER TABLE {table} ADD COLUMN {definition}"), [])
            .map_err(|err| format!("Failed to add {table}.{column}: {err}"))?;
    }

    Ok(())
}

fn parse_string_list(value: &str) -> Vec<String> {
    serde_json::from_str(value).unwrap_or_default()
}
