defmodule TarakanWeb.AlertLive do
  @moduledoc """
  Watchlist management for finding alerts.

  Each watchlist is a named set of `owner/name` repository refs; the daily
  `Tarakan.Billing.AlertWorker` emails the account when verified or fixed
  findings first appear on those repositories (when `notify` is on).
  """

  use TarakanWeb, :live_view

  alias Tarakan.Billing
  alias Tarakan.Billing.Watchlist

  @impl true
  def mount(_params, _session, socket) do
    account = socket.assigns.current_scope.account

    {:ok,
     socket
     |> assign(:page_title, "Alerts")
     |> assign(:editing_id, nil)
     |> assign(:form, watchlist_form(%{}))
     |> stream(:watchlists, Billing.list_watchlists(account),
       dom_id: fn watchlist -> "watchlist-#{watchlist.id}" end
     )}
  end

  @impl true
  def handle_event("validate", %{"watchlist" => params}, socket) do
    {:noreply, assign(socket, :form, watchlist_form(params))}
  end

  def handle_event("save", %{"watchlist" => params}, socket) do
    attrs = %{"name" => params["name"], "entries" => parse_entries(params["entries"])}

    case socket.assigns.editing_id do
      nil -> create(socket, attrs)
      id -> save_edit(socket, id, attrs)
    end
  end

  def handle_event("edit", %{"id" => id}, socket) do
    watchlist = find_watchlist(socket, id)

    if watchlist do
      {:noreply,
       socket
       |> assign(:editing_id, watchlist.id)
       |> assign(
         :form,
         watchlist_form(%{
           "name" => watchlist.name,
           "entries" => Enum.join(watchlist.entries, "\n")
         })
       )}
    else
      {:noreply, socket}
    end
  end

  def handle_event("cancel_edit", _params, socket) do
    {:noreply,
     socket
     |> assign(:editing_id, nil)
     |> assign(:form, watchlist_form(%{}))}
  end

  def handle_event("delete", %{"id" => id}, socket) do
    with %Watchlist{} = watchlist <- find_watchlist(socket, id),
         {:ok, _} <- Billing.delete_watchlist(socket.assigns.current_scope, watchlist) do
      {:noreply,
       socket
       |> stream_delete(:watchlists, watchlist)
       |> put_flash(:info, "Watchlist deleted.")}
    else
      _other -> {:noreply, put_flash(socket, :error, "Could not delete that watchlist.")}
    end
  end

  def handle_event("toggle_notify", %{"id" => id}, socket) do
    with %Watchlist{} = watchlist <- find_watchlist(socket, id),
         {:ok, updated} <-
           Billing.update_watchlist(socket.assigns.current_scope, watchlist, %{
             "notify" => !watchlist.notify
           }) do
      {:noreply, stream_insert(socket, :watchlists, updated)}
    else
      _other -> {:noreply, put_flash(socket, :error, "Could not update that watchlist.")}
    end
  end

  defp create(socket, attrs) do
    case Billing.create_watchlist(socket.assigns.current_scope, attrs) do
      {:ok, watchlist} ->
        {:noreply,
         socket
         |> stream_insert(:watchlists, watchlist, at: 0)
         |> assign(:form, watchlist_form(%{}))
         |> put_flash(:info, "Watchlist created.")}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, put_flash(socket, :error, changeset_message(changeset))}
    end
  end

  defp save_edit(socket, id, attrs) do
    with %Watchlist{} = watchlist <- find_watchlist(socket, id),
         {:ok, updated} <-
           Billing.update_watchlist(socket.assigns.current_scope, watchlist, attrs) do
      {:noreply,
       socket
       |> stream_insert(:watchlists, updated)
       |> assign(:editing_id, nil)
       |> assign(:form, watchlist_form(%{}))
       |> put_flash(:info, "Watchlist updated.")}
    else
      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, put_flash(socket, :error, changeset_message(changeset))}

      _other ->
        {:noreply, put_flash(socket, :error, "Could not update that watchlist.")}
    end
  end

  # Watchlists are small and owner-scoped; re-reading the list keeps the
  # stream and form lookups simple.
  defp find_watchlist(socket, id) do
    id = if is_binary(id), do: String.to_integer(id), else: id
    account = socket.assigns.current_scope.account

    Enum.find(Billing.list_watchlists(account), fn watchlist ->
      watchlist.id == id
    end)
  end

  defp parse_entries(nil), do: []

  defp parse_entries(text) when is_binary(text) do
    text
    |> String.split("\n", trim: true)
    |> Enum.map(&String.trim/1)
    |> Enum.reject(&(&1 == ""))
  end

  defp changeset_message(changeset) do
    details =
      changeset
      |> Ecto.Changeset.traverse_errors(fn {message, _meta} -> message end)
      |> Enum.map_join("; ", fn {field, messages} -> "#{field} #{Enum.join(messages, ", ")}" end)

    "The watchlist could not be saved: #{details}."
  end

  defp watchlist_form(params), do: to_form(params, as: :watchlist)

  @impl true
  def render(assigns) do
    ~H"""
    <Layouts.app flash={@flash} current_scope={@current_scope} current_path={assigns[:current_path]}>
      <Layouts.page width={:focused} class="space-y-8">
        <header class="border-b-2 border-strong pb-6">
          <h1 class="font-display text-3xl font-medium uppercase leading-none tracking-[0.02em] text-ink">
            Alerts
          </h1>
          <p class="mt-3 text-sm leading-6 text-ink-muted">
            Watch repositories and get a daily email when verified or fixed findings first
            appear on them. Alert delivery requires an active plan (see <.link
              navigate={~p"/pricing"}
              class="text-ink underline"
            >pricing</.link>).
          </p>
        </header>

        <.form
          for={@form}
          id="watchlist-form"
          phx-change="validate"
          phx-submit="save"
          class="space-y-5 border-2 border-strong p-5"
        >
          <h2 class="font-display text-sm uppercase tracking-[0.12em] text-ink">
            {if @editing_id, do: "Edit watchlist", else: "New watchlist"}
          </h2>
          <.input field={@form[:name]} type="text" label="Name" required maxlength="100" />
          <.input
            field={@form[:entries]}
            type="textarea"
            label="Repositories - one owner/name per line"
            rows="5"
            placeholder="openai/codex\nacme/widget"
          />
          <div class="flex flex-wrap gap-3">
            <button
              id="watchlist-save-button"
              type="submit"
              class="cursor-pointer border-2 border-signal px-5 py-2 font-display text-xs uppercase tracking-[0.12em] text-signal transition hover:bg-signal hover:text-ground"
            >
              {if @editing_id, do: "Save changes", else: "Create watchlist"}
            </button>
            <button
              :if={@editing_id}
              id="watchlist-cancel-button"
              type="button"
              phx-click="cancel_edit"
              class="cursor-pointer border border-strong px-5 py-2 font-display text-xs uppercase tracking-[0.12em] text-ink transition hover:bg-panel"
            >
              Cancel
            </button>
          </div>
        </.form>

        <div id="watchlists" phx-update="stream" class="space-y-4">
          <div
            id="watchlists-empty"
            class="hidden only:block border-2 border-dashed border-rule p-5 text-sm text-ink-faint"
          >
            No watchlists yet - create one above to start getting alerts.
          </div>
          <div
            :for={{id, watchlist} <- @streams.watchlists}
            id={id}
            class="border-2 border-strong p-5"
          >
            <div class="flex flex-wrap items-start justify-between gap-3">
              <div class="min-w-0">
                <h3 class="font-display text-base uppercase tracking-[0.02em] text-ink">
                  {watchlist.name}
                </h3>
                <p class="mt-1 font-mono text-[11px] uppercase tracking-[0.12em] text-ink-faint">
                  {length(watchlist.entries)} repositories
                  <span :if={watchlist.last_notified_at}>
                    · last alerted {Calendar.strftime(watchlist.last_notified_at, "%Y-%m-%d")}
                  </span>
                </p>
              </div>
              <div class="flex flex-wrap gap-2">
                <button
                  id={"watchlist-notify-#{watchlist.id}"}
                  type="button"
                  phx-click="toggle_notify"
                  phx-value-id={watchlist.id}
                  class={[
                    "cursor-pointer border-2 px-3 py-1.5 font-mono text-[11px] uppercase tracking-[0.12em] transition",
                    watchlist.notify && "border-signal text-signal hover:bg-signal hover:text-ground",
                    !watchlist.notify && "border-strong text-ink-faint hover:bg-panel hover:text-ink"
                  ]}
                >
                  {if watchlist.notify, do: "Alerts on", else: "Alerts off"}
                </button>
                <button
                  id={"watchlist-edit-#{watchlist.id}"}
                  type="button"
                  phx-click="edit"
                  phx-value-id={watchlist.id}
                  class="cursor-pointer border border-strong px-3 py-1.5 font-mono text-[11px] uppercase tracking-[0.12em] text-ink transition hover:bg-panel"
                >
                  Edit
                </button>
                <button
                  id={"watchlist-delete-#{watchlist.id}"}
                  type="button"
                  phx-click="delete"
                  phx-value-id={watchlist.id}
                  data-confirm="Delete this watchlist?"
                  class="cursor-pointer border border-strong px-3 py-1.5 font-mono text-[11px] uppercase tracking-[0.12em] text-ink-faint transition hover:bg-panel hover:text-signal"
                >
                  Delete
                </button>
              </div>
            </div>
            <ul :if={watchlist.entries != []} class="mt-3 space-y-1 font-mono text-xs text-ink-muted">
              <li :for={entry <- watchlist.entries}>{entry}</li>
            </ul>
          </div>
        </div>
      </Layouts.page>
    </Layouts.app>
    """
  end
end
