defmodule Oban.Web.Queues.DetailComponent do
  use Oban.Web, :live_component

  import Oban.Web.Helpers.QueueHelper
  import Phoenix.HTML.Form, only: [options_for_select: 2]

  alias Oban.Config
  alias Oban.Met
  alias Oban.Web.Components.Core
  alias Oban.Web.Queues.DetailInsanceComponent

  @impl Phoenix.LiveComponent
  def update(%{local_limit: new_limit}, socket) do
    %{checks: checks, inputs: inputs} = socket.assigns

    local_limit =
      cond do
        match?([_], checks) ->
          new_limit

        match?([_ | _], checks) ->
          max(local_limit(checks), new_limit)

        true ->
          inputs.local_limit
      end

    inputs = %{inputs | local_limit: local_limit}

    {:ok, assign(socket, inputs: inputs)}
  end

  def update(assigns, socket) do
    checks = Enum.filter(assigns.checks, &(&1["queue"] == assigns.queue))

    counts =
      Met.latest(assigns.conf.name, :full_count, group: "state", filters: [queue: assigns.queue])

    socket =
      socket
      |> assign(access: assigns.access, conf: assigns.conf, queue: assigns.queue)
      |> assign(counts: counts, checks: checks)
      |> assign_new(:inputs, fn ->
        %{
          local_limit: local_limit(checks),
          global_allowed: global_allowed(checks),
          global_partition_fields: partition_value(checks, "global_limit", "fields"),
          global_partition_keys: partition_value(checks, "global_limit", "keys"),
          rate_allowed: rate_allowed(checks),
          rate_period: rate_period(checks),
          rate_partition_fields: partition_value(checks, "rate_limit", "fields"),
          rate_partition_keys: partition_value(checks, "rate_limit", "keys")
        }
      end)

    {:ok, socket}
  end

  @impl Phoenix.LiveComponent
  def render(assigns) do
    ~H"""
    <div id="queue-details">
      <div class="flex justify-between items-center px-3 py-6">
        <button
          id="back-link"
          class="flex items-center hover:text-blue-500 cursor-pointer bg-transparent border-0 p-0"
          phx-hook="HistoryBack"
          type="button"
        >
          <Icons.arrow_left class="w-5 h-5" />
          <span class="text-lg capitalize font-bold ml-2">{@queue} Queue</span>
        </button>
      </div>

      <table class="table-fixed w-full bg-blue-50 dark:bg-blue-950 dark:bg-opacity-25">
        <thead>
          <tr class="text-sm text-gray-600 dark:text-gray-100 dark:text-opacity-60">
            <th scope="col" class="text-left font-normal pt-6 pb-1 px-3">Started</th>
            <th scope="col" class="text-left font-normal pt-6 pb-1 px-3">Executing</th>
            <th scope="col" class="text-left font-normal pt-6 pb-1 px-3">Available</th>
            <th scope="col" class="text-left font-normal pt-6 pb-1 px-3">Scheduled</th>
            <th scope="col" class="text-left font-normal pt-6 pb-1 px-3">Retryable</th>
            <th scope="col" class="text-left font-normal pt-6 pb-1 px-3">Cancelled</th>
            <th scope="col" class="text-left font-normal pt-6 pb-1 px-3">Discarded</th>
            <th scope="col" class="text-left font-normal pt-6 pb-1 px-3">Completed</th>
          </tr>
        </thead>
        <tbody>
          <tr class="text-lg text-gray-800 dark:text-gray-100 tabular">
            <td class="pb-6 px-3">
              <div class="flex items-center space-x-2">
                <Icons.clock class="w-4 h-5 text-gray-600 dark:text-gray-300" />
                <span>{started_at(@checks)}</span>
              </div>
            </td>

            <td class="pb-6 px-3">
              <div class="flex items-center space-x-2">
                <Icons.cog class="w-4 h-5 text-gray-600 dark:text-gray-300" />
                <span>{executing_count(@checks)}</span>
              </div>
            </td>

            <%= for state <- ~w(available scheduled retryable cancelled discarded completed) do %>
              <td class="pb-6 px-3">
                <div class="flex items-center space-x-2">
                  <Icons.square_stack class="w-4 h-5 text-gray-600 dark:text-gray-300" />
                  <span>{integer_to_estimate(@counts[state])}</span>
                </div>
              </td>
            <% end %>
          </tr>
        </tbody>
      </table>

      <div>
        <div class="flex items-center pl-3 py-6">
          <Icons.adjustments_horizontal class="w-6 h-6 mr-1 text-gray-600 dark:text-gray-400" />
          <h3 class="font-medium text-base">Configuration</h3>
        </div>

        <div class="flex w-full px-3 border-t border-gray-200 dark:border-gray-700">
          <form
            id="local-form"
            class="w-2/12 pr-3 pt-3 pb-6"
            phx-target={@myself}
            phx-change="form-change"
            phx-submit="local-submit"
          >
            <h3 class="flex items-center mb-4">
              <Icons.map_pin class="w-5 h-5 mr-1 text-gray-500" />
              <span class="text-base font-medium">Local</span>
            </h3>

            <Core.number_input
              disabled={not can?(:scale_queues, @access)}
              label="Limit"
              myself={@myself}
              name="local_limit"
              value={@inputs.local_limit}
            />

            <.submit_input
              locked={not can?(:scale_queues, @access)}
              disabled={
                @inputs.local_limit == local_limit(@checks) or not can?(:scale_queues, @access)
              }
              label="Scale"
            />
          </form>

          <form
            class={"relative w-5/12 px-3 pt-3 pb-6 border-l border-r border-gray-200 dark:border-gray-700 #{if missing_pro?(@conf), do: "bg-white dark:bg-black bg-opacity-30"}"}
            id="global-form"
            phx-change="form-change"
            phx-submit="global-update"
            phx-target={@myself}
          >
            <.pro_blocker :if={missing_pro?(@conf)} />

            <div class={
              if missing_pro?(@conf),
                do: "opacity-20 cursor-not-allowed pointer-events-none select-none"
            }>
              <div class="flex items-center justify-between mb-4">
                <h3 class="flex items-center">
                  <Icons.globe class="w-5 h-5 mr-1 text-gray-500" />
                  <span class="text-base font-medium">Global</span>
                </h3>

                <.toggle_button
                  disabled={not can?(:scale_queues, @access)}
                  enabled={not is_nil(@inputs.global_allowed)}
                  feature="global"
                  myself={@myself}
                />
              </div>

              <div class="flex w-full space-x-3 mb-6">
                <Core.number_input
                  disabled={not can?(:scale_queues, @access) or is_nil(@inputs.global_allowed)}
                  label="Limit"
                  myself={@myself}
                  name="global_allowed"
                  value={@inputs.global_allowed}
                />
              </div>

              <div class="flex w-full space-x-3">
                <div class="w-1/2">
                  <label for="global_partition_fields" class="block font-medium text-sm mb-2">
                    Partition Fields
                  </label>
                  <select
                    id="global_partition_fields"
                    name="global_partition_fields"
                    class="block w-full font-mono text-sm pl-3 pr-10 py-2 shadow-sm border-gray-300 dark:border-gray-500 bg-gray-50 dark:bg-gray-800 disabled:opacity-50 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
                    disabled={not can?(:scale_queues, @access) or is_nil(@inputs.global_allowed)}
                  >
                    {options_for_select(partition_options(), @inputs.global_partition_fields)}
                  </select>
                </div>

                <div class="w-1/2">
                  <label for="global_partition_keys" class="block font-medium text-sm mb-2">
                    Partition Keys
                  </label>

                  <input
                    class="block w-full font-mono text-sm py-2 shadow-sm border-gray-300 dark:border-gray-500 bg-gray-50 dark:bg-gray-800 disabled:opacity-50 rounded-md focus:ring-blue-400 focus:border-blue-400"
                    disabled={
                      keyless_partition?(@inputs.global_partition_fields) or
                        not can?(:scale_queues, @access)
                    }
                    id="global_partition_keys"
                    name="global_partition_keys"
                    type="text"
                    value={@inputs.global_partition_keys}
                  />
                </div>
              </div>

              <.submit_input
                locked={not can?(:scale_queues, @access)}
                disabled={global_unchanged?(@checks, @inputs) or not can?(:scale_queues, @access)}
                label="Apply"
              />
            </div>
          </form>

          <form
            class={"relative w-5/12 pt-3 pb-6 pl-3 #{if missing_pro?(@conf), do: "bg-white dark:bg-black bg-opacity-30"}"}
            id="rate-limit-form"
            phx-change="form-change"
            phx-submit="rate-limit-update"
            phx-target={@myself}
          >
            <.pro_blocker :if={missing_pro?(@conf)} />

            <div class={
              if missing_pro?(@conf),
                do: "opacity-20 cursor-not-allowed pointer-events-none select-none"
            }>
              <div class="flex items-center justify-between mb-4">
                <h3 class="flex items-center">
                  <Icons.arrow_trending_down class="w-5 h-5 mr-1 text-gray-500" />
                  <span class="text-base font-medium">Rate Limit</span>
                </h3>

                <.toggle_button
                  disabled={not can?(:scale_queues, @access)}
                  enabled={not is_nil(@inputs.rate_allowed)}
                  feature="rate-limit"
                  myself={@myself}
                />
              </div>

              <div class="flex w-full space-x-3 mb-6">
                <div class="w-1/2">
                  <Core.number_input
                    disabled={not can?(:scale_queues, @access) or is_nil(@inputs.rate_allowed)}
                    label="Allowed"
                    myself={@myself}
                    name="rate_allowed"
                    value={@inputs.rate_allowed}
                  />
                </div>

                <div class="w-1/2">
                  <Core.number_input
                    disabled={not can?(:scale_queues, @access) or is_nil(@inputs.rate_allowed)}
                    label="Period"
                    myself={@myself}
                    name="rate_period"
                    value={@inputs.rate_period}
                  />
                </div>
              </div>

              <div class="flex w-full space-x-3">
                <div class="w-1/2">
                  <label for="rate_partition_fields" class="block font-medium text-sm mb-2">
                    Partition Fields
                  </label>
                  <select
                    id="rate_partition_fields"
                    name="rate_partition_fields"
                    class="block w-full font-mono text-sm pl-3 pr-10 py-2 shadow-sm border-gray-300 dark:border-gray-500 bg-gray-50 dark:bg-gray-800 disabled:opacity-50 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
                    disabled={not can?(:scale_queues, @access) or is_nil(@inputs.rate_allowed)}
                  >
                    {options_for_select(partition_options(), @inputs.rate_partition_fields)}
                  </select>
                </div>

                <div class="w-1/2">
                  <label for="rate_keys" class="block font-medium text-sm mb-2">
                    Partition Keys
                  </label>

                  <input
                    class="block w-full font-mono text-sm py-2 shadow-sm border-gray-300 dark:border-gray-500 bg-gray-50 dark:bg-gray-800 disabled:opacity-50 rounded-md focus:ring-blue-400 focus:border-blue-400"
                    disabled={
                      keyless_partition?(@inputs.rate_partition_fields) or
                        not can?(:scale_queues, @access)
                    }
                    id="rate_partition_keys"
                    name="rate_partition_keys"
                    type="text"
                    value={@inputs.rate_partition_keys}
                  />
                </div>
              </div>

              <.submit_input
                locked={not can?(:scale_queues, @access)}
                disabled={rate_unchanged?(@checks, @inputs) or not can?(:scale_queues, @access)}
                label="Apply"
              />
            </div>
          </form>
        </div>
      </div>

      <div id="queue-instances" class="border-t border-gray-200 dark:border-gray-700">
        <div class="flex items-center pl-3 py-6">
          <Icons.square_2x2 class="w-6 h-6 mr-1 text-gray-600 dark:text-gray-400" />
          <h3 class="font-medium text-base">Instances</h3>
        </div>

        <table class="table-fixed min-w-full divide-y divide-gray-200 dark:divide-gray-700 border-t border-gray-200 dark:border-gray-700">
          <thead>
            <tr class="bg-gray-50 dark:bg-gray-950 text-gray-500 dark:text-gray-500">
              <th
                scope="col"
                class="w-1/2 text-left text-xs font-medium uppercase tracking-wider pl-3 py-3"
              >
                Node/Name
              </th>
              <th
                scope="col"
                class="w-12 text-right text-xs font-medium uppercase tracking-wider py-3"
              >
                Executing
              </th>
              <th
                scope="col"
                class="w-16 text-right text-xs font-medium uppercase tracking-wider py-3"
              >
                Started
              </th>
              <th
                scope="col"
                class="w-8 text-left text-xs font-medium uppercase tracking-wider pl-6 py-3"
              >
                Pause
              </th>
              <th
                scope="col"
                class="w-32 text-left text-xs font-medium uppercase tracking-wider pr-3 py-3"
              >
                Scale
              </th>
            </tr>
          </thead>

          <tbody class="divide-y divide-gray-100 dark:divide-gray-800">
            <%= for checks <- @checks do %>
              <.live_component
                access={@access}
                checks={checks}
                id={node_name(checks)}
                module={DetailInsanceComponent}
              />
            <% end %>
          </tbody>
        </table>
      </div>
    </div>
    """
  end

  # Handlers

  @integer_inputs ~w(local_limit global_allowed rate_allowed rate_period)

  @impl Phoenix.LiveComponent
  def handle_event("form-change", %{"_target" => ["rate_partition_fields"]} = params, socket) do
    fields = params["rate_partition_fields"]
    inputs = %{socket.assigns.inputs | rate_partition_fields: fields}

    {:noreply, assign(socket, inputs: inputs)}
  end

  def handle_event("form-change", %{"_target" => ["global_partition_fields"]} = params, socket) do
    fields = params["global_partition_fields"]
    inputs = %{socket.assigns.inputs | global_partition_fields: fields}

    {:noreply, assign(socket, inputs: inputs)}
  end

  def handle_event("form-change", params, socket) do
    inputs =
      for {key, val} <- params, key in @integer_inputs, reduce: socket.assigns.inputs do
        acc ->
          case Integer.parse(val) do
            {int, _} when int > 0 ->
              %{acc | String.to_existing_atom(key) => int}

            _ ->
              acc
          end
      end

    {:noreply, assign(socket, inputs: inputs)}
  end

  def handle_event("local-submit", params, socket) do
    enforce_access!(:scale_queues, socket.assigns.access)

    limit = String.to_integer(params["local_limit"])

    send(self(), {:scale_queue, socket.assigns.queue, limit: limit})

    inputs = %{socket.assigns.inputs | local_limit: limit}

    {:noreply, assign(socket, inputs: inputs)}
  end

  def handle_event("global-update", params, socket) do
    enforce_access!(:scale_queues, socket.assigns.access)

    inputs =
      if is_nil(params["global_allowed"]) do
        send(self(), {:scale_queue, socket.assigns.queue, global_limit: nil})

        socket.assigns.inputs
        |> Map.replace!(:global_allowed, nil)
        |> Map.replace!(:global_partition_fields, nil)
        |> Map.replace!(:global_partition_keys, nil)
      else
        allowed = String.to_integer(params["global_allowed"])
        fields = maybe_split(params["global_partition_fields"])
        keys = maybe_split(params["global_partition_keys"])

        global_limit =
          case fields do
            [] ->
              %{allowed: allowed}

            ["worker"] ->
              %{allowed: allowed, partition: [fields: fields]}

            _ ->
              %{allowed: allowed, partition: [fields: fields, keys: keys]}
          end

        send(self(), {:scale_queue, socket.assigns.queue, global_limit: global_limit})

        socket.assigns.inputs
        |> Map.replace!(:global_allowed, allowed)
        |> Map.replace!(:global_partition_fields, Enum.join(fields, ","))
        |> Map.replace!(:global_partition_keys, Enum.join(keys, ","))
      end

    {:noreply, assign(socket, inputs: inputs)}
  end

  def handle_event("rate-limit-update", params, socket) do
    enforce_access!(:scale_queues, socket.assigns.access)

    inputs =
      if is_nil(params["rate_allowed"]) do
        send(self(), {:scale_queue, socket.assigns.queue, rate_limit: nil})

        socket.assigns.inputs
        |> Map.replace!(:rate_allowed, nil)
        |> Map.replace!(:rate_period, nil)
        |> Map.replace!(:rate_partition_fields, nil)
        |> Map.replace!(:rate_partition_keys, nil)
      else
        allowed = String.to_integer(params["rate_allowed"])
        period = String.to_integer(params["rate_period"])
        fields = maybe_split(params["rate_partition_fields"])
        keys = maybe_split(params["rate_partition_keys"])

        rate_limit =
          case fields do
            [] ->
              %{allowed: allowed, period: period}

            ["worker"] ->
              %{allowed: allowed, period: period, partition: [fields: fields]}

            _ ->
              %{allowed: allowed, period: period, partition: [fields: fields, keys: keys]}
          end

        send(self(), {:scale_queue, socket.assigns.queue, rate_limit: rate_limit})

        socket.assigns.inputs
        |> Map.replace!(:rate_allowed, allowed)
        |> Map.replace!(:rate_period, period)
        |> Map.replace!(:rate_partition_fields, Enum.join(fields, ","))
        |> Map.replace!(:rate_partition_keys, Enum.join(keys, ","))
      end

    {:noreply, assign(socket, inputs: inputs)}
  end

  def handle_event("increment", %{"field" => field}, socket) do
    {:noreply, change_input(socket, field, 1)}
  end

  def handle_event("decrement", %{"field" => field}, socket) do
    {:noreply, change_input(socket, field, -1)}
  end

  def handle_event("toggle-feature", %{"feature" => "global"}, socket) do
    inputs =
      Map.update!(socket.assigns.inputs, :global_allowed, fn value ->
        if is_nil(value), do: socket.assigns.inputs.local_limit, else: nil
      end)

    {:noreply, assign(socket, inputs: inputs)}
  end

  def handle_event("toggle-feature", %{"feature" => "rate-limit"}, socket) do
    inputs =
      if is_nil(socket.assigns.inputs.rate_allowed) do
        socket.assigns.inputs
        |> Map.put(:rate_allowed, socket.assigns.inputs.local_limit)
        |> Map.put(:rate_period, 60)
      else
        socket.assigns.inputs
        |> Map.put(:rate_allowed, nil)
        |> Map.put(:rate_period, nil)
        |> Map.put(:rate_partition_fields, nil)
        |> Map.put(:rate_partition_keys, nil)
      end

    {:noreply, assign(socket, inputs: inputs)}
  end

  # Components

  defp toggle_button(assigns) do
    ~H"""
    <button
      class="bg-gray-200 dark:bg-gray-800 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
      role="switch"
      aria-checked="false"
      disabled={@disabled}
      id={"toggle-#{@feature}"}
      phx-target={@myself}
      phx-click="toggle-feature"
      phx-value-feature={@feature}
      type="button"
    >
      <span class={"#{if @enabled, do: "translate-x-5", else: "translate-x-0"} pointer-events-none relative inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"}>
        <span
          class={"#{if @enabled, do: "opacity-0 ease-out duration-100", else: "opacity-100 ease-in duration-200"} absolute inset-0 h-full w-full flex items-center justify-center transition-opacity"}
          aria-hidden="true"
        >
          <Icons.x_mark class="h-3 w-3 text-gray-400" />
        </span>
        <span
          class={"#{if @enabled, do: "opacity-100 ease-in duration-200", else: "opacity-0 ease-out duration-100"} absolute inset-0 h-full w-full flex items-center justify-center transition-opacity"}
          aria-hidden="true"
        >
          <Icons.check class="h-3 w-3 text-blue-500" />
        </span>
      </span>
    </button>
    """
  end

  defp submit_input(assigns) do
    ~H"""
    <div class="flex items-center justify-end mt-4 space-x-2">
      <%= if @locked do %>
        <Icons.lock_closed class="w-5 h-5 text-gray-600 dark:text-gray-400" />
      <% end %>

      <button
        class={"block px-3 py-2 font-medium text-sm text-gray-600 dark:text-gray-100 bg-gray-300 dark:bg-blue-950 dark:bg-opacity-25 hover:bg-blue-500 hover:text-white dark:hover:bg-blue-500 dark:hover:text-white rounded-md shadow-sm #{if @disabled, do: "opacity-30 pointer-events-none"}"}
        disabled={@disabled}
        type="submit"
      >
        {@label}
      </button>
    </div>
    """
  end

  defp pro_blocker(assigns) do
    ~H"""
    <span
      rel="requires-pro"
      class="text-center w-full text-sm text-gray-600 dark:text-gray-300 absolute top-1/2 -mt-6 -ml-3"
    >
      Requires
      <a href="https://oban.pro" class="text-blue-500 font-semibold">
        Oban Pro <Icons.arrow_top_right_on_square class="w-3 h-3 inline-block align-text-top" />
      </a>
    </span>
    """
  end

  # Helpers

  defp local_limit([]), do: 0

  defp local_limit(checks) do
    checks
    |> Enum.map(& &1["local_limit"])
    |> Enum.max()
  end

  defp global_allowed(checks) do
    checks
    |> Enum.map(& &1["global_limit"])
    |> Enum.filter(&is_map/1)
    |> Enum.find_value(& &1["allowed"])
  end

  defp rate_allowed(checks) do
    checks
    |> Enum.map(& &1["rate_limit"])
    |> Enum.filter(&is_map/1)
    |> Enum.find_value(& &1["allowed"])
  end

  defp rate_period(checks) do
    checks
    |> Enum.map(& &1["rate_limit"])
    |> Enum.filter(&is_map/1)
    |> Enum.find_value(& &1["period"])
  end

  defp partition_value(checks, parent, key) do
    checks
    |> Enum.map(& &1[parent])
    |> Enum.filter(&is_map/1)
    |> List.first()
    |> case do
      %{"partition" => %{^key => [_ | _] = value}} -> Enum.join(value, ",")
      _ -> nil
    end
  end

  defp global_unchanged?(checks, inputs) do
    inputs.global_allowed == global_allowed(checks) and
      inputs.global_partition_fields == partition_value(checks, "global_limit", "fields") and
      inputs.global_partition_keys == partition_value(checks, "global_limit", "keys")
  end

  defp rate_unchanged?(checks, inputs) do
    inputs.rate_allowed == rate_allowed(checks) and
      inputs.rate_period == rate_period(checks) and
      inputs.rate_partition_fields == partition_value(checks, "rate_limit", "fields") and
      inputs.rate_partition_keys == partition_value(checks, "rate_limit", "keys")
  end

  defp change_input(socket, field, change) do
    field = String.to_existing_atom(field)

    inputs =
      Map.update!(socket.assigns.inputs, field, fn value ->
        if is_integer(value) and value + change > 0 do
          value + change
        else
          value
        end
      end)

    assign(socket, inputs: inputs)
  end

  defp maybe_split(""), do: []
  defp maybe_split(nil), do: []
  defp maybe_split(value) when is_binary(value), do: String.split(value, ",")

  defp partition_options do
    [Off: nil, Worker: "worker", Args: "args", "Worker + Args": "args,worker"]
  end

  defp keyless_partition?(inputs), do: inputs not in ["args", "args,worker"]

  # Pro Helpers

  defp missing_pro?(%Config{engine: engine}) do
    engine in [Oban.Queue.BasicEngine, Oban.Engines.Basic]
  end
end
