pub mod commands;
pub mod db;
pub mod images;
pub mod metadata;
pub mod models;
pub mod scanners;

use crate::storage::AppStorage;
use db::{ensure_library_schema, list_games, upsert_candidate};
use images::ImageCache;
use metadata::{LocalCacheMetadataProvider, MetadataProvider};
use models::{GameCandidate, GameSource, LibraryGame, ScanProviderReport, ScanSummary};
use scanners::{
    epic::EpicScanner, gog::GogScanner, local::LocalAppScanner, steam::SteamScanner, ScanContext,
    ScannerProvider,
};
use std::collections::HashMap;
use std::path::PathBuf;

pub fn initialize(storage: &AppStorage) -> Result<(), String> {
    let conn = storage.connect_library()?;
    ensure_library_schema(&conn)
}

pub fn list_visible_games(storage: &AppStorage) -> Result<Vec<LibraryGame>, String> {
    let conn = storage.connect_library()?;
    ensure_library_schema(&conn)?;
    list_games(&conn, false)
}

pub fn scan_library(
    storage: AppStorage,
    local_folders: Vec<PathBuf>,
) -> Result<ScanSummary, String> {
    let conn = storage.connect_library()?;
    ensure_library_schema(&conn)?;

    let context = ScanContext { local_folders };

    let scanners: Vec<Box<dyn ScannerProvider>> = vec![
        Box::new(SteamScanner::new()),
        Box::new(EpicScanner::new()),
        Box::new(GogScanner::new()),
        Box::new(LocalAppScanner::new()),
    ];

    let metadata_provider =
        LocalCacheMetadataProvider::new(ImageCache::new(storage.image_cache_dir));
    let mut provider_reports = Vec::new();
    let mut all_candidates = Vec::new();

    for scanner in scanners {
        let source = scanner.source();
        match scanner.scan(&context) {
            Ok(mut candidates) => {
                let discovered = candidates.len();
                all_candidates.append(&mut candidates);
                provider_reports.push(ScanProviderReport {
                    source,
                    discovered,
                    error: None,
                });
            }
            Err(error) => {
                provider_reports.push(ScanProviderReport {
                    source,
                    discovered: 0,
                    error: Some(error),
                });
            }
        }
    }

    let candidates = dedupe_candidates(all_candidates);
    let mut inserted_or_updated = 0;
    let mut metadata_matched = 0;
    let mut unmatched = 0;

    for candidate in &candidates {
        let metadata = metadata_provider.resolve(candidate)?;
        if metadata.is_some() {
            metadata_matched += 1;
        } else {
            unmatched += 1;
        }
        upsert_candidate(&conn, candidate, metadata.as_ref())?;
        inserted_or_updated += 1;
    }

    let games = list_games(&conn, false)?;

    Ok(ScanSummary {
        discovered: candidates.len(),
        inserted_or_updated,
        metadata_matched,
        unmatched,
        providers: provider_reports,
        games,
    })
}

fn dedupe_candidates(candidates: Vec<GameCandidate>) -> Vec<GameCandidate> {
    let mut by_identity: HashMap<String, GameCandidate> = HashMap::new();

    for candidate in candidates {
        let key = match (&candidate.source, &candidate.app_id) {
            (source, Some(app_id)) => format!("source:{}:{app_id}", source.as_str()),
            (_, None) => format!(
                "path:{}",
                candidate.install_path.to_string_lossy().to_lowercase()
            ),
        };

        by_identity.entry(key).or_insert(candidate);
    }

    let mut unique: Vec<GameCandidate> = by_identity.into_values().collect();
    unique.sort_by(|left, right| left.title.to_lowercase().cmp(&right.title.to_lowercase()));
    unique
}

#[allow(dead_code)]
pub struct LibraryExpansionHooks;

#[allow(dead_code)]
impl LibraryExpansionHooks {
    pub const ACHIEVEMENTS_DB: &'static str = "databases/achievements.db";
    pub const SAVES_DB: &'static str = "databases/saves.db";
    pub const MODS_DB: &'static str = "databases/mods.db";
    pub const ACCOUNTS_DB: &'static str = "databases/accounts.db";
}

#[allow(dead_code)]
pub trait AccountLinkedLibraryProvider: ScannerProvider {
    fn account_provider_id(&self) -> &'static str;
}

#[allow(dead_code)]
pub trait LaunchProvider {
    fn source(&self) -> GameSource;
    fn launch(&self, game: &LibraryGame) -> Result<(), String>;
}
