Skip to content
use serde::Deserialize;
use std::sync::Arc;
use tokio::sync::Semaphore;

use crate::models::{Pokemon, PokemonMove};

const USER_AGENT: &str = "pokedex-autobattler/1.0 (contact: https://github.com)";

#[derive(Deserialize)]
struct ApiPokemon {
    id: i32,
    name: String,
    height: i32,
    weight: i32,
    stats: Vec<ApiStatSlot>,
    types: Vec<ApiTypeSlot>,
    sprites: ApiSprites,
    moves: Vec<ApiMoveSlot>,
}

#[derive(Deserialize)]
struct ApiStatSlot {
    base_stat: i32,
    stat: ApiNamedResource,
}

#[derive(Deserialize)]
struct ApiTypeSlot {
    slot: i32,
    #[serde(rename = "type")]
    type_info: ApiNamedResource,
}

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

#[derive(Deserialize)]
struct ApiSprites {
    front_default: Option<String>,
    other: Option<ApiSpritesOther>,
}

#[derive(Deserialize)]
struct ApiSpritesOther {
    #[serde(rename = "official-artwork")]
    official_artwork: Option<ApiOfficialArtwork>,
}

#[derive(Deserialize)]
struct ApiOfficialArtwork {
    front_default: Option<String>,
}

#[derive(Deserialize)]
struct ApiMoveSlot {
    #[serde(rename = "move")]
    move_info: ApiNamedResource,
    version_group_details: Vec<ApiVersionGroupDetail>,
}

#[derive(Deserialize)]
struct ApiVersionGroupDetail {
    level_learned_at: i32,
    move_learn_method: ApiNamedResource,
    version_group: ApiNamedResource,
}

#[derive(Deserialize)]
struct ApiSpecies {
    names: Vec<ApiName>,
    genera: Vec<ApiGenus>,
    flavor_text_entries: Vec<ApiFlavorText>,
}

#[derive(Deserialize)]
struct ApiName {
    name: String,
    language: ApiNamedResource,
}

#[derive(Deserialize)]
struct ApiGenus {
    genus: String,
    language: ApiNamedResource,
}

#[derive(Deserialize)]
struct ApiFlavorText {
    flavor_text: String,
    language: ApiNamedResource,
    version: ApiNamedResource,
}

pub fn create_agent() -> ureq::Agent {
    ureq::Agent::new_with_defaults()
}

#[allow(clippy::too_many_lines)]
pub fn fetch_single_pokemon(agent: &ureq::Agent, id: i32) -> Result<Pokemon, String> {
    let pokemon_url = format!("https://pokeapi.co/api/v2/pokemon/{id}");
    let mut resp = agent
        .get(&pokemon_url)
        .header("User-Agent", USER_AGENT)
        .call()
        .map_err(|e| format!("Failed to fetch pokemon #{id}: {e}"))?;

    let api_pk: ApiPokemon = resp
        .body_mut()
        .read_json()
        .map_err(|e| format!("Failed to parse pokemon #{id} json: {e}"))?;

    let species_url = format!("https://pokeapi.co/api/v2/pokemon-species/{id}");
    let mut species_resp = agent
        .get(&species_url)
        .header("User-Agent", USER_AGENT)
        .call()
        .map_err(|e| format!("Failed to fetch species #{id}: {e}"))?;

    let api_sp: ApiSpecies = species_resp
        .body_mut()
        .read_json()
        .map_err(|e| format!("Failed to parse species #{id} json: {e}"))?;

    let stat_val = |stat_name: &str| -> i32 {
        api_pk
            .stats
            .iter()
            .find(|s| s.stat.name == stat_name)
            .map_or(0, |s| s.base_stat)
    };

    let hp = stat_val("hp");
    let attack = stat_val("attack");
    let defense = stat_val("defense");
    let sp_attack = stat_val("special-attack");
    let sp_defense = stat_val("special-defense");
    let speed = stat_val("speed");

    let mut types_slots = api_pk.types;
    types_slots.sort_by_key(|t| t.slot);
    let types: Vec<String> = types_slots.into_iter().map(|t| t.type_info.name).collect();

    let sprite_url = api_pk.sprites.front_default.unwrap_or_else(|| {
        format!("https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{id}.png")
    });

    let artwork_url = api_pk
        .sprites
        .other
        .and_then(|o| o.official_artwork)
        .and_then(|a| a.front_default)
        .unwrap_or_else(|| {
            format!("https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/{id}.png")
        });

    let mut moves = Vec::new();
    for m in api_pk.moves {
        if let Some(vgd) = m
            .version_group_details
            .into_iter()
            .find(|v| v.version_group.name == "firered-leafgreen")
        {
            moves.push(PokemonMove {
                name: m.move_info.name,
                level: vgd.level_learned_at,
                method: vgd.move_learn_method.name,
            });
        }
    }

    let mut level_moves: Vec<PokemonMove> = moves
        .iter()
        .filter(|m| m.method == "level-up")
        .cloned()
        .collect();
    level_moves.sort_by_key(|m| m.level);

    let mut other_moves: Vec<PokemonMove> = moves
        .into_iter()
        .filter(|m| m.method != "level-up")
        .collect();
    other_moves.sort_by(|a, b| a.name.cmp(&b.name));

    let mut sorted_moves = level_moves;
    sorted_moves.extend(other_moves);

    let display_name = api_sp
        .names
        .iter()
        .find(|n| n.language.name == "en")
        .map_or_else(
            || {
                let mut c = api_pk.name.chars();
                match c.next() {
                    None => String::new(),
                    Some(first) => first.to_uppercase().collect::<String>() + c.as_str(),
                }
            },
            |n| n.name.clone(),
        );

    let genus = api_sp
        .genera
        .iter()
        .find(|g| g.language.name == "en")
        .map(|g| g.genus.clone())
        .unwrap_or_default();

    let raw_desc = api_sp
        .flavor_text_entries
        .iter()
        .find(|f| {
            f.language.name == "en" && (f.version.name == "firered" || f.version.name == "leafgreen")
        })
        .or_else(|| api_sp.flavor_text_entries.iter().find(|f| f.language.name == "en"))
        .map(|f| f.flavor_text.as_str())
        .unwrap_or_default();

    let cleaned_desc = raw_desc
        .replace(['\n', '\x0c', '\r'], " ")
        .split_whitespace()
        .collect::<Vec<&str>>()
        .join(" ");

    Ok(Pokemon {
        id: api_pk.id,
        name: api_pk.name,
        display_name,
        types,
        hp,
        attack,
        defense,
        sp_attack,
        sp_defense,
        speed,
        height: api_pk.height,
        weight: api_pk.weight,
        sprite_url,
        artwork_url,
        genus,
        description: cleaned_desc,
        moves: sorted_moves,
    })
}

pub async fn fetch_gen1_pokemon_concurrent(
    ids: Vec<i32>,
    concurrency: usize,
) -> Result<Vec<Pokemon>, String> {
    let semaphore = Arc::new(Semaphore::new(concurrency));
    let agent = Arc::new(create_agent());
    let mut tasks = Vec::with_capacity(ids.len());

    for id in ids {
        let sem = semaphore.clone();
        let ag = agent.clone();
        tasks.push(tokio::spawn(async move {
            let _permit = sem
                .acquire()
                .await
                .map_err(|e| format!("Semaphore acquire error: {e}"))?;
            tokio::task::spawn_blocking(move || fetch_single_pokemon(&ag, id))
                .await
                .map_err(|e| format!("Join error for pokemon #{id}: {e}"))?
        }));
    }

    let mut results = Vec::with_capacity(tasks.len());
    for task in tasks {
        match task.await {
            Ok(Ok(pokemon)) => results.push(pokemon),
            Ok(Err(e)) => return Err(e),
            Err(e) => return Err(format!("Task execution error: {e}")),
        }
    }

    results.sort_by_key(|p| p.id);
    Ok(results)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[ignore = "requires network connection to pokeapi.co"]
    fn test_fetch_bulbasaur() {
        let agent = create_agent();
        let pk = fetch_single_pokemon(&agent, 1).expect("fetch bulbasaur");
        assert_eq!(pk.id, 1);
        assert_eq!(pk.name, "bulbasaur");
        assert_eq!(pk.display_name, "Bulbasaur");
        assert_eq!(pk.types, vec!["grass", "poison"]);
        assert_eq!(pk.genus, "Seed Pokémon");
        assert!(pk.hp > 0);
        assert!(!pk.moves.is_empty());
    }

    #[tokio::test]
    #[ignore = "requires network connection to pokeapi.co"]
    async fn test_fetch_multiple_concurrent() {
        let pks = fetch_gen1_pokemon_concurrent(vec![1, 2, 3], 3)
            .await
            .expect("fetch 1..=3");
        assert_eq!(pks.len(), 3);
        assert_eq!(pks[0].name, "bulbasaur");
        assert_eq!(pks[1].name, "ivysaur");
        assert_eq!(pks[2].name, "venusaur");
    }
}