defmodule Microwaveprop.Weather.SolarClient do
  @moduledoc false

  @gfz_url "https://kp.gfz.de/app/files/Kp_ap_Ap_SN_F107_since_1932.txt"

  @spec data_url() :: String.t()
  def data_url, do: @gfz_url

  @spec fetch_solar_indices() :: {:ok, [map()]} | {:error, term()}
  def fetch_solar_indices do
    Microwaveprop.Instrument.span([:solar, :fetch_indices], %{}, fn ->
      req_options = Application.get_env(:microwaveprop, :solar_req_options, [])

      case Req.get(@gfz_url, [receive_timeout: 120_000, retry: false] ++ req_options) do
        {:ok, %{status: 200, body: body}} ->
          {:ok, parse_gfz_file(body)}

        {:ok, %{status: status}} ->
          {:error, "HTTP #{status}"}

        {:error, reason} ->
          {:error, reason}
      end
    end)
  end

  @spec filter_since([map()], Date.t()) :: [map()]
  def filter_since(records, since_date) do
    Enum.filter(records, fn r -> Date.compare(r.date, since_date) != :lt end)
  end

  @spec parse_gfz_file(String.t()) :: [map()]
  def parse_gfz_file(text) do
    text
    |> String.split("\n")
    |> Enum.reject(fn line ->
      trimmed = String.trim(line)
      trimmed == "" or String.starts_with?(trimmed, "#")
    end)
    |> Enum.flat_map(fn line ->
      case parse_gfz_row(line) do
        {:ok, record} -> [record]
        :error -> []
      end
    end)
  end

  @spec parse_gfz_row(String.t()) :: {:ok, map()} | :error
  def parse_gfz_row(line) do
    fields = String.split(line)

    if length(fields) >= 28 do
      [yyyy, mm, dd | rest] = fields

      # Skip days, days_m, Bsr, dB (indices 0-3 of rest)
      # Kp1-Kp8 at rest indices 4-11
      # ap1-ap8 at rest indices 12-19
      # Ap at rest index 20
      # SN at rest index 21
      # F10.7obs at rest index 22
      # F10.7adj at rest index 23
      kp_raw = Enum.slice(rest, 4, 8)
      ap_daily = Enum.at(rest, 20)
      sn = Enum.at(rest, 21)
      f107_obs = Enum.at(rest, 22)
      f107_adj = Enum.at(rest, 23)

      {:ok,
       %{
         date: Date.new!(String.to_integer(yyyy), String.to_integer(mm), String.to_integer(dd)),
         sfi: parse_float_or_nil(f107_obs),
         sfi_adjusted: parse_float_or_nil(f107_adj),
         sunspot_number: parse_int_or_nil(sn),
         ap_index: parse_int_or_nil(ap_daily),
         kp_values: Enum.map(kp_raw, &parse_float_or_nil/1)
       }}
    else
      :error
    end
  end

  defp parse_float_or_nil(str) do
    val = String.to_float(str)
    if val < 0, do: nil, else: val
  rescue
    ArgumentError ->
      case Integer.parse(str) do
        {n, _} when n < 0 -> nil
        {n, _} -> n / 1
        :error -> nil
      end
  end

  defp parse_int_or_nil(str) do
    case Integer.parse(str) do
      {n, _} when n < 0 -> nil
      {n, _} -> n
      :error -> nil
    end
  end
end
