defmodule WhoNeedHelp.Activities.Activity do
  use Ecto.Schema
  import Ecto.Changeset

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

  schema "activities" do
    field :title, :string
    field :description, :string
    field :structured_data, :map, default: %{}
    field :location_label, :string
    field :location, Geo.PostGIS.Geometry
    field :status, Ecto.Enum, values: [:open, :completed, :cancelled], default: :open

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

    field :starts_at, :utc_datetime
    field :join_deadline, :utc_datetime
    field :capacity, :integer
    field :cancelled_at, :utc_datetime
    field :completed_at, :utc_datetime
    field :hidden_at, :utc_datetime
    field :hidden_reason, :string
    field :approved_participant_count, :integer, virtual: true, default: 0
    field :safety_confirmed, :boolean, virtual: true, default: false

    belongs_to :creator, WhoNeedHelp.Accounts.User
    belongs_to :category, WhoNeedHelp.Catalog.Category
    has_many :participants, WhoNeedHelp.Activities.Participant
    has_many :messages, WhoNeedHelp.Activities.Message

    timestamps(type: :utc_datetime)
  end

  def create_changeset(activity, attrs) do
    activity
    |> cast(attrs, [
      :title,
      :description,
      :structured_data,
      :location_label,
      :location_visibility,
      :starts_at,
      :join_deadline,
      :capacity,
      :category_id,
      :safety_confirmed
    ])
    |> put_location(attrs)
    |> validate_required([
      :title,
      :description,
      :location_label,
      :location,
      :location_visibility,
      :starts_at,
      :join_deadline,
      :capacity,
      :category_id
    ])
    |> validate_length(:title, min: 5, max: 120)
    |> validate_length(:description, min: 10, max: 2_000)
    |> validate_length(:location_label, max: 255)
    |> validate_number(:capacity,
      greater_than_or_equal_to: 2,
      less_than_or_equal_to: 2_147_483_647
    )
    |> validate_acceptance(:safety_confirmed,
      message: "confirm the safety guidance before publishing"
    )
    |> validate_schedule()
  end

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

  def public_coordinates(%__MODULE__{
        location_visibility: :approximate_public,
        location: %Geo.Point{coordinates: {lng, lat}}
      }) do
    %{latitude: Float.round(lat, 2), longitude: Float.round(lng, 2), exact: false}
  end

  def public_coordinates(_activity), do: nil

  def exact_coordinates(%__MODULE__{location: %Geo.Point{coordinates: {lng, lat}}}) do
    %{latitude: lat, longitude: lng, exact: true}
  end

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

    with {:ok, lat} <- parse_coordinate(latitude),
         {:ok, lng} <- parse_coordinate(longitude),
         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_schedule(changeset) do
    starts_at = get_field(changeset, :starts_at)
    join_deadline = get_field(changeset, :join_deadline)
    now = DateTime.utc_now()

    changeset
    |> then(fn changeset ->
      if starts_at && DateTime.after?(starts_at, now),
        do: changeset,
        else: add_error(changeset, :starts_at, "must be in the future")
    end)
    |> then(fn changeset ->
      cond do
        is_nil(join_deadline) ->
          changeset

        not DateTime.after?(join_deadline, now) ->
          add_error(changeset, :join_deadline, "must be in the future")

        starts_at && DateTime.after?(join_deadline, starts_at) ->
          add_error(changeset, :join_deadline, "must be on or before the start time")

        true ->
          changeset
      end
    end)
  end
end
