defmodule CobblemonUi.PokeApi do @moduledoc """ Fetches and caches Pokémon type data from PokeAPI. Uses an ETS table for fast lookups. Data is fetched once per species and cached for the lifetime of the process. """ use GenServer require Logger @table :poke_api_types_cache # --------------------------------------------------------------------------- # Client API # --------------------------------------------------------------------------- def start_link(_opts) do GenServer.start_link(__MODULE__, [], name: __MODULE__) end @doc """ Returns a list of type names (lowercase strings) for the given species. Example: `get_types("bulbasaur")` → `["grass", "poison"]` """ @spec get_types(String.t()) :: [String.t()] def get_types(species) when is_binary(species) do key = String.downcase(species) case :ets.lookup(@table, key) do [{^key, types}] -> types [] -> GenServer.call(__MODULE__, {:fetch_types, key}, 15_000) end end # --------------------------------------------------------------------------- # Server callbacks # --------------------------------------------------------------------------- @impl true def init(_opts) do :ets.new(@table, [:named_table, :set, :public, read_concurrency: true]) {:ok, %{}} end @impl true def handle_call({:fetch_types, species}, _from, state) do result = case :ets.lookup(@table, species) do [{^species, types}] -> types [] -> types = fetch_types(species) :ets.insert(@table, {species, types}) types end {:reply, result, state} end # --------------------------------------------------------------------------- # Private helpers # --------------------------------------------------------------------------- defp fetch_types(species) do url = "https://pokeapi.co/api/v2/pokemon/#{species}" case Req.get(url) do {:ok, %Req.Response{status: 200, body: %{"types" => types}}} -> types |> Enum.sort_by(fn t -> t["slot"] end) |> Enum.map(fn t -> get_in(t, ["type", "name"]) end) |> Enum.reject(&is_nil/1) {:ok, %Req.Response{status: status}} -> Logger.warning("[PokeApi] Type lookup failed for #{species}: HTTP #{status}") [] {:error, reason} -> Logger.warning("[PokeApi] Type lookup failed for #{species}: #{inspect(reason)}") [] end end end