defmodule Geolixir do
  @moduledoc """
  Geolixir provides a simple and unified interface for geocoding and reverse
  geocoding using various external providers.

  It allows converting addresses to geographical coordinates (latitude and longitude)
  and vice-versa.

  ## Usage

  Add Geolixir to your `mix.exs` dependencies:

  ```elixir
  def deps do
    [
      {:geolixir, "~> 0.1.2"}
    ]
  end
  ```

  Then fetch the dependencies:

  ```bash
  mix deps.get
  ```

  ### Geocoding

  ```elixir
  # Using the default provider (OpenStreetMap)
  iex> Geolixir.geocode("1600 Amphitheatre Parkway, Mountain View, CA")
  {:ok, %Geolixir.Result{
    coordinates: %Geolixir.Coords{lat: 37.4224764, lon: -122.0842499},
    location: %Geolixir.Location{...},
    ...
  }}

  # Using a specific provider (e.g., Geoapify, requires API key)
  iex> Geolixir.geocode("Eiffel Tower, Paris", provider: :geoapify, api_key: "YOUR_GEOAPIFY_KEY")
  {:ok, %Geolixir.Result{...}}
  ```

  ### Reverse Geocoding

  ```elixir
  # Using the default provider
  iex> Geolixir.reverse_geocode(48.8584, 2.2945)
  {:ok, %Geolixir.Result{
    location: %Geolixir.Location{formatted_address: "Tour Eiffel, 5, Avenue Anatole France, ...", ...},
    ...
  }}

  # Using a specific provider (e.g., PositionStack, requires API key)
  iex> Geolixir.reverse_geocode(40.7128, -74.0060, provider: :position_stack, api_key: "YOUR_POSITIONSTACK_KEY")
  {:ok, %Geolixir.Result{...}}
  ```

  See `geocode/2` and `reverse_geocode/3` for more details and options.
  """

  alias Geolixir.Result

  @provider_modules %{
    open_street_maps: Geolixir.Providers.OpenStreetMap,
    geoapify: Geolixir.Providers.Geoapify,
    geocoding: Geolixir.Providers.Geocoding,
    position_stack: Geolixir.Providers.PositionStack
  }

  @default_provider :open_street_maps

  @doc """
  Geocodes a given address string into geographical coordinates.

  It uses the configured default provider unless a specific `:provider` is
  specified in the options. Some providers may require an `:api_key`.

  ## Parameters
    - `address`: The address string to geocode (e.g., "1600 Amphitheatre Parkway, Mountain View, CA").
    - `opts`: A keyword list of options.
      - `:provider`: (Optional) The atom representing the provider module to use (e.g., `:geoapify`, `:open_street_maps`). Defaults to `#{inspect(@default_provider)}`.
      - `:api_key`: (Optional) The API key required by some providers.
      - Other options specific to the chosen provider may also be passed.

  ## Returns
    - `{:ok, %Result{}}`: On success, returns a tuple with `:ok` and a `Geolixir.Result` struct containing coordinates, location details, bounds, and metadata.
    - `{:error, reason}`: On failure, returns a tuple with `:error` and the reason (e.g., `:no_results_found`, `:invalid_api_key`, HTTP error details).

  ## Examples

      iex> Geolixir.geocode("Eiffel Tower, Paris")
      {:ok, %Geolixir.Result{coordinates: %Geolixir.Coords{lat: 48.858..., lon: 2.294...}, ...}}

      iex> Geolixir.geocode("NonExistentPlace12345", provider: :open_street_maps)
      {:error, :no_results_found} # Example error, actual error may vary

      iex> Geolixir.geocode("Statue of Liberty", provider: :geoapify, api_key: "YOUR_KEY")
      {:ok, %Geolixir.Result{...}} # Requires a valid Geoapify key

      iex> Geolixir.geocode("Some Address", provider: :unknown_provider)
      ** (ArgumentError) Unknown provider: unknown_provider. Available providers: [...]
  """
  @spec geocode(String.t(), keyword()) :: {:ok, Result.t()} | {:error, atom() | map()}
  def geocode(address, opts \\ []) do
    provider = resolve_provider(opts[:provider] || @default_provider)
    provider.geocode(%{address: address}, opts)
  end

  @doc """
  Reverse geocodes geographical coordinates (latitude and longitude) into an address.

  It uses the configured default provider unless a specific `:provider` is
  specified in the options. Some providers may require an `:api_key`.

  ## Parameters
    - `lat`: The latitude.
    - `lon`: The longitude.
    - `opts`: A keyword list of options.
      - `:provider`: (Optional) The atom representing the provider module to use (e.g., `:geoapify`, `:open_street_maps`). Defaults to `#{inspect(@default_provider)}`.
      - `:api_key`: (Optional) The API key required by some providers.
      - Other options specific to the chosen provider may also be passed.

  ## Returns
    - `{:ok, %Result{}}`: On success, returns a tuple with `:ok` and a `Geolixir.Result` struct containing location details, coordinates, bounds, and metadata.
    - `{:error, reason}`: On failure, returns a tuple with `:error` and the reason (e.g., `:no_results_found`, `:invalid_coordinates`, HTTP error details).

  ## Examples

      iex> Geolixir.reverse_geocode(40.7128, -74.0060) # New York City Hall approx.
      {:ok, %Geolixir.Result{location: %Geolixir.Location{formatted_address: "City Hall Park, ...", ...}, ...}}

      iex> Geolixir.reverse_geocode(999.0, 999.0)
      {:error, :invalid_coordinates} # Example error, actual error may vary

      iex> Geolixir.reverse_geocode(51.5074, -0.1278, provider: :position_stack, api_key: "YOUR_KEY")
      {:ok, %Geolixir.Result{...}} # Requires a valid PositionStack key
  """
  @spec reverse_geocode(lat :: float(), lon :: float(), opts :: keyword()) ::
          {:ok, Result.t()} | {:error, atom() | map()}
  def reverse_geocode(lat, lon, opts \\ []) do
    provider = resolve_provider(opts[:provider] || @default_provider)
    provider.reverse_geocode(%{lat: lat, lon: lon}, opts)
  end

  # Private function to resolve the provider
  defp resolve_provider(provider_key) when is_atom(provider_key) do
    case @provider_modules[provider_key] do
      nil ->
        raise ArgumentError,
              "Unknown provider: #{provider_key}. Available providers: #{inspect(Map.keys(@provider_modules))}"

      module ->
        module
    end
  end
end
