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

  import Ecto.Changeset

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

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

  schema "contacts" do
    field :station1, :string
    field :station2, :string
    field :qso_timestamp, :utc_datetime
    field :grid1, :string
    field :grid2, :string
    field :pos1, :map
    field :pos2, :map
    field :mode, :string
    field :band, :decimal
    field :distance_km, :decimal

    field :hrrr_status, Ecto.Enum,
      values: [:pending, :queued, :processing, :complete, :failed, :unavailable],
      default: :pending

    field :weather_status, Ecto.Enum,
      values: [:pending, :queued, :processing, :complete, :failed, :unavailable],
      default: :pending

    field :terrain_status, Ecto.Enum,
      values: [:pending, :queued, :processing, :complete, :failed, :unavailable],
      default: :pending

    field :iemre_status, Ecto.Enum,
      values: [:pending, :queued, :processing, :complete, :failed, :unavailable],
      default: :pending

    field :radar_status, Ecto.Enum,
      values: [:pending, :queued, :processing, :complete, :failed, :unavailable],
      default: :pending

    # ADIF PROP_MODE string as submitted by the operator. Trusted as
    # ground truth by MechanismClassifier when present.
    field :user_declared_prop_mode, :string

    # Output of Microwaveprop.Propagation.MechanismClassifier. Values
    # align with the classifier's result type — kept as a string for
    # schema-free extension (new mechanisms don't require a migration).
    field :propagation_mechanism, :string
    field :propagation_mechanism_confidence, Ecto.Enum, values: [:high, :medium, :low]

    field :propagation_mechanism_status, Ecto.Enum,
      values: [:pending, :queued, :processing, :complete, :failed, :unavailable],
      default: :pending

    field :user_submitted, :boolean, default: false
    field :submitter_email, :string
    field :flagged_invalid, :boolean, default: false
    field :private, :boolean, default: false

    # Antenna height above ground level (feet). Optional — when set, the
    # elevation profile and terrain analysis use the actual heights
    # instead of a default 10 ft.
    field :height1_ft, :integer
    field :height2_ft, :integer

    belongs_to :user, User

    timestamps(type: :utc_datetime)
  end

  @type t :: %__MODULE__{}

  @required_fields ~w(station1 station2 qso_timestamp band)a
  @optional_fields ~w(grid1 grid2 pos1 pos2 distance_km user_id mode user_declared_prop_mode height1_ft height2_ft private)a

  @spec changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
  def changeset(contact, attrs) do
    contact
    |> cast(attrs, @required_fields ++ @optional_fields)
    |> validate_required(@required_fields)
  end

  @submission_fields ~w(station1 station2 qso_timestamp mode band grid1 grid2 submitter_email user_declared_prop_mode height1_ft height2_ft private)a
  @submission_required ~w(station1 station2 qso_timestamp band grid1 grid2)a
  @allowed_modes ~w(CW SSB FM FT8 FT4 Q65)
  # Keep in sync with Microwaveprop.Propagation.BandConfig.all_bands/0 and
  # Microwaveprop.Radio.AdifImport.@allowed_bands.
  @allowed_bands Enum.map(
                   ~w(50 144 222 432 902 1296 2304 3400 5760 10000 24000 47000 68000 75000 122000 134000 241000),
                   &Decimal.new/1
                 )

  @spec submission_changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
  def submission_changeset(contact, attrs) do
    contact
    |> cast(attrs, @submission_fields)
    |> validate_required(@submission_required)
    |> validate_user_or_email()
    |> sanitize_callsign(:station1)
    |> sanitize_callsign(:station2)
    |> sanitize_grid(:grid1)
    |> sanitize_grid(:grid2)
    |> normalize_blank_mode()
    |> validate_callsign(:station1)
    |> validate_callsign(:station2)
    |> validate_grid_format(:grid1)
    |> validate_grid_format(:grid2)
    |> validate_inclusion(:mode, @allowed_modes)
    |> validate_inclusion(:band, @allowed_bands)
    |> validate_email_format()
    |> validate_length(:submitter_email, max: 254)
    |> validate_length(:station1, max: 20)
    |> validate_length(:station2, max: 20)
    |> validate_height(:height1_ft)
    |> validate_height(:height2_ft)
  end

  # Sanity bounds on AGL antenna height. Negative heights and anything
  # taller than ~1000 ft are almost certainly data-entry errors.
  defp validate_height(changeset, field) do
    validate_number(changeset, field, greater_than_or_equal_to: 0, less_than_or_equal_to: 1000)
  end

  # Treat empty / whitespace-only mode strings as "not provided" so the column
  # ends up NULL instead of failing validate_inclusion. Mode is optional on the
  # submission path.
  defp normalize_blank_mode(changeset) do
    case get_change(changeset, :mode) do
      nil ->
        changeset

      value ->
        case String.trim(value) do
          "" -> put_change(changeset, :mode, nil)
          trimmed -> put_change(changeset, :mode, trimmed)
        end
    end
  end

  # Either a user_id (logged-in submitter) or a submitter_email (anonymous)
  # must be present so we know who submitted the contact.
  defp validate_user_or_email(changeset) do
    user_id = get_field(changeset, :user_id)
    email = get_field(changeset, :submitter_email)

    if user_id || (email && email != "") do
      changeset
    else
      add_error(changeset, :submitter_email, "can't be blank")
    end
  end

  defp validate_email_format(changeset) do
    case get_field(changeset, :submitter_email) do
      nil -> changeset
      "" -> changeset
      _email -> validate_format(changeset, :submitter_email, ~r/^[^\s<>]+@[^\s<>]+\.[^\s<>]+$/)
    end
  end

  # Strip whitespace and upcase callsigns
  defp sanitize_callsign(changeset, field) do
    case get_change(changeset, field) do
      nil -> changeset
      val -> put_change(changeset, field, val |> String.trim() |> String.upcase())
    end
  end

  # Upcase grid squares (Maidenhead uses uppercase letters + digits)
  defp sanitize_grid(changeset, field) do
    case get_change(changeset, field) do
      nil -> changeset
      val -> put_change(changeset, field, val |> String.trim() |> String.upcase())
    end
  end

  # Callsigns: letters, digits, and / only (e.g. W5XD, KG5CCI/P, VE3/W5XD)
  defp validate_callsign(changeset, field) do
    validate_format(changeset, field, ~r/^[A-Z0-9\/]+$/, message: "must contain only letters, digits, and /")
  end

  defp validate_grid_format(changeset, field) do
    validate_change(changeset, field, fn _, value ->
      if Maidenhead.valid?(value) do
        []
      else
        [{field, "is not a valid Maidenhead grid square"}]
      end
    end)
  end
end
