defmodule WhoNeedHelp.Catalog do
  @moduledoc "Data-driven categories, proposals, and community votes."

  import Ecto.Query
  alias WhoNeedHelp.Accounts
  alias WhoNeedHelp.Accounts.Scope
  alias WhoNeedHelp.Catalog.{Category, CategoryProposal, CategoryVote}
  alias WhoNeedHelp.Pagination
  alias WhoNeedHelp.Repo
  alias WhoNeedHelp.Trust

  def list_categories(mode \\ :help) when mode in [:help, :activity] do
    Category
    |> where([c], c.active and c.mode == ^mode)
    |> order_by([c], asc: c.sort_order, asc: c.slug)
    |> preload(:parent)
    |> Repo.all()
  end

  def list_all_categories do
    Category
    |> where([category], category.active)
    |> order_by([category], asc: category.mode, asc: category.sort_order, asc: category.slug)
    |> preload(:parent)
    |> Repo.all()
  end

  def get_category!(id), do: Repo.get!(Category, id)

  def category_path(%Category{parent: %Category{} = parent} = category, locale) do
    "#{Category.name(parent, locale)} — #{Category.name(category, locale)}"
  end

  def category_path(%Category{} = category, locale), do: Category.name(category, locale)

  def list_proposals do
    paginate_proposals().entries
  end

  def paginate_proposals(options \\ []) do
    limit = Pagination.limit(options)
    cursor = Pagination.cursor(options)
    public_user = Accounts.public_user_query()

    CategoryProposal
    |> where([p], p.status == :open)
    |> before_proposal(cursor)
    |> order_by([proposal], desc: proposal.inserted_at, desc: proposal.id)
    |> limit(^(limit + 1))
    |> with_vote_count()
    |> preload([proposal], proposer: ^public_user, parent: [])
    |> Repo.all()
    |> Pagination.page(limit, &{&1.inserted_at, &1.id})
  end

  def change_proposal(%CategoryProposal{} = proposal, attrs \\ %{}) do
    CategoryProposal.changeset(proposal, attrs)
  end

  def propose(%Scope{user: user} = scope, attrs) do
    with {:ok, _limit} <- Trust.authorize_action(scope, :category_proposal),
         {:ok, attrs} <- normalize_proposal_mode(attrs) do
      Repo.transact(fn ->
        with {:ok, proposal} <-
               %CategoryProposal{proposer_id: user.id}
               |> CategoryProposal.changeset(attrs)
               |> Repo.insert(),
             {:ok, _audit} <-
               Trust.audit(user.id, "category_proposal.created", "category_proposal", proposal.id) do
          {:ok, proposal}
        end
      end)
    end
  end

  def vote(%Scope{user: user} = scope, proposal_id) do
    with {:ok, proposal_id} <- cast_id(proposal_id),
         {:ok, _limit} <- Trust.authorize_action(scope, :category_vote) do
      Repo.transact(fn ->
        case CategoryProposal
             |> where([proposal], proposal.id == ^proposal_id)
             |> lock("FOR UPDATE")
             |> Repo.one() do
          nil ->
            {:error, :not_found}

          %CategoryProposal{status: :open} ->
            %CategoryVote{}
            |> CategoryVote.changeset(%{proposal_id: proposal_id, user_id: user.id})
            |> Repo.insert()

          %CategoryProposal{} ->
            {:error, :proposal_closed}
        end
      end)
    end
  end

  def unvote(%Scope{user: user}, proposal_id) do
    with {:ok, proposal_id} <- cast_id(proposal_id) do
      {count, _} =
        CategoryVote
        |> where([v], v.proposal_id == ^proposal_id and v.user_id == ^user.id)
        |> Repo.delete_all()

      {:ok, count}
    end
  end

  def seed_defaults do
    categories =
      Enum.reduce(default_category_specs(), %{}, fn {parent_slug, attrs}, seeded ->
        attrs =
          case parent_slug do
            nil -> attrs
            slug -> Map.put(attrs, :parent_id, Map.fetch!(seeded, slug).id)
          end

        category = seed_category(attrs)
        Map.put(seeded, category.slug, category)
      end)

    Map.fetch!(categories, "medicine-pickup")
  end

  def structured_fields(%Category{structured_fields: %{"fields" => fields}})
      when is_list(fields),
      do: fields

  def structured_fields(_category), do: []

  def validate_structured_data(%Category{} = category, data) when is_map(data) do
    fields = structured_fields(category)
    allowed = MapSet.new(Enum.map(fields, & &1["key"]))
    unknown = data |> Map.keys() |> Enum.reject(&MapSet.member?(allowed, &1))

    errors = Enum.map(unknown, &"#{&1} is not allowed")

    {clean, errors} =
      Enum.reduce(fields, {%{}, errors}, fn field, {clean, errors} ->
        key = field["key"]
        value = data[key]

        cond do
          field["required"] == true and value in [nil, ""] ->
            {clean, ["#{key} is required" | errors]}

          value in [nil, ""] ->
            {clean, errors}

          valid_structured_value?(field, value) ->
            {Map.put(clean, key, normalize_structured_value(field, value)), errors}

          true ->
            {clean, ["#{key} has an invalid value" | errors]}
        end
      end)

    if errors == [], do: {:ok, clean}, else: {:error, Enum.reverse(errors)}
  end

  def validate_structured_data(%Category{} = category, nil),
    do: validate_structured_data(category, %{})

  def validate_structured_data(_category, _data), do: {:error, ["must be an object"]}

  def list_proposals_for_moderation(%Scope{user: user}) do
    paginate_proposals_for_moderation(%Scope{user: user}).entries
  end

  def paginate_proposals_for_moderation(%Scope{user: user}, options \\ []) do
    if Accounts.moderator_authorized?(user) do
      limit = Pagination.limit(options)
      cursor = Pagination.cursor(options)

      CategoryProposal
      |> before_proposal(cursor)
      |> order_by([proposal], desc: proposal.inserted_at, desc: proposal.id)
      |> limit(^(limit + 1))
      |> with_vote_count()
      |> preload([proposal], [:parent, :merged_into])
      |> Repo.all()
      |> Pagination.page(limit, &{&1.inserted_at, &1.id})
    else
      %Pagination.Page{}
    end
  end

  def approve_proposal(%Scope{user: moderator}, proposal_id, category_attrs) do
    with {:ok, proposal_id} <- cast_id(proposal_id),
         true <- Accounts.moderator_authorized?(moderator) do
      Repo.transact(fn ->
        proposal = locked_proposal(proposal_id)

        cond do
          is_nil(proposal) ->
            {:error, :not_found}

          proposal.status == :open ->
            category_attrs =
              category_attrs
              |> Map.new(fn {key, value} -> {to_string(key), value} end)
              |> normalize_descriptions()
              |> Map.put_new("parent_id", proposal.parent_id)
              |> Map.put_new("mode", to_string(proposal.mode))

            with {:ok, category} <-
                   %Category{} |> Category.changeset(category_attrs) |> Repo.insert(),
                 {:ok, proposal} <-
                   proposal
                   |> CategoryProposal.moderation_changeset(%{
                     status: :approved,
                     merged_into_id: category.id,
                     reviewed_by_id: moderator.id,
                     reviewed_at: DateTime.utc_now(:second),
                     moderation_note: category_attrs["moderation_note"]
                   })
                   |> Repo.update(),
                 {:ok, _audit} <-
                   Trust.audit(
                     moderator.id,
                     "category_proposal.approved",
                     "category_proposal",
                     proposal.id,
                     %{"category_id" => category.id}
                   ) do
              {:ok, %{proposal: proposal, category: category}}
            end

          true ->
            {:error, :proposal_closed}
        end
      end)
    else
      false -> {:error, :forbidden}
      {:error, :not_found} = error -> error
    end
  end

  def reject_proposal(%Scope{user: moderator}, proposal_id, note) do
    moderate_proposal(moderator, proposal_id, :rejected, nil, note)
  end

  def merge_proposal(%Scope{user: moderator}, proposal_id, category_id, note) do
    with {:ok, category_id} <- cast_id(category_id),
         %Category{} <- Repo.get(Category, category_id) do
      moderate_proposal(moderator, proposal_id, :merged, category_id, note)
    else
      {:error, :not_found} = error -> error
      nil -> {:error, :not_found}
    end
  end

  defp moderate_proposal(moderator, proposal_id, status, merged_into_id, note) do
    with {:ok, proposal_id} <- cast_id(proposal_id),
         true <- Accounts.moderator_authorized?(moderator) do
      Repo.transact(fn ->
        proposal = locked_proposal(proposal_id)

        cond do
          is_nil(proposal) ->
            {:error, :not_found}

          proposal.status == :open ->
            with {:ok, proposal} <-
                   proposal
                   |> CategoryProposal.moderation_changeset(%{
                     status: status,
                     merged_into_id: merged_into_id,
                     reviewed_by_id: moderator.id,
                     reviewed_at: DateTime.utc_now(:second),
                     moderation_note: note
                   })
                   |> Repo.update(),
                 {:ok, _audit} <-
                   Trust.audit(
                     moderator.id,
                     "category_proposal.#{status}",
                     "category_proposal",
                     proposal.id,
                     %{"merged_into_id" => merged_into_id}
                   ) do
              {:ok, proposal}
            end

          true ->
            {:error, :proposal_closed}
        end
      end)
    else
      false -> {:error, :forbidden}
      {:error, :not_found} = error -> error
    end
  end

  defp locked_proposal(proposal_id) do
    CategoryProposal
    |> where([proposal], proposal.id == ^proposal_id)
    |> lock("FOR UPDATE")
    |> Repo.one()
  end

  defp valid_structured_value?(%{"type" => "select", "options" => options}, value)
       when is_list(options) do
    Enum.any?(options, fn
      %{"value" => allowed} -> same_scalar_value?(allowed, value)
      allowed -> same_scalar_value?(allowed, value)
    end)
  end

  defp valid_structured_value?(%{"type" => "boolean"}, value),
    do: value in [true, false, "true", "false"]

  defp valid_structured_value?(%{"type" => "text"} = field, value) when is_binary(value) do
    case field["max_length"] do
      max when is_integer(max) and max > 0 -> String.length(value) <= max
      _ -> true
    end
  end

  defp valid_structured_value?(_field, _value), do: false

  defp same_scalar_value?(left, right)
       when (is_binary(left) or is_atom(left) or is_number(left) or is_boolean(left)) and
              (is_binary(right) or is_atom(right) or is_number(right) or is_boolean(right)),
       do: to_string(left) == to_string(right)

  defp same_scalar_value?(_left, _right), do: false

  defp normalize_structured_value(%{"type" => "boolean"}, "true"), do: true
  defp normalize_structured_value(%{"type" => "boolean"}, "false"), do: false
  defp normalize_structured_value(_field, value), do: value

  defp normalize_descriptions(%{"descriptions" => descriptions} = attrs)
       when is_map(descriptions),
       do: attrs

  defp normalize_descriptions(%{"description" => description} = attrs)
       when is_binary(description) and description != "",
       do: Map.put(attrs, "descriptions", %{"en" => description})

  defp normalize_descriptions(attrs), do: attrs

  defp seed_category(attrs) do
    case Repo.get_by(Category, slug: attrs.slug) do
      nil ->
        %Category{} |> Category.changeset(attrs) |> Repo.insert!()

      %Category{} = category ->
        backfill =
          %{}
          |> put_backfill(:names, category.names in [nil, %{}], attrs.names)
          |> put_backfill(
            :descriptions,
            category.descriptions in [nil, %{}],
            attrs.descriptions
          )
          |> put_backfill(
            :structured_fields,
            category.structured_fields in [nil, %{}],
            attrs.structured_fields
          )
          |> put_backfill(
            :parent_id,
            is_nil(category.parent_id) and not is_nil(attrs[:parent_id]),
            attrs[:parent_id]
          )

        if map_size(backfill) == 0 do
          category
        else
          category |> Category.changeset(backfill) |> Repo.update!()
        end
    end
  end

  defp put_backfill(attrs, _key, false, _value), do: attrs
  defp put_backfill(attrs, key, true, value), do: Map.put(attrs, key, value)

  defp normalize_proposal_mode(attrs) do
    attrs = Map.new(attrs, fn {key, value} -> {to_string(key), value} end)

    case attrs["parent_id"] do
      parent_id when parent_id not in [nil, ""] ->
        with {:ok, parent_id} <- cast_id(parent_id),
             %Category{} = parent <- Repo.get(Category, parent_id) do
          {:ok, Map.put(attrs, "mode", to_string(parent.mode))}
        else
          _invalid_or_missing -> {:error, :invalid_parent}
        end

      _ ->
        {:ok, Map.put_new(attrs, "mode", "help")}
    end
  end

  defp default_category_specs do
    [
      {nil,
       %{
         slug: "urgent-delivery",
         names: names("Urgent delivery", "Термінова доставка", "Срочная доставка"),
         descriptions:
           names(
             "Time-sensitive delivery of an item that is already legally arranged.",
             "Термінова доставка речі, отримання якої вже законно узгоджено.",
             "Срочная доставка вещи, получение которой уже законно согласовано."
           ),
         active: false,
         sort_order: 0,
         structured_fields: %{}
       }},
      {"urgent-delivery",
       %{
         slug: "medicine-pickup",
         names: names("Medicine pickup", "Доставка ліків", "Доставка лекарств"),
         descriptions:
           names(
             "Pickup and deliver a legal medicine that is already purchased or reserved.",
             "Отримати й доставити дозволені ліки, які вже придбані або зарезервовані.",
             "Получить и доставить разрешённое лекарство, которое уже куплено или зарезервировано."
           ),
         sort_order: 10,
         structured_fields: %{
           "fields" => [
             select_field(
               "pickup_status",
               names(
                 "Medicine pickup status",
                 "Статус отримання ліків",
                 "Статус получения лекарства"
               ),
               [
                 option("reserved", names("Reserved", "Зарезервовано", "Зарезервировано")),
                 option("already_paid", names("Already paid", "Вже оплачено", "Уже оплачено"))
               ]
             )
           ]
         }
       }},
      {nil,
       %{
         slug: "roadside-help",
         names: names("Roadside help", "Допомога в дорозі", "Помощь в дороге"),
         descriptions:
           names(
             "Non-emergency roadside assistance after people and vehicles are away from active danger.",
             "Неекстрена допомога в дорозі після усунення безпосередньої небезпеки для людей і транспорту.",
             "Неэкстренная помощь в дороге после устранения непосредственной опасности для людей и транспорта."
           ),
         active: false,
         sort_order: 100,
         structured_fields: %{}
       }},
      {"roadside-help",
       %{
         slug: "fuel-delivery",
         names: names("Out of fuel", "Закінчилося пальне", "Закончилось топливо"),
         descriptions:
           names(
             "Bring a small lawful quantity of fuel to a vehicle in a secured location.",
             "Привезти невелику дозволену кількість пального до транспорту в безпечному місці.",
             "Привезти небольшое разрешённое количество топлива к транспорту в безопасном месте."
           ),
         sort_order: 110,
         structured_fields: %{
           "fields" => [
             select_field("fuel_type", names("Fuel type", "Тип пального", "Тип топлива"), [
               option("petrol", names("Petrol", "Бензин", "Бензин")),
               option("diesel", names("Diesel", "Дизель", "Дизель"))
             ]),
             text_field(
               "fuel_details",
               names("Grade or details", "Марка або деталі", "Марка или детали"),
               80,
               false
             ),
             boolean_field(
               "vehicle_in_safe_place",
               names(
                 "Vehicle is away from active traffic",
                 "Авто поза активним рухом",
                 "Авто вне активного движения"
               )
             )
           ]
         }
       }},
      {"roadside-help",
       %{
         slug: "car-wheel-help",
         names: names("Car wheel help", "Допомога з колесом авто", "Помощь с колесом авто"),
         descriptions:
           names(
             "Help with a flat tyre, inflation, or fitting an available spare in a secured location.",
             "Допомога з проколом, підкачуванням або встановленням наявного запасного колеса в безпечному місці.",
             "Помощь с проколом, подкачкой или установкой имеющегося запасного колеса в безопасном месте."
           ),
         sort_order: 120,
         structured_fields: %{
           "fields" => [
             select_field(
               "wheel_issue",
               names("Wheel problem", "Проблема з колесом", "Проблема с колесом"),
               [
                 option("flat_tyre", names("Flat tyre", "Проколоте колесо", "Проколото колесо")),
                 option(
                   "fit_spare",
                   names("Fit an available spare", "Встановити запаску", "Установить запаску")
                 ),
                 option(
                   "inflate",
                   names("Inflation needed", "Потрібне підкачування", "Нужна подкачка")
                 )
               ]
             ),
             boolean_field(
               "spare_available",
               names("Usable spare is available", "Є придатна запаска", "Есть пригодная запаска"),
               false
             ),
             boolean_field(
               "vehicle_in_safe_place",
               names(
                 "Vehicle is away from active traffic",
                 "Авто поза активним рухом",
                 "Авто вне активного движения"
               )
             )
           ]
         }
       }},
      {"roadside-help",
       %{
         slug: "bicycle-roadside-help",
         names:
           names(
             "Bicycle roadside help",
             "Допомога з велосипедом",
             "Помощь с велосипедом"
           ),
         descriptions:
           names(
             "Help with a bicycle puncture, chain, or brake problem.",
             "Допомога з проколом, ланцюгом або гальмами велосипеда.",
             "Помощь с проколом, цепью или тормозами велосипеда."
           ),
         sort_order: 130,
         structured_fields: %{
           "fields" => [
             select_field(
               "bicycle_issue",
               names("Bicycle problem", "Проблема велосипеда", "Проблема велосипеда"),
               [
                 option("puncture", names("Puncture", "Прокол", "Прокол")),
                 option(
                   "chain",
                   names("Chain problem", "Проблема з ланцюгом", "Проблема с цепью")
                 ),
                 option(
                   "brakes",
                   names("Brake problem", "Проблема з гальмами", "Проблема с тормозами")
                 ),
                 option("other", names("Other", "Інше", "Другое"))
               ]
             ),
             boolean_field(
               "tools_available",
               names(
                 "Requester has tools",
                 "У заявника є інструменти",
                 "У заявителя есть инструменты"
               ),
               false
             )
           ]
         }
       }},
      {"roadside-help",
       %{
         slug: "motorcycle-roadside-help",
         names:
           names(
             "Motorcycle roadside help",
             "Допомога з мотоциклом",
             "Помощь с мотоциклом"
           ),
         descriptions:
           names(
             "Help with a motorcycle chain, tyre, or battery after the vehicle is secured.",
             "Допомога з ланцюгом, колесом або акумулятором мотоцикла після його переміщення в безпечне місце.",
             "Помощь с цепью, колесом или аккумулятором мотоцикла после его перемещения в безопасное место."
           ),
         sort_order: 140,
         structured_fields: %{
           "fields" => [
             select_field(
               "motorcycle_issue",
               names("Motorcycle problem", "Проблема мотоцикла", "Проблема мотоцикла"),
               [
                 option("broken_chain", names("Broken chain", "Порваний ланцюг", "Порвана цепь")),
                 option("flat_tyre", names("Flat tyre", "Проколоте колесо", "Проколото колесо")),
                 option("battery", names("Battery", "Акумулятор", "Аккумулятор")),
                 option("other", names("Other", "Інше", "Другое"))
               ]
             ),
             boolean_field(
               "vehicle_in_safe_place",
               names(
                 "Motorcycle is away from active traffic",
                 "Мотоцикл поза активним рухом",
                 "Мотоцикл вне активного движения"
               )
             )
           ]
         }
       }},
      {"roadside-help",
       %{
         slug: "vehicle-breakdown-help",
         names:
           names(
             "Vehicle breakdown",
             "Поломка транспортного засобу",
             "Поломка транспортного средства"
           ),
         descriptions:
           names(
             "Non-emergency practical help with a stopped vehicle in a secured location.",
             "Неекстрена практична допомога із зупиненим транспортом у безпечному місці.",
             "Неэкстренная практическая помощь с остановившимся транспортом в безопасном месте."
           ),
         sort_order: 150,
         structured_fields: %{
           "fields" => [
             select_field(
               "vehicle_type",
               names("Vehicle type", "Тип транспорту", "Тип транспорта"),
               [
                 option("car", names("Car", "Авто", "Авто")),
                 option("van", names("Van", "Фургон", "Фургон")),
                 option("scooter", names("Scooter", "Скутер", "Скутер")),
                 option("other", names("Other", "Інше", "Другое"))
               ]
             ),
             select_field(
               "breakdown_type",
               names("Problem type", "Тип проблеми", "Тип проблемы"),
               [
                 option(
                   "battery",
                   names(
                     "Battery or jump-start",
                     "Акумулятор або запуск",
                     "Аккумулятор или запуск"
                   )
                 ),
                 option("mechanical", names("Mechanical", "Механічна", "Механическая")),
                 option("unknown", names("Unknown", "Невідомо", "Неизвестно")),
                 option("other", names("Other", "Інше", "Другое"))
               ]
             ),
             boolean_field(
               "vehicle_in_safe_place",
               names(
                 "Vehicle is away from active traffic",
                 "Транспорт поза активним рухом",
                 "Транспорт вне активного движения"
               )
             )
           ]
         }
       }},
      {"roadside-help",
       %{
         slug: "secured-incident-support",
         names:
           names(
             "Support after a road incident",
             "Допомога після дорожньої події",
             "Помощь после дорожного происшествия"
           ),
         descriptions:
           names(
             "Non-emergency presence or practical support only after immediate danger is handled.",
             "Неекстрена присутність або практична підтримка лише після усунення безпосередньої небезпеки.",
             "Неэкстренное присутствие или практическая поддержка только после устранения непосредственной опасности."
           ),
         sort_order: 160,
         structured_fields: %{
           "fields" => [
             select_field(
               "safety_status",
               names("Safety status", "Стан безпеки", "Состояние безопасности"),
               [
                 option(
                   "scene_secured",
                   names(
                     "No immediate danger; scene is secured",
                     "Негайної небезпеки немає",
                     "Немедленной опасности нет"
                   )
                 ),
                 option(
                   "emergency_services_contacted",
                   names(
                     "Emergency services already contacted",
                     "Екстрені служби вже викликані",
                     "Экстренные службы уже вызваны"
                   )
                 )
               ]
             ),
             select_field(
               "support_needed",
               names("Support needed", "Потрібна підтримка", "Нужна поддержка"),
               [
                 option(
                   "safe_ride",
                   names("Safe ride or pickup", "Безпечна поїздка", "Безопасная поездка")
                 ),
                 option("translation", names("Translation", "Переклад", "Перевод")),
                 option(
                   "wait_with_me",
                   names("Wait with me", "Почекати поруч", "Подождать рядом")
                 ),
                 option(
                   "other",
                   names(
                     "Other practical support",
                     "Інша практична допомога",
                     "Другая практическая помощь"
                   )
                 )
               ]
             )
           ]
         }
       }},
      {nil,
       %{
         slug: "social-activity",
         names: names("Social activity", "Спільне дозвілля", "Совместный досуг"),
         descriptions:
           names(
             "Optional social plans with organizer-approved participants and no effect on helper reputation.",
             "Необов’язкові спільні плани з учасниками, яких схвалює організатор, без впливу на репутацію помічника.",
             "Необязательные совместные планы с участниками, которых одобряет организатор, без влияния на репутацию помощника."
           ),
         mode: :activity,
         active: false,
         sort_order: 1_000,
         structured_fields: %{}
       }},
      {"social-activity",
       %{
         slug: "coffee-meetup",
         names: names("Coffee or tea", "Кава або чай", "Кофе или чай"),
         descriptions:
           names(
             "Meet in a public place for coffee, tea, or conversation.",
             "Зустрітися в публічному місці за кавою, чаєм або розмовою.",
             "Встретиться в публичном месте за кофе, чаем или разговором."
           ),
         mode: :activity,
         sort_order: 1_010,
         structured_fields: %{
           "fields" => [
             select_field(
               "setting",
               names("Preferred setting", "Бажане місце", "Желаемое место"),
               [
                 option("cafe", names("Café", "Кав'ярня", "Кофейня")),
                 option(
                   "outdoors",
                   names(
                     "Outdoor public place",
                     "Публічне місце надворі",
                     "Публичное место на улице"
                   )
                 ),
                 option("either", names("Either", "Будь-яке", "Любое"))
               ]
             )
           ]
         }
       }},
      {"social-activity",
       %{
         slug: "cinema-meetup",
         names: names("Cinema", "Кіно", "Кино"),
         descriptions:
           names(
             "Find company for a public cinema screening.",
             "Знайти компанію для перегляду фільму в кінотеатрі.",
             "Найти компанию для просмотра фильма в кинотеатре."
           ),
         mode: :activity,
         sort_order: 1_020,
         structured_fields: %{
           "fields" => [
             text_field(
               "film_or_genre",
               names("Film or genre", "Фільм або жанр", "Фильм или жанр"),
               120,
               true
             )
           ]
         }
       }},
      {"social-activity",
       %{
         slug: "city-walk",
         names: names("Walk", "Прогулянка", "Прогулка"),
         descriptions:
           names(
             "Arrange a walk in a public area.",
             "Домовитися про прогулянку в публічному місці.",
             "Договориться о прогулке в публичном месте."
           ),
         mode: :activity,
         sort_order: 1_030,
         structured_fields: %{
           "fields" => [
             select_field("pace", names("Walking pace", "Темп прогулянки", "Темп прогулки"), [
               option("relaxed", names("Relaxed", "Спокійний", "Спокойный")),
               option("moderate", names("Moderate", "Середній", "Средний")),
               option("active", names("Active", "Активний", "Активный"))
             ])
           ]
         }
       }},
      {"social-activity",
       %{
         slug: "hiking-group",
         names: names("Hiking", "Похід", "Поход"),
         descriptions:
           names(
             "Plan a group hike with explicit route difficulty and safety preparation.",
             "Спланувати груповий похід із зазначенням складності маршруту та підготовкою з безпеки.",
             "Спланировать групповой поход с указанием сложности маршрута и подготовкой по безопасности."
           ),
         mode: :activity,
         sort_order: 1_040,
         structured_fields: %{
           "fields" => [
             select_field(
               "difficulty",
               names("Route difficulty", "Складність маршруту", "Сложность маршрута"),
               [
                 option("easy", names("Easy", "Легкий", "Легкий")),
                 option("moderate", names("Moderate", "Середній", "Средний")),
                 option("hard", names("Hard", "Складний", "Сложный"))
               ]
             ),
             text_field(
               "route_plan",
               names("Public route summary", "Опис маршруту", "Описание маршрута"),
               300,
               true
             ),
             boolean_field(
               "safety_plan_confirmed",
               names(
                 "Organizer has a safety and return plan",
                 "Організатор має план безпеки й повернення",
                 "У организатора есть план безопасности и возвращения"
               )
             )
           ]
         }
       }}
    ]
  end

  defp with_vote_count(query) do
    counts =
      CategoryVote
      |> group_by([vote], vote.proposal_id)
      |> select([vote], %{proposal_id: vote.proposal_id, vote_count: count(vote.id)})

    query
    |> join(:left, [proposal], count in subquery(counts),
      on: count.proposal_id == proposal.id,
      as: :vote_counts
    )
    |> select_merge([vote_counts: count], %{
      vote_count: fragment("coalesce(?, 0)", count.vote_count)
    })
  end

  defp before_proposal(query, nil), do: query

  defp before_proposal(query, {inserted_at, id}) do
    where(
      query,
      [proposal],
      proposal.inserted_at < ^inserted_at or
        (proposal.inserted_at == ^inserted_at and proposal.id < ^id)
    )
  end

  defp cast_id(value) do
    case Ecto.UUID.cast(value) do
      {:ok, id} -> {:ok, id}
      :error -> {:error, :not_found}
    end
  end

  defp names(en, uk, ru), do: %{"en" => en, "uk" => uk, "ru" => ru}

  defp option(value, label), do: %{"value" => value, "label" => label}

  defp select_field(key, label, options, required \\ true) do
    %{
      "key" => key,
      "type" => "select",
      "required" => required,
      "label" => label,
      "options" => options
    }
  end

  defp boolean_field(key, label, required \\ true) do
    %{"key" => key, "type" => "boolean", "required" => required, "label" => label}
  end

  defp text_field(key, label, max_length, required) do
    %{
      "key" => key,
      "type" => "text",
      "required" => required,
      "label" => label,
      "max_length" => max_length
    }
  end
end
