defmodule WhoNeedHelp.Help.HelpRequest do
  use Ecto.Schema
  import Ecto.Changeset

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

  schema "help_requests" do
    field :title, :string
    field :description, :string
    field :pickup_instructions, :string
    field :structured_data, :map, default: %{}
    field :location_label, :string
    field :location, Geo.PostGIS.Geometry

    field :status, Ecto.Enum,
      values: [:open, :matched, :in_progress, :completed, :cancelled, :expired],
      default: :open

    field :urgency, Ecto.Enum, values: [:now, :today, :scheduled], default: :now

    field :location_visibility, Ecto.Enum,
      values: [:hidden, :approximate_public, :exact_for_active_match, :exact_public],
      default: :approximate_public

    field :expires_at, :utc_datetime
    field :cancelled_at, :utc_datetime
    field :completed_at, :utc_datetime
    field :hidden_at, :utc_datetime
    field :hidden_reason, :string
    field :safety_confirmed, :boolean, virtual: true, default: false
    belongs_to :requester, WhoNeedHelp.Accounts.User
    belongs_to :category, WhoNeedHelp.Catalog.Category
    has_one :assignment, WhoNeedHelp.Help.Assignment, foreign_key: :request_id

    timestamps(type: :utc_datetime)
  end

  def create_changeset(request, attrs) do
    request
    |> cast(attrs, [
      :title,
      :description,
      :pickup_instructions,
      :structured_data,
      :location_label,
      :urgency,
      :location_visibility,
      :expires_at,
      :category_id,
      :safety_confirmed
    ])
    |> put_location(attrs)
    |> validate_required([
      :title,
      :description,
      :location_label,
      :location,
      :urgency,
      :location_visibility,
      :expires_at,
      :category_id
    ])
    |> validate_length(:title, min: 5, max: 120)
    |> validate_length(:description, min: 10, max: 2_000)
    |> validate_length(:pickup_instructions, max: 1_000)
    |> validate_length(:location_label, max: 255)
    |> validate_acceptance(:safety_confirmed,
      message: "confirm the safety guidance before publishing"
    )
    |> validate_expiry()
  end

  def moderation_changeset(request, attrs) do
    request
    |> cast(attrs, [:hidden_at, :hidden_reason])
    |> validate_length(:hidden_reason, max: 1_000)
  end

  defp put_location(changeset, attrs) do
    lat = attrs["latitude"] || attrs[:latitude]
    lng = attrs["longitude"] || attrs[:longitude]

    with {:ok, lat} <- parse_coordinate(lat),
         {:ok, lng} <- parse_coordinate(lng),
         true <- lat >= -90 and lat <= 90 and lng >= -180 and lng <= 180 do
      put_change(changeset, :location, %Geo.Point{coordinates: {lng, lat}, srid: 4326})
    else
      _ -> add_error(changeset, :location, "select a valid location")
    end
  end

  defp parse_coordinate(value) when is_float(value), do: {:ok, value}
  defp parse_coordinate(value) when is_integer(value), do: {:ok, value * 1.0}

  defp parse_coordinate(value) when is_binary(value) do
    case Float.parse(value) do
      {coordinate, ""} -> {:ok, coordinate}
      _ -> :error
    end
  end

  defp parse_coordinate(_value), do: :error

  defp validate_expiry(changeset) do
    validate_change(changeset, :expires_at, fn :expires_at, value ->
      if DateTime.after?(value, DateTime.utc_now()),
        do: [],
        else: [expires_at: "must be in the future"]
    end)
  end

  def public_coordinates(%__MODULE__{location: %Geo.Point{coordinates: {lng, lat}}} = request) do
    case request.location_visibility do
      :hidden -> nil
      :exact_public -> %{latitude: lat, longitude: lng, exact: true}
      _ -> %{latitude: Float.round(lat, 2), longitude: Float.round(lng, 2), exact: false}
    end
  end
end
