defmodule Microwaveprop.CallsignLocation do
  @moduledoc """
  Resolves a callsign to a lat/lon + Maidenhead grid by combining a
  QRZ.com lookup (licensee address) with the Google Maps geocoder,
  caching the result in `callsign_locations`.

  If QRZ already has `<lat>`/`<lon>` fields for the callsign, those are
  used directly and the geocoder is skipped. Otherwise the QRZ address
  is joined into a single line and sent to Google's geocoding endpoint.
  """
  alias Microwaveprop.CallsignLocation.Location
  alias Microwaveprop.Geocoder
  alias Microwaveprop.Qrz
  alias Microwaveprop.Radio.Maidenhead
  alias Microwaveprop.Repo

  @type result :: %{
          callsign: String.t(),
          latitude: float(),
          longitude: float(),
          gridsquare: String.t(),
          name: String.t() | nil,
          address: String.t() | nil
        }

  @spec lookup(String.t()) :: {:ok, result()} | {:error, String.t()}
  def lookup(callsign) do
    callsign = String.upcase(callsign)

    case Repo.get_by(Location, callsign: callsign) do
      %Location{} = cached ->
        {:ok, format_cached(cached)}

      nil ->
        fetch_and_cache(callsign)
    end
  end

  defp fetch_and_cache(callsign) do
    with {:ok, record} <- Qrz.lookup_callsign(callsign),
         {:ok, %{lat: lat, lon: lon}, address} <- resolve_coordinates(record) do
      gridsquare = Maidenhead.from_latlon(lat, lon, 8)
      name = build_name(record)

      {:ok, _} =
        %Location{}
        |> Location.changeset(%{
          callsign: callsign,
          latitude: lat,
          longitude: lon,
          gridsquare: gridsquare
        })
        |> Repo.insert(
          on_conflict: [
            set: [
              latitude: lat,
              longitude: lon,
              gridsquare: gridsquare,
              updated_at: DateTime.utc_now(:second)
            ]
          ],
          conflict_target: :callsign,
          returning: true
        )

      {:ok,
       %{
         callsign: callsign,
         latitude: lat,
         longitude: lon,
         gridsquare: gridsquare,
         name: name,
         address: address
       }}
    end
  end

  defp resolve_coordinates(record) do
    address = build_address_string(record)

    case {record.lat, record.lon} do
      {lat, lon} when is_float(lat) and is_float(lon) ->
        {:ok, %{lat: lat, lon: lon}, address}

      _ ->
        with {:ok, addr} <- require_address(address),
             {:ok, coords} <- Geocoder.geocode(addr) do
          {:ok, coords, addr}
        end
    end
  end

  defp build_address_string(record) do
    parts =
      Enum.reject(
        [record.addr1, record.addr2, record.state, record.zip, record.country],
        &(is_nil(&1) or String.trim(&1) == "")
      )

    if parts == [], do: nil, else: Enum.join(parts, ", ")
  end

  defp require_address(nil), do: {:error, "No address available for callsign"}
  defp require_address(address), do: {:ok, address}

  defp build_name(record) do
    joined =
      [record.fname, record.name]
      |> Enum.reject(&is_nil/1)
      |> Enum.join(" ")

    if joined == "", do: nil, else: joined
  end

  defp format_cached(%Location{} = loc) do
    %{
      callsign: loc.callsign,
      latitude: loc.latitude,
      longitude: loc.longitude,
      gridsquare: loc.gridsquare,
      name: nil,
      address: nil
    }
  end
end
