alias Microwaveprop.Radio.Qso
alias Microwaveprop.Repo

# -- Config --
data_path = Path.expand("~/dev/ntms/propdata/data/microwave_contacts.json")
gridmap_base = "https://gridmap.org/grid/"
batch_size = 1000

IO.puts("Reading #{data_path}...")
data = data_path |> File.read!() |> Jason.decode!()

# -- Band normalization --
# Contest logs use "10G", "24G", etc. ARRL uses "10 GHz". UKuG uses band_ghz float.
band_map = %{
  "10G" => Decimal.new("10000"),
  "24G" => Decimal.new("24000"),
  "47G" => Decimal.new("47000"),
  "75G" => Decimal.new("75000"),
  "LIGHT" => Decimal.new("300000000")
}

defmodule BandParser do
  def parse(band) when is_binary(band) do
    case Map.get(band_map(), String.upcase(band)) do
      nil -> parse_ghz_string(band)
      val -> val
    end
  end

  def parse(ghz) when is_number(ghz) do
    ghz |> Decimal.from_float() |> Decimal.mult(Decimal.new("1000"))
  end

  def parse(_), do: nil

  defp parse_ghz_string(s) do
    case Float.parse(String.replace(s, ~r/\s*GHz\s*/i, "")) do
      {ghz, _} -> ghz |> Decimal.from_float() |> Decimal.mult(Decimal.new("1000"))
      :error -> nil
    end
  end

  defp band_map, do: unquote(Macro.escape(band_map))
end

# -- Collect all unique grids --
IO.puts("Collecting unique grids...")

collect_grids = fn records, grid_keys ->
  Enum.reduce(records, MapSet.new(), fn record, acc ->
    Enum.reduce(grid_keys, acc, fn key, inner_acc ->
      case Map.get(record, key) do
        nil -> inner_acc
        "" -> inner_acc
        grid -> MapSet.put(inner_acc, String.upcase(grid))
      end
    end)
  end)
end

contest_qsos =
  Enum.flat_map(data["contest_logs"], fn log -> Map.get(log, "qsos", []) end)

all_grids =
  MapSet.new()
  |> MapSet.union(collect_grids.(contest_qsos, ["grid1", "grid2"]))
  |> MapSet.union(collect_grids.(data["arrl_distance_records"], ["grid1", "grid2"]))
  |> MapSet.union(collect_grids.(data["ukug_distance_records"], ["grid1", "grid2"]))

IO.puts("Found #{MapSet.size(all_grids)} unique grids to look up")

# -- Fetch grid coordinates from gridmap.org --
IO.puts("Fetching coordinates from gridmap.org...")

grid_coords =
  all_grids
  |> Task.async_stream(
    fn grid ->
      case Req.get("#{gridmap_base}#{grid}") do
        {:ok, %{status: 200, body: %{"latitude" => lat, "longitude" => lng}}} ->
          {grid, %{"lat" => lat, "lon" => lng}}

        {:ok, resp} ->
          IO.puts("  Warning: grid #{grid} returned status #{resp.status}")
          {grid, nil}

        {:error, err} ->
          IO.puts("  Error fetching grid #{grid}: #{inspect(err)}")
          {grid, nil}
      end
    end,
    max_concurrency: 20,
    timeout: 15_000,
    ordered: false
  )
  |> Enum.reduce(%{}, fn
    {:ok, {grid, coords}}, acc -> Map.put(acc, grid, coords)
    {:exit, _reason}, acc -> acc
  end)

resolved = Enum.count(grid_coords, fn {_k, v} -> v != nil end)
IO.puts("Resolved #{resolved}/#{MapSet.size(all_grids)} grids")

# -- Haversine distance --
defmodule Haversine do
  @earth_radius_km 6371.0

  def distance_km(nil, _), do: nil
  def distance_km(_, nil), do: nil

  def distance_km(%{"lat" => lat1, "lon" => lng1}, %{"lat" => lat2, "lon" => lng2}) do
    dlat = deg_to_rad(lat2 - lat1)
    dlng = deg_to_rad(lng2 - lng1)
    lat1_r = deg_to_rad(lat1)
    lat2_r = deg_to_rad(lat2)

    a =
      :math.sin(dlat / 2) * :math.sin(dlat / 2) +
        :math.cos(lat1_r) * :math.cos(lat2_r) * :math.sin(dlng / 2) * :math.sin(dlng / 2)

    c = 2 * :math.atan2(:math.sqrt(a), :math.sqrt(1 - a))
    Decimal.from_float(Float.round(@earth_radius_km * c, 1))
  end

  defp deg_to_rad(deg), do: deg * :math.pi() / 180.0
end

# -- Build QSO rows --
IO.puts("Building QSO records...")
now = DateTime.utc_now() |> DateTime.truncate(:second)

lookup = fn grid ->
  case grid do
    nil -> nil
    "" -> nil
    g -> Map.get(grid_coords, String.upcase(g))
  end
end

parse_timestamp = fn date_str, time_str ->
  case {date_str, time_str} do
    {nil, _} ->
      nil

    {_, nil} ->
      nil

    {date, time} ->
      padded_time =
        time |> String.pad_leading(4, "0")

      case DateTime.new(
             Date.from_iso8601!(date),
             Time.new!(
               String.to_integer(String.slice(padded_time, 0, 2)),
               String.to_integer(String.slice(padded_time, 2, 2)),
               0
             ),
             "Etc/UTC"
           ) do
        {:ok, dt} -> DateTime.truncate(dt, :second)
        _ -> nil
      end
  end
end

parse_date_only = fn date_str ->
  cond do
    is_nil(date_str) or date_str == "" ->
      nil

    # ISO format: "2010-06-10"
    String.match?(date_str, ~r/^\d{4}-\d{2}-\d{2}$/) ->
      case DateTime.new(Date.from_iso8601!(date_str), ~T[00:00:00], "Etc/UTC") do
        {:ok, dt} -> DateTime.truncate(dt, :second)
        _ -> nil
      end

    # ARRL format: "22-Jun-07"
    String.match?(date_str, ~r/^\d{1,2}-\w{3}-\d{2}$/) ->
      months = %{
        "Jan" => 1,
        "Feb" => 2,
        "Mar" => 3,
        "Apr" => 4,
        "May" => 5,
        "Jun" => 6,
        "Jul" => 7,
        "Aug" => 8,
        "Sep" => 9,
        "Oct" => 10,
        "Nov" => 11,
        "Dec" => 12
      }

      [day, mon, yr] = String.split(date_str, "-")
      short_year = String.to_integer(yr)
      year = if short_year >= 30, do: 1900 + short_year, else: 2000 + short_year

      case DateTime.new(
             Date.new!(year, Map.fetch!(months, mon), String.to_integer(day)),
             ~T[00:00:00],
             "Etc/UTC"
           ) do
        {:ok, dt} -> DateTime.truncate(dt, :second)
        _ -> nil
      end

    true ->
      nil
  end
end

# Contest log QSOs
contest_rows =
  Enum.flat_map(data["contest_logs"], fn log ->
    Enum.map(Map.get(log, "qsos", []), fn q ->
      g1 = q["grid1"]
      g2 = q["grid2"]
      pos1 = lookup.(g1)
      pos2 = lookup.(g2)

      %{
        station1: q["station1"],
        station2: q["station2"],
        qso_timestamp: parse_timestamp.(q["date"], q["time"]),
        grid1: g1,
        grid2: g2,
        pos1: pos1,
        pos2: pos2,
        mode: q["mode"],
        band: BandParser.parse(q["band"]),
        distance_km: Haversine.distance_km(pos1, pos2),
        inserted_at: now,
        updated_at: now
      }
    end)
  end)

# ARRL distance records
arrl_rows =
  Enum.map(data["arrl_distance_records"], fn r ->
    g1 = r["grid1"]
    g2 = r["grid2"]
    pos1 = lookup.(g1)
    pos2 = lookup.(g2)

    %{
      id: Ecto.UUID.generate(),
      station1: r["station1"],
      station2: r["station2"],
      qso_timestamp: parse_date_only.(r["date"]),
      grid1: g1,
      grid2: g2,
      pos1: pos1,
      pos2: pos2,
      mode: r["mode"] || "N/A",
      band: BandParser.parse(r["band"]),
      distance_km:
        if(r["distance_km"], do: Decimal.from_float(r["distance_km"] / 1), else: nil),
      inserted_at: now,
      updated_at: now
    }
  end)

# UKuG distance records
ukug_rows =
  Enum.map(data["ukug_distance_records"], fn r ->
    g1 = r["grid1"]
    g2 = r["grid2"]
    pos1 = lookup.(g1)
    pos2 = lookup.(g2)

    %{
      id: Ecto.UUID.generate(),
      station1: r["station1"],
      station2: r["station2"],
      qso_timestamp: parse_date_only.(r["date"]),
      grid1: g1,
      grid2: g2,
      pos1: pos1,
      pos2: pos2,
      mode: r["mode"] || "N/A",
      band: BandParser.parse(r["band_ghz"]),
      distance_km:
        if(r["distance_km"], do: Decimal.from_float(r["distance_km"] / 1), else: nil),
      inserted_at: now,
      updated_at: now
    }
  end)

all_rows =
  (contest_rows ++ arrl_rows ++ ukug_rows)
  |> Enum.filter(fn row -> row.band != nil and row.qso_timestamp != nil end)

IO.puts("Total records to insert: #{length(all_rows)}")

# -- Insert in batches --
IO.puts("Inserting into database in batches of #{batch_size}...")

all_rows
|> Enum.chunk_every(batch_size)
|> Enum.with_index(1)
|> Enum.each(fn {batch, i} ->
  Repo.insert_all(Qso, batch)
  IO.puts("  Batch #{i}: inserted #{length(batch)} records")
end)

total = Repo.aggregate(Qso, :count)
IO.puts("Done! #{total} QSOs in database.")
