defmodule Microwaveprop.Qrz do
  @moduledoc """
  Callsign-lookup facade around the QRZ.com XML API.

  Results are cached in the `qrz_callsigns` table keyed by uppercased
  callsign with a configurable TTL (`cache_ttl_hours`, default 168 / 7
  days). Entries that are still fresh return from the DB without
  touching the upstream.
  """
  import Ecto.Query

  alias Microwaveprop.Qrz.Callsign
  alias Microwaveprop.Qrz.Client
  alias Microwaveprop.Qrz.Record
  alias Microwaveprop.Repo

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

    case get_fresh_cache(callsign) do
      %Callsign{data: data} ->
        {:ok, Record.from_map(data)}

      nil ->
        fetch_and_upsert(callsign)
    end
  end

  @spec get_callsign(String.t()) :: Callsign.t() | nil
  def get_callsign(callsign) do
    callsign = String.upcase(callsign)
    Repo.get_by(Callsign, callsign: callsign)
  end

  defp get_fresh_cache(callsign) do
    ttl_hours = config()[:cache_ttl_hours] || 168
    cutoff = DateTime.add(DateTime.utc_now(), -ttl_hours, :hour)

    Callsign
    |> where([c], c.callsign == ^callsign and c.updated_at > ^cutoff)
    |> Repo.one()
  end

  defp fetch_and_upsert(callsign) do
    case Client.lookup(callsign) do
      {:ok, data} ->
        now = DateTime.utc_now(:second)

        result =
          %Callsign{}
          |> Callsign.changeset(%{callsign: callsign, data: data})
          |> Repo.insert(
            on_conflict: [set: [data: data, updated_at: now]],
            conflict_target: :callsign,
            returning: true
          )

        case result do
          {:ok, %Callsign{data: data}} -> {:ok, Record.from_map(data)}
          {:error, changeset} -> {:error, "Failed to save: #{inspect(changeset.errors)}"}
        end

      {:error, _} = error ->
        error
    end
  end

  defp config do
    Application.get_env(:microwaveprop, __MODULE__, [])
  end
end
