defmodule Microwaveprop.Radio.ContactEdit do
  @moduledoc false
  use Ecto.Schema

  import Ecto.Changeset

  alias Microwaveprop.Accounts.User
  alias Microwaveprop.Radio.Contact
  alias Microwaveprop.Radio.Maidenhead

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id

  @allowed_modes ~w(CW SSB FM FT8 FT4 Q65)
  @allowed_bands ~w(50 144 222 432 902 1296 2304 3400 5760 10000 24000 47000 68000 75000 122000 134000 241000)

  schema "contact_edits" do
    belongs_to :contact, Contact
    belongs_to :user, User
    belongs_to :reviewed_by, User

    field :proposed_changes, :map
    field :status, Ecto.Enum, values: [:pending, :approved, :rejected], default: :pending
    field :admin_note, :string
    field :reviewed_at, :utc_datetime

    timestamps(type: :utc_datetime)
  end

  @type t :: %__MODULE__{}

  @editable_fields ~w(station1 station2 grid1 grid2 band mode qso_timestamp height1_ft height2_ft private)

  @spec changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
  def changeset(edit, attrs) do
    edit
    |> cast(attrs, [:contact_id, :user_id, :proposed_changes])
    |> validate_required([:contact_id, :user_id, :proposed_changes])
    |> validate_proposed_changes()
    |> foreign_key_constraint(:contact_id)
    |> foreign_key_constraint(:user_id)
  end

  @spec review_changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
  def review_changeset(edit, attrs) do
    edit
    |> cast(attrs, [:status, :admin_note, :reviewed_by_id, :reviewed_at])
    |> validate_required([:status, :reviewed_by_id, :reviewed_at])
    |> validate_inclusion(:status, [:approved, :rejected])
  end

  defp validate_proposed_changes(changeset) do
    case get_field(changeset, :proposed_changes) do
      nil ->
        changeset

      changes when changes == %{} ->
        add_error(changeset, :proposed_changes, "must contain at least one change")

      changes when is_map(changes) ->
        keys = Map.keys(changes)
        invalid_keys = Enum.reject(keys, &(&1 in @editable_fields))

        changeset =
          if invalid_keys == [] do
            changeset
          else
            add_error(changeset, :proposed_changes, "contains invalid fields: #{Enum.join(invalid_keys, ", ")}")
          end

        Enum.reduce(keys, changeset, &validate_field_value/2)

      _ ->
        add_error(changeset, :proposed_changes, "must be a map")
    end
  end

  defp validate_field_value("station1", cs), do: validate_callsign_value(cs, "station1")
  defp validate_field_value("station2", cs), do: validate_callsign_value(cs, "station2")
  defp validate_field_value("grid1", cs), do: validate_grid_value(cs, "grid1")
  defp validate_field_value("grid2", cs), do: validate_grid_value(cs, "grid2")

  defp validate_field_value("band", changeset) do
    val = get_field(changeset, :proposed_changes)["band"]

    if to_string(val) in @allowed_bands do
      changeset
    else
      add_error(changeset, :proposed_changes, "band is not a valid frequency")
    end
  end

  defp validate_field_value("mode", changeset) do
    val = get_field(changeset, :proposed_changes)["mode"]

    if val in @allowed_modes do
      changeset
    else
      add_error(changeset, :proposed_changes, "mode must be one of: #{Enum.join(@allowed_modes, ", ")}")
    end
  end

  defp validate_field_value("qso_timestamp", changeset) do
    val = get_field(changeset, :proposed_changes)["qso_timestamp"]

    case val do
      %DateTime{} ->
        changeset

      s when is_binary(s) ->
        case DateTime.from_iso8601(s) do
          {:ok, _, _} -> changeset
          _ -> add_error(changeset, :proposed_changes, "qso_timestamp is not a valid datetime")
        end

      _ ->
        add_error(changeset, :proposed_changes, "qso_timestamp is not a valid datetime")
    end
  end

  defp validate_field_value("height1_ft", cs), do: validate_height_value(cs, "height1_ft")
  defp validate_field_value("height2_ft", cs), do: validate_height_value(cs, "height2_ft")

  defp validate_field_value("private", changeset) do
    val = get_field(changeset, :proposed_changes)["private"]

    if is_boolean(val) do
      changeset
    else
      add_error(changeset, :proposed_changes, "private must be a boolean")
    end
  end

  defp validate_field_value(_, changeset), do: changeset

  defp validate_height_value(changeset, field) do
    val = get_field(changeset, :proposed_changes)[field]

    if is_integer(val) and val >= 0 and val <= 1000 do
      changeset
    else
      add_error(changeset, :proposed_changes, "#{field} must be an integer between 0 and 1000")
    end
  end

  defp validate_callsign_value(changeset, field) do
    val = get_field(changeset, :proposed_changes)[field] || ""
    val = val |> String.trim() |> String.upcase()

    if Regex.match?(~r/^[A-Z0-9\/]+$/, val) do
      changeset
    else
      add_error(changeset, :proposed_changes, "#{field} must contain only letters, digits, and /")
    end
  end

  defp validate_grid_value(changeset, field) do
    val = get_field(changeset, :proposed_changes)[field] || ""
    val = val |> String.trim() |> String.upcase()

    if Maidenhead.valid?(val) do
      changeset
    else
      add_error(changeset, :proposed_changes, "#{field} is not a valid Maidenhead grid square")
    end
  end
end
