use super::images::ImageCache;
use super::models::{
    infer_library_item_kind, GameCandidate, GameMetadata, GameSource, MetadataStatus,
};
use serde_json::Value;

pub mod providers {
    pub mod igdb {
        use crate::library::metadata::MetadataProvider;

        #[allow(dead_code)]
        pub trait IgdbMetadataProvider: MetadataProvider {}
    }

    pub mod rawg {
        use crate::library::metadata::MetadataProvider;

        #[allow(dead_code)]
        pub trait RawgMetadataProvider: MetadataProvider {}
    }

    pub mod steam {
        use crate::library::metadata::MetadataProvider;

        #[allow(dead_code)]
        pub trait SteamMetadataProvider: MetadataProvider {}
    }
}

pub trait MetadataProvider: Send + Sync {
    #[allow(dead_code)]
    fn provider_id(&self) -> &'static str;
    fn resolve(&self, candidate: &GameCandidate) -> Result<Option<GameMetadata>, String>;
}

pub struct LocalCacheMetadataProvider {
    image_cache: ImageCache,
}

impl LocalCacheMetadataProvider {
    pub fn new(image_cache: ImageCache) -> Self {
        Self { image_cache }
    }
}

impl MetadataProvider for LocalCacheMetadataProvider {
    fn provider_id(&self) -> &'static str {
        "local_cache"
    }

    fn resolve(&self, candidate: &GameCandidate) -> Result<Option<GameMetadata>, String> {
        if candidate.source == GameSource::Steam {
            return self.resolve_steam(candidate);
        }

        Ok(None)
    }
}

impl LocalCacheMetadataProvider {
    fn resolve_steam(&self, candidate: &GameCandidate) -> Result<Option<GameMetadata>, String> {
        let Some(app_id) = candidate.app_id.as_deref() else {
            return Ok(None);
        };

        let store_data = fetch_steam_store_data(app_id)?;
        let title = store_data
            .as_ref()
            .and_then(|value| value.get("name"))
            .and_then(Value::as_str)
            .map(ToString::to_string)
            .or_else(|| Some(candidate.title.clone()));
        let description = store_data
            .as_ref()
            .and_then(|value| value.get("short_description"))
            .and_then(Value::as_str)
            .map(ToString::to_string);
        let genres = store_data
            .as_ref()
            .and_then(|value| value.get("genres"))
            .and_then(Value::as_array)
            .map(|items| {
                items
                    .iter()
                    .filter_map(|item| item.get("description").and_then(Value::as_str))
                    .map(ToString::to_string)
                    .collect::<Vec<_>>()
            })
            .unwrap_or_default();
        let steam_categories = store_data
            .as_ref()
            .and_then(|value| value.get("categories"))
            .and_then(Value::as_array)
            .map(|items| {
                items
                    .iter()
                    .filter_map(|item| item.get("description").and_then(Value::as_str))
                    .map(ToString::to_string)
                    .collect::<Vec<_>>()
            })
            .unwrap_or_default();
        let steam_app_type = store_data
            .as_ref()
            .and_then(|value| value.get("type"))
            .and_then(Value::as_str)
            .map(ToString::to_string);
        let app_kind = infer_library_item_kind(
            steam_app_type.as_deref(),
            title.as_deref(),
            &genres,
            &steam_categories,
        );
        let release_date = store_data
            .as_ref()
            .and_then(|value| value.get("release_date"))
            .and_then(|value| value.get("date"))
            .and_then(Value::as_str)
            .map(ToString::to_string);
        let developer = joined_string_array(store_data.as_ref(), "developers");
        let publisher = joined_string_array(store_data.as_ref(), "publishers");
        let header_image = store_data
            .as_ref()
            .and_then(|value| value.get("header_image"))
            .and_then(Value::as_str)
            .map(ToString::to_string);

        let source = candidate.source.as_str();
        let cover_image = self.image_cache.cache_first_remote_image(
            source,
            app_id,
            "cover",
            "jpg",
            &[
                format!("https://cdn.cloudflare.steamstatic.com/steam/apps/{app_id}/library_600x900.jpg"),
                format!("https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/{app_id}/library_600x900.jpg"),
                format!("https://cdn.cloudflare.steamstatic.com/steam/apps/{app_id}/capsule_616x353.jpg"),
            ],
        )?;
        let hero_urls = [
            Some(format!(
                "https://cdn.cloudflare.steamstatic.com/steam/apps/{app_id}/library_hero.jpg"
            )),
            header_image.clone(),
            Some(format!(
                "https://cdn.cloudflare.steamstatic.com/steam/apps/{app_id}/header.jpg"
            )),
        ]
        .into_iter()
        .flatten()
        .collect::<Vec<_>>();
        let hero_image = self
            .image_cache
            .cache_first_remote_image(source, app_id, "hero", "jpg", &hero_urls)?;
        let icon_image = self.image_cache.cache_first_remote_image(
            source,
            app_id,
            "icon",
            "jpg",
            &[
                format!(
                    "https://cdn.cloudflare.steamstatic.com/steam/apps/{app_id}/capsule_184x69.jpg"
                ),
                format!("https://cdn.cloudflare.steamstatic.com/steam/apps/{app_id}/header.jpg"),
            ],
        )?;

        Ok(Some(GameMetadata {
            title,
            description,
            app_kind,
            steam_app_type,
            genres,
            steam_categories,
            release_date,
            developer,
            publisher,
            cover_image,
            hero_image,
            icon_image,
            status: MetadataStatus::Matched,
        }))
    }
}

fn fetch_steam_store_data(app_id: &str) -> Result<Option<Value>, String> {
    let url = format!("https://store.steampowered.com/api/appdetails?appids={app_id}");
    let response = match ureq::get(&url)
        .header("User-Agent", "NebulaOS/0.1 library-scanner")
        .call()
    {
        Ok(response) => response,
        Err(_) => return Ok(None),
    };

    if !response.status().is_success() {
        return Ok(None);
    }

    let mut response = response;
    let body = response
        .body_mut()
        .read_to_string()
        .map_err(|err| format!("Failed to read Steam metadata response: {err}"))?;
    let value: Value = serde_json::from_str(&body)
        .map_err(|err| format!("Failed to parse Steam metadata: {err}"))?;

    Ok(value
        .get(app_id)
        .filter(|entry| {
            entry
                .get("success")
                .and_then(Value::as_bool)
                .unwrap_or(false)
        })
        .and_then(|entry| entry.get("data"))
        .cloned())
}

fn joined_string_array(value: Option<&Value>, key: &str) -> Option<String> {
    value
        .and_then(|value| value.get(key))
        .and_then(Value::as_array)
        .map(|items| {
            items
                .iter()
                .filter_map(Value::as_str)
                .collect::<Vec<_>>()
                .join(", ")
        })
        .filter(|value| !value.is_empty())
}

#[allow(dead_code)]
pub struct MetadataMatchPlan {
    pub exact_app_id: bool,
    pub store_specific_lookup: bool,
    pub fuzzy_title_matching: bool,
    pub manual_user_correction: bool,
}

impl Default for MetadataMatchPlan {
    fn default() -> Self {
        Self {
            exact_app_id: true,
            store_specific_lookup: true,
            fuzzy_title_matching: true,
            manual_user_correction: true,
        }
    }
}
