defmodule Microwaveprop.Radio.ImportMatcher do
  @moduledoc """
  Pure classification for CSV/ADIF upload rows against existing Contact records.

  `prefix_key/5` builds a sortable key from `{station, 4-char grid prefix, band}`
  so callers can bulk-lookup candidates from the database. `classify_against_existing/2`
  compares a single upload row against a single candidate contact and decides
  whether the upload is a plain duplicate, a grid/mode refinement of the existing
  record, or an irreconcilable contradiction that must be skipped.
  """

  alias Microwaveprop.Radio.Contact

  @allowed_modes ~w(CW SSB FM FT8 FT4 Q65)

  @type prefix_key ::
          {{String.t(), String.t()}, {String.t(), String.t()}, integer()} | :invalid

  @type change_map :: %{optional(:grid1) => String.t(), optional(:grid2) => String.t(), optional(:mode) => String.t()}
  @type classification :: :duplicate | {:refinement, change_map()} | :contradiction

  @spec prefix_key(String.t() | nil, String.t() | nil, String.t() | nil, String.t() | nil, term()) ::
          prefix_key()
  def prefix_key(station1, grid1, station2, grid2, band) do
    with {:ok, s1, g1_prefix} <- build_side(station1, grid1),
         {:ok, s2, g2_prefix} <- build_side(station2, grid2),
         {:ok, band_int} <- normalize_band(band) do
      [a, b] = Enum.sort([{s1, g1_prefix}, {s2, g2_prefix}])
      {a, b, band_int}
    else
      _ -> :invalid
    end
  end

  defp build_side(station, grid) do
    with station_up when is_binary(station_up) <- upcase_trim(station),
         grid_up when is_binary(grid_up) <- upcase_trim(grid),
         true <- byte_size(grid_up) >= 4 do
      {:ok, station_up, binary_part(grid_up, 0, 4)}
    else
      _ -> :error
    end
  end

  defp upcase_trim(nil), do: nil

  defp upcase_trim(value) when is_binary(value) do
    trimmed = String.trim(value)
    if trimmed == "", do: nil, else: String.upcase(trimmed)
  end

  defp upcase_trim(_), do: nil

  defp normalize_band(%Decimal{} = d) do
    {:ok, Decimal.to_integer(d)}
  rescue
    _ -> :error
  end

  defp normalize_band(value) when is_integer(value), do: {:ok, value}

  defp normalize_band(value) when is_binary(value) do
    case value |> String.trim() |> Integer.parse() do
      {int, ""} -> {:ok, int}
      _ -> :error
    end
  end

  defp normalize_band(_), do: :error

  @spec classify_against_existing(map(), Contact.t()) :: classification()
  def classify_against_existing(upload_attrs, %Contact{} = existing) do
    up_s1 = upcase_trim(upload_attrs["station1"])
    up_s2 = upcase_trim(upload_attrs["station2"])
    ex_s1 = upcase_trim(existing.station1)
    ex_s2 = upcase_trim(existing.station2)

    case resolve_direction(ex_s1, ex_s2, up_s1, up_s2) do
      :ambiguous -> :duplicate
      {up_grid1_key, up_grid2_key} -> compare_sides(existing, upload_attrs, up_grid1_key, up_grid2_key)
    end
  end

  # Returns which upload keys ("grid1"/"grid2") correspond to existing's side1/side2.
  # If direction matches as-is: {"grid1", "grid2"}. If swapped: {"grid2", "grid1"}.
  # `:ambiguous` when callsigns don't pair cleanly (e.g. same callsign both sides).
  defp resolve_direction(ex_s1, ex_s2, up_s1, up_s2) do
    if pairable_callsigns?(ex_s1, ex_s2, up_s1, up_s2) do
      match_direction(ex_s1, ex_s2, up_s1, up_s2)
    else
      :ambiguous
    end
  end

  defp pairable_callsigns?(ex_s1, ex_s2, up_s1, up_s2) do
    not is_nil(ex_s1) and not is_nil(ex_s2) and not is_nil(up_s1) and not is_nil(up_s2) and
      ex_s1 != ex_s2 and up_s1 != up_s2
  end

  defp match_direction(ex_s1, ex_s2, up_s1, up_s2) do
    cond do
      ex_s1 == up_s1 and ex_s2 == up_s2 -> {"grid1", "grid2"}
      ex_s1 == up_s2 and ex_s2 == up_s1 -> {"grid2", "grid1"}
      true -> :ambiguous
    end
  end

  defp compare_sides(existing, upload_attrs, up_grid1_key, up_grid2_key) do
    up_g1 = upcase_trim(upload_attrs[up_grid1_key])
    up_g2 = upcase_trim(upload_attrs[up_grid2_key])
    ex_g1 = upcase_trim(existing.grid1)
    ex_g2 = upcase_trim(existing.grid2)

    grid1_result = compare_grid(ex_g1, up_g1)
    grid2_result = compare_grid(ex_g2, up_g2)
    mode_result = compare_mode(existing.mode, upload_attrs["mode"])

    combine_results(grid1_result, grid2_result, mode_result)
  end

  # Each compare_* returns :same | :contradiction | {:refine, new_value}
  defp compare_grid(nil, _up), do: :same
  defp compare_grid(_ex, nil), do: :same
  defp compare_grid(ex, up) when ex == up, do: :same

  defp compare_grid(ex, up) do
    cond do
      byte_size(up) > byte_size(ex) and String.starts_with?(up, ex) -> {:refine, up}
      byte_size(up) < byte_size(ex) and String.starts_with?(ex, up) -> :same
      true -> :contradiction
    end
  end

  defp compare_mode(_ex, nil), do: :same

  defp compare_mode(nil, up) do
    normalized = upcase_trim(up)

    cond do
      is_nil(normalized) -> :same
      normalized in @allowed_modes -> {:refine, normalized}
      true -> :same
    end
  end

  defp compare_mode(ex, up) do
    up_norm = upcase_trim(up)

    cond do
      is_nil(up_norm) -> :same
      String.upcase(ex) == up_norm -> :same
      true -> :contradiction
    end
  end

  defp combine_results(grid1_result, grid2_result, mode_result) do
    results = [{:grid1, grid1_result}, {:grid2, grid2_result}, {:mode, mode_result}]

    if Enum.any?(results, fn {_k, r} -> r == :contradiction end) do
      :contradiction
    else
      changes =
        Enum.reduce(results, %{}, fn
          {key, {:refine, value}}, acc -> Map.put(acc, key, value)
          _, acc -> acc
        end)

      if changes == %{}, do: :duplicate, else: {:refinement, changes}
    end
  end
end
