defmodule TarakanWeb.AccountLive.Settings do
  use TarakanWeb, :live_view

  on_mount {TarakanWeb.AccountAuth, :require_sudo_mode}

  alias Tarakan.Accounts
  alias Tarakan.Accounts.{ApiCredential, ApiCredentials, SshKeys}
  alias Tarakan.ModelAnalytics
  alias Tarakan.Repositories
  alias TarakanWeb.AccountAuth

  # One long scroll made every panel equally prominent, so the two people
  # actually use daily - email and credentials - sat next to the API reference.
  # Each panel is now its own URL behind a rail, and the rail carries the
  # counts so nothing has to be opened to be seen.
  @sections [
    %{id: :account, slug: nil, label: "Account", icon: "hero-user-circle"},
    %{id: :credentials, slug: "credentials", label: "Credentials", icon: "hero-key"},
    %{id: :ssh, slug: "ssh-keys", label: "SSH keys", icon: "hero-command-line"},
    %{id: :agents, slug: "agents", label: "Agents", icon: "hero-cpu-chip"}
  ]

  @impl true
  def render(assigns) do
    ~H"""
    <Layouts.app flash={@flash} current_scope={@current_scope} current_path={assigns[:current_path]}>
      <Layouts.page width={:wide}>
        <div class="mx-auto w-full max-w-5xl">
          <header class="border-b-2 border-strong pb-5">
            <h1 class="font-display text-2xl font-medium uppercase leading-9 tracking-[0.02em] text-ink">
              Account settings
            </h1>
            <p class="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 font-mono text-xs">
              <span class="text-ink-muted">@{@current_scope.account.handle}</span>
              <span class="text-ink-faint">{@current_scope.account.email}</span>
              <span class={if @email_confirmed?, do: "text-phosphor", else: "text-signal"}>
                {if @email_confirmed?, do: "confirmed", else: "unconfirmed"}
              </span>
            </p>
          </header>

          <div class="mt-6 grid gap-6 lg:grid-cols-[13rem_minmax(0,1fr)] lg:gap-8">
            <nav
              id="settings-navigation"
              aria-label="Account settings"
              class="flex overflow-x-auto overscroll-x-contain border-b-2 border-rule text-sm [-ms-overflow-style:none] [scrollbar-width:none] lg:sticky lg:top-20 lg:block lg:self-start lg:overflow-visible lg:border-b-0 lg:border-l-2 [&::-webkit-scrollbar]:hidden"
            >
              <.settings_tab
                :for={section <- sections()}
                id={"settings-tab-#{section.id}"}
                active={@section == section.id}
                patch={settings_path(section.slug)}
                icon={section.icon}
                label={section.label}
                badge={section_badge(section.id, assigns)}
                badge_class={section_badge_class(section.id, assigns)}
              />
            </nav>

            <div class="min-w-0">
              <.account_section
                :if={@section == :account}
                email_form={@email_form}
                password_form={@password_form}
                trigger_submit={@trigger_submit}
                email_confirmed?={@email_confirmed?}
                identity_providers={@identity_providers}
              />
              <.credentials_section
                :if={@section == :credentials}
                api_token={@api_token}
                api_credentials={@api_credentials}
                api_base_url={@api_base_url}
                credential_form={@credential_form}
                email_confirmed?={@email_confirmed?}
              />
              <.ssh_section
                :if={@section == :ssh}
                handle={@current_scope.account.handle}
                ssh_keys={@ssh_keys}
                ssh_key_form={@ssh_key_form}
                email_confirmed?={@email_confirmed?}
              />
              <.agents_section :if={@section == :agents} model_scoreboard={@model_scoreboard} />
            </div>
          </div>
        </div>
      </Layouts.page>
    </Layouts.app>
    """
  end

  # The active entry stays a link so it is focusable and re-clickable;
  # aria-current carries "you are here". Horizontal rail on narrow screens,
  # vertical rail from lg up - the same element either way.
  attr :id, :string, required: true
  attr :active, :boolean, required: true
  attr :patch, :string, required: true
  attr :icon, :string, required: true
  attr :label, :string, required: true
  attr :badge, :string, default: nil
  attr :badge_class, :string, default: nil

  defp settings_tab(assigns) do
    ~H"""
    <.link
      id={@id}
      patch={@patch}
      aria-current={@active && "page"}
      class={[
        "-mb-0.5 flex shrink-0 items-center gap-2 whitespace-nowrap border-b-2 px-3 py-3 transition",
        "lg:-ml-0.5 lg:mb-0 lg:w-full lg:border-b-0 lg:border-l-2 lg:px-4 lg:py-2.5",
        @active && "border-signal font-semibold text-ink lg:bg-panel",
        !@active && "border-transparent text-ink-muted hover:border-rule hover:text-ink"
      ]}
    >
      <.icon name={@icon} class="size-4 shrink-0" />
      <span>{@label}</span>
      <span :if={@badge} class={["font-mono text-[11px] tabular-nums lg:ml-auto", @badge_class]}>
        {@badge}
      </span>
    </.link>
    """
  end

  defp account_section(assigns) do
    ~H"""
    <div class="space-y-5">
      <.form
        for={@email_form}
        id="email_form"
        phx-submit="update_email"
        phx-change="validate_email"
        class="space-y-3 border-2 border-strong bg-panel p-6"
      >
        <h2 class="text-sm font-semibold text-ink">Email address</h2>
        <.input
          field={@email_form[:email]}
          type="email"
          label="Email"
          autocomplete="username"
          spellcheck="false"
          required
        />
        <.button variant="primary" phx-disable-with="Changing...">Save email</.button>
      </.form>

      <section
        :if={not @email_confirmed?}
        id="email-confirmation-required"
        class="border-2 border-strong bg-panel p-6"
      >
        <h2 class="text-sm font-semibold text-ink">Confirm your email to unlock credentials</h2>
        <p class="mt-1 text-xs leading-5 text-ink-faint">
          Setting a password, adding SSH keys, and creating client credentials require a
          confirmed email address. Sign in with the login link we emailed you - or request a
          new one from the
          <.link navigate={~p"/accounts/log-in"} class="text-signal hover:underline">login page</.link>
          - to confirm it.
        </p>
      </section>

      <.form
        :if={@email_confirmed?}
        for={@password_form}
        id="password_form"
        action={~p"/accounts/update-password"}
        method="post"
        phx-change="validate_password"
        phx-submit="update_password"
        phx-trigger-action={@trigger_submit}
        class="space-y-3 border-2 border-strong bg-panel p-6"
      >
        <h2 class="text-sm font-semibold text-ink">Password</h2>
        <p class="text-xs leading-5 text-ink-faint">
          At least 15 characters.
        </p>
        <.input
          field={@password_form[:password]}
          type="password"
          label="New password"
          autocomplete="new-password"
          spellcheck="false"
          required
        />
        <.input
          field={@password_form[:password_confirmation]}
          type="password"
          label="Confirm new password"
          autocomplete="new-password"
          spellcheck="false"
        />
        <.button variant="primary" phx-disable-with="Saving...">
          Save password
        </.button>
      </.form>

      <section class="border-2 border-strong bg-panel p-6">
        <h2 class="text-sm font-semibold text-ink">Connected code hosts</h2>
        <div class="mt-4 grid gap-3 sm:grid-cols-2">
          <.link
            id="settings-github-identity"
            href={~p"/auth/github?#{[return_to: ~p"/accounts/settings"]}"}
            class="flex items-center justify-between border-2 border-rule px-4 py-3 text-sm text-ink-muted transition hover:bg-panel"
          >
            <span>GitHub</span>
            <span class={
              if MapSet.member?(@identity_providers, "github"),
                do: "text-signal",
                else: "text-ink-faint"
            }>
              {if MapSet.member?(@identity_providers, "github"), do: "Connected", else: "Connect"}
            </span>
          </.link>
          <.link
            id="settings-gitlab-identity"
            href={~p"/auth/gitlab?#{[return_to: ~p"/accounts/settings"]}"}
            class="flex items-center justify-between border-2 border-rule px-4 py-3 text-sm text-ink-muted transition hover:bg-panel"
          >
            <span>GitLab</span>
            <span class={
              if MapSet.member?(@identity_providers, "gitlab"),
                do: "text-signal",
                else: "text-ink-faint"
            }>
              {if MapSet.member?(@identity_providers, "gitlab"), do: "Connected", else: "Connect"}
            </span>
          </.link>
        </div>
      </section>
    </div>
    """
  end

  defp credentials_section(assigns) do
    ~H"""
    <section id="api-token" class="border-2 border-strong bg-panel p-6">
      <h2 class="text-sm font-semibold text-ink">Client credentials</h2>
      <p class="mt-1 text-xs leading-5 text-ink-faint">
        <span class="font-mono">TARAKAN_API_TOKEN</span>, sent as <span class="font-mono">Authorization: Bearer &lt;token&gt;</span>. Defaults expire after {ApiCredentials.default_validity_days()} days (max {ApiCredentials.maximum_validity_days()}).
      </p>
      <div :if={@api_token} class="mt-4">
        <p class="font-mono text-[11px] text-ink-faint">
          Shown only once.
        </p>
        <p
          id="api-token-value"
          class="mt-1 break-all border-2 border-rule px-3 py-2 font-mono text-xs text-ink"
        >
          {@api_token}
        </p>
      </div>
      <div
        :if={@api_credentials != []}
        id="api-credentials"
        class="mt-4 divide-y-2 divide-rule border-y-2 border-rule"
      >
        <div
          :for={credential <- @api_credentials}
          id={"api-credential-#{credential.id}"}
          class="flex items-center justify-between gap-4 py-3"
        >
          <div class="min-w-0">
            <p class="truncate text-xs font-semibold text-ink">{credential.name}</p>
            <p class="mt-1 font-mono text-[11px] text-ink-faint">
              {credential.token_prefix}… · {credential_status(credential)}
            </p>
            <p class="mt-1 font-mono text-[11px] text-ink-faint">
              {Enum.join(credential.scopes, " · ")}
              <span :if={credential.repository}>
                · {credential.repository.owner}/{credential.repository.name}
              </span>
            </p>
          </div>
          <.button
            :if={ApiCredential.active?(credential)}
            variant="danger"
            size="sm"
            id={"revoke-api-credential-#{credential.id}"}
            phx-click="revoke_api_credential"
            phx-value-id={credential.id}
            data-confirm="Revoke this credential? Clients using it will immediately lose access."
          >
            Revoke
          </.button>
        </div>
      </div>
      <.form
        :if={@email_confirmed?}
        for={@credential_form}
        id="api-credential-form"
        phx-submit="generate_api_token"
        class="mt-5 space-y-4 border-t-2 border-rule pt-5"
      >
        <.input
          field={@credential_form[:name]}
          type="text"
          label="Credential name"
          maxlength="80"
          required
        />
        <fieldset>
          <legend class="text-xs font-semibold text-ink">Permissions</legend>
          <div class="mt-2 grid gap-2 sm:grid-cols-2">
            <label
              :for={scope <- ApiCredential.scopes()}
              class="flex items-start gap-2 border-2 border-rule px-3 py-2 text-xs text-ink-muted"
            >
              <input
                type="checkbox"
                name="credential[scopes][]"
                value={scope}
                checked={scope in @credential_form.params["scopes"]}
                class="mt-0.5 border-rule text-signal focus:ring-phosphor"
              />
              <span>
                <span class="block font-mono text-ink">{scope}</span>
                <span class="mt-0.5 block text-ink-faint">{credential_scope_label(scope)}</span>
              </span>
            </label>
          </div>
        </fieldset>
        <.input
          field={@credential_form[:repository]}
          type="text"
          label="Limit to one repository (optional)"
          placeholder="owner/repository"
          autocomplete="off"
          spellcheck="false"
        />
        <.button id="generate-api-token-button" variant="primary">
          Generate client credential
        </.button>
      </.form>

      <details id="api-reference" class="group mt-5 border-t-2 border-rule pt-5">
        <summary class="flex cursor-pointer list-none items-center justify-between gap-4 text-sm font-semibold text-ink marker:hidden">
          <span>API reference</span>
          <span class="font-mono text-[11px] font-normal uppercase tracking-[0.12em] text-ink-faint group-open:hidden">
            Open
          </span>
          <span class="hidden font-mono text-[11px] font-normal uppercase tracking-[0.12em] text-ink-faint group-open:inline">
            Close
          </span>
        </summary>

        <div class="mt-4 space-y-5 text-xs leading-5 text-ink-muted">
          <div>
            <p class="font-semibold text-ink">Authentication</p>
            <p class="mt-1">
              Base URL:
              <code id="api-reference-base-url" class="font-mono text-ink">{@api_base_url}</code>
            </p>
            <p>
              Send <code class="font-mono text-ink">Authorization: Bearer &lt;token&gt;</code>
              and <code class="font-mono text-ink">Accept: application/json</code>.
            </p>
            <pre class="mt-2 overflow-x-auto border-2 border-rule bg-ground p-3 font-mono text-[11px] leading-5 text-ink"><code>curl "{@api_base_url}/jobs" \
    -H "Authorization: Bearer $TARAKAN_API_TOKEN" \
    -H "Accept: application/json"</code></pre>
          </div>

          <div>
            <p class="font-semibold text-ink">Client authorization</p>
            <dl class="mt-2 grid gap-x-3 gap-y-1 font-mono text-[11px] sm:grid-cols-[4rem_1fr]">
              <dt class="text-ink">POST</dt><dd>/client-auth/start</dd>
              <dt class="text-ink">POST</dt><dd>/client-auth/exchange</dd>
              <dt class="text-ink">DELETE</dt><dd>/client-auth/session</dd>
            </dl>
            <p class="mt-2">
              Start with <code class="font-mono text-ink">&#123;&quot;client_name&quot;:&quot;Tarakan Client&quot;&#125;</code>,
              open the returned verification URL, then poll exchange with
              <code class="font-mono text-ink">&#123;&quot;device_code&quot;:&quot;...&quot;&#125;</code>
              at the returned interval. Session revocation requires the issued bearer token.
            </p>
          </div>

          <div>
            <p class="font-semibold text-ink">Repository discovery</p>
            <dl class="mt-2 grid gap-x-3 gap-y-1 font-mono text-[11px] sm:grid-cols-[4rem_1fr]">
              <dt class="text-ink">GET</dt><dd>/repositories</dd>
              <dt class="text-ink">POST</dt><dd>/repositories</dd>
            </dl>
            <p class="mt-2">
              List with <code class="font-mono text-ink">status=unscanned</code>
              and <code class="font-mono text-ink">limit=100</code>.
              Register with
              <code class="font-mono text-ink">&#123;&quot;url&quot;:&quot;owner/name&quot;&#125;</code>
              (scopes: findings:submit or reviews:submit).
            </p>
          </div>

          <div>
            <p class="font-semibold text-ink">Jobs</p>
            <dl class="mt-2 grid gap-x-3 gap-y-1 font-mono text-[11px] sm:grid-cols-[4rem_1fr]">
              <dt class="text-ink">GET</dt><dd>/jobs</dd>
              <dt class="text-ink">GET</dt><dd>/:host/:owner/:name/jobs</dd>
              <dt class="text-ink">GET</dt><dd>/jobs/:id</dd>
              <dt class="text-ink">POST</dt><dd>/jobs/:id/claim</dd>
              <dt class="text-ink">POST</dt><dd>/jobs/:id/claim/renew</dd>
              <dt class="text-ink">DELETE</dt><dd>/jobs/:id/claim</dd>
              <dt class="text-ink">POST</dt><dd>/jobs/:id/complete</dd>
            </dl>
            <p class="mt-2">
              Scopes: <code class="font-mono text-ink">tasks:read</code>, <code class="font-mono text-ink">tasks:claim</code>, and <code class="font-mono text-ink">contributions:write</code>.
            </p>
          </div>

          <div>
            <p class="font-semibold text-ink">Reports and Checks</p>
            <dl class="mt-2 grid gap-x-3 gap-y-1 font-mono text-[11px] sm:grid-cols-[4rem_1fr]">
              <dt class="text-ink">GET</dt><dd>/:host/:owner/:name/reports</dd>
              <dt class="text-ink">POST</dt><dd>/:host/:owner/:name/reports</dd>
              <dt class="text-ink">GET</dt><dd>/:host/:owner/:name/memory</dd>
              <dt class="text-ink">POST</dt><dd>/:host/:owner/:name/reports/:id/check</dd>
              <dt class="text-ink">POST</dt><dd>
                /:host/:owner/:name/findings/:public_id/check
              </dd>
            </dl>
            <p class="mt-2">
              Use <code class="font-mono text-ink">reviews:submit</code>
              to publish, <code class="font-mono text-ink">reviews:read</code>
              for restricted evidence,
              and <code class="font-mono text-ink">reviews:verify</code>
              to check findings.
              Checks also require qualified reviewer standing.
            </p>
          </div>

          <div class="border-l-2 border-rule pl-3">
            <p class="font-semibold text-ink">Repository host</p>
            <p class="mt-1">
              Use <code class="font-mono text-ink">github.com</code>
              or <code class="font-mono text-ink">tarakan.lol</code>
              for <code class="font-mono text-ink">:host</code>. JSON bodies only;
              validation failures return field errors with a non-2xx status.
            </p>
          </div>
        </div>
      </details>
    </section>
    """
  end

  defp ssh_section(assigns) do
    ~H"""
    <section id="ssh-keys" class="border-2 border-strong bg-panel p-6">
      <h2 class="text-sm font-semibold text-ink">SSH keys</h2>
      <p class="mt-1 font-mono text-xs leading-5 text-ink-faint">
        git clone ssh://git@&lt;host&gt;/{@handle}/&lt;name&gt;.git
      </p>
      <div
        :if={@ssh_keys != []}
        id="ssh-key-list"
        class="mt-4 divide-y-2 divide-rule border-y-2 border-rule"
      >
        <div
          :for={key <- @ssh_keys}
          id={"ssh-key-#{key.id}"}
          class="flex items-center justify-between gap-4 py-3"
        >
          <div class="min-w-0">
            <p class="truncate text-xs font-semibold text-ink">{key.name}</p>
            <p class="mt-1 break-all font-mono text-[11px] text-ink-faint">
              {key.fingerprint_sha256}
            </p>
            <p class="mt-1 font-mono text-[11px] text-ink-faint">
              {key.key_type} · {ssh_key_last_used(key)}
            </p>
          </div>
          <.button
            variant="danger"
            size="sm"
            id={"delete-ssh-key-#{key.id}"}
            phx-click="delete_ssh_key"
            phx-value-id={key.id}
            data-confirm="Remove this key? Clients using it will immediately lose SSH access."
          >
            Remove
          </.button>
        </div>
      </div>
      <.form
        :if={@email_confirmed?}
        for={@ssh_key_form}
        id="ssh-key-form"
        phx-submit="add_ssh_key"
        class="mt-5 space-y-4 border-t-2 border-rule pt-5"
      >
        <.input
          field={@ssh_key_form[:name]}
          type="text"
          label="Key name"
          placeholder="work laptop"
          maxlength="100"
          required
        />
        <.input
          field={@ssh_key_form[:public_key]}
          type="textarea"
          label="Public key"
          placeholder="ssh-ed25519 AAAA…"
          rows="3"
          spellcheck="false"
          required
        />
        <p class="text-xs leading-5 text-ink-faint">
          ed25519, ECDSA, and RSA (3072-bit or larger) keys are accepted.
        </p>
        <.button id="add-ssh-key-button" variant="primary">Add SSH key</.button>
      </.form>
    </section>
    """
  end

  # Which of the agents this contributor pays for actually finds real bugs,
  # measured against their own submissions rather than the corpus average.
  defp agents_section(assigns) do
    ~H"""
    <section id="settings-model-scoreboard" class="border-2 border-strong bg-panel p-6">
      <div class="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
        <h2 class="text-sm font-semibold text-ink">Your agents</h2>
        <.link navigate={~p"/models"} class="font-mono text-[11px] text-signal hover:underline">
          All models →
        </.link>
      </div>

      <p :if={@model_scoreboard == []} class="mt-4 font-mono text-xs text-ink-faint">
        None yet.
      </p>

      <div
        :if={@model_scoreboard != []}
        class="mt-4 overflow-x-auto border-2 border-rule bg-ground"
      >
        <table class="w-full min-w-[28rem] border-collapse text-left">
          <thead>
            <tr class="border-b border-rule font-mono text-[10px] uppercase tracking-[0.12em] text-ink-faint">
              <th class="px-3 py-2 font-normal">Model</th>
              <th class="w-20 px-2 py-2 text-right font-normal">Findings</th>
              <th class="w-24 px-2 py-2 text-right font-normal">Confirmed</th>
              <th class="w-24 px-2 py-2 text-right font-normal">Disputed</th>
              <th class="w-24 px-3 py-2 text-right font-normal">Precision</th>
            </tr>
          </thead>
          <tbody class="divide-y divide-rule">
            <tr :for={row <- @model_scoreboard} id={"settings-model-#{row.model}"}>
              <td class="min-w-0 px-3 py-2.5">
                <span class="block truncate font-mono text-xs font-semibold text-ink">
                  {row.model}
                </span>
              </td>
              <td class="px-2 py-2.5 text-right font-mono text-xs tabular-nums text-ink">
                {row.findings}
              </td>
              <td class="px-2 py-2.5 text-right font-mono text-xs tabular-nums text-phosphor">
                {row.confirmed}
              </td>
              <td class="px-2 py-2.5 text-right font-mono text-xs tabular-nums text-ink-muted">
                {row.disputed}
              </td>
              <td class="px-3 py-2.5 text-right font-mono text-xs tabular-nums text-ink">
                {if row.precision, do: "#{row.precision}%", else: "-"}
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </section>
    """
  end

  @impl true
  def mount(%{"token" => token}, _session, socket) do
    socket =
      case Accounts.update_account_email(socket.assigns.current_scope.account, token) do
        {:ok, _account} ->
          put_flash(socket, :info, "Email changed successfully.")

        {:error, _} ->
          put_flash(socket, :error, "Email change link is invalid or it has expired.")
      end

    {:ok, push_navigate(socket, to: ~p"/accounts/settings")}
  end

  def mount(_params, _session, socket) do
    account = socket.assigns.current_scope.account
    email_changeset = Accounts.change_account_email(account, %{}, validate_unique: false)
    password_changeset = Accounts.change_account_password(account, %{}, hash_password: false)

    identity_providers =
      account
      |> Accounts.list_external_identities()
      |> MapSet.new(& &1.provider)

    socket =
      socket
      |> assign(:email_form, to_form(email_changeset))
      |> assign(:password_form, to_form(password_changeset))
      |> assign(:identity_providers, identity_providers)
      |> assign(:trigger_submit, false)
      |> assign(:email_confirmed?, not is_nil(account.confirmed_at))
      |> assign(:api_token, nil)
      |> assign(:api_credentials, ApiCredentials.list(account))
      |> assign(:api_base_url, TarakanWeb.Endpoint.url() <> "/api")
      |> assign(:credential_form, credential_form())
      |> assign(:ssh_keys, SshKeys.list_for_account(account))
      |> assign(:ssh_key_form, ssh_key_form())
      |> assign(:model_scoreboard, ModelAnalytics.account_model_scoreboard(account.id))
      |> assign(:section, :account)

    {:ok, socket}
  end

  @impl true
  def handle_params(params, _uri, socket) do
    {:noreply, assign(socket, :section, section_from_params(params))}
  end

  @impl true
  def handle_event("validate_email", params, socket) do
    %{"account" => account_params} = params

    email_form =
      socket.assigns.current_scope.account
      |> Accounts.change_account_email(account_params, validate_unique: false)
      |> Map.put(:action, :validate)
      |> to_form()

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

  def handle_event("update_email", params, socket) do
    %{"account" => account_params} = params

    with :ok <- ensure_sudo(socket),
         account <- socket.assigns.current_scope.account do
      case Accounts.change_account_email(account, account_params) do
        %{valid?: true} = changeset ->
          Accounts.deliver_account_update_email_instructions(
            Ecto.Changeset.apply_action!(changeset, :insert),
            account.email,
            &url(~p"/accounts/settings/confirm-email/#{&1}")
          )

          info = "A link to confirm your email change has been sent to the new address."
          {:noreply, socket |> put_flash(:info, info)}

        changeset ->
          {:noreply, assign(socket, :email_form, to_form(changeset, action: :insert))}
      end
    else
      {:error, :sudo_required} -> reauth_settings(socket)
    end
  end

  def handle_event("validate_password", params, socket) do
    %{"account" => account_params} = params

    password_form =
      socket.assigns.current_scope.account
      |> Accounts.change_account_password(account_params, hash_password: false)
      |> Map.put(:action, :validate)
      |> to_form()

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

  def handle_event("generate_api_token", %{"credential" => params}, socket) do
    with :ok <- ensure_sudo(socket),
         account <- socket.assigns.current_scope.account,
         {:ok, repository_id} <- credential_repository_id(socket, params["repository"]),
         attrs <- %{
           "name" => params["name"],
           "scopes" => List.wrap(params["scopes"]),
           "repository_id" => repository_id
         },
         {:ok, token, _credential} <- ApiCredentials.create(account, attrs) do
      {:noreply,
       assign(socket,
         api_token: token,
         api_credentials: ApiCredentials.list(account),
         credential_form: credential_form()
       )}
    else
      {:error, :sudo_required} ->
        reauth_settings(socket)

      {:error, :repository_not_found} ->
        {:noreply,
         socket
         |> assign(:credential_form, credential_form(params))
         |> put_flash(:error, "Choose a registered repository you are allowed to view.")}

      {:error, :credential_limit} ->
        {:noreply,
         socket
         |> assign(:credential_form, credential_form(params))
         |> put_flash(:error, "Revoke an active credential before creating another one.")}

      {:error, %Ecto.Changeset{}} ->
        {:noreply,
         socket
         |> assign(:credential_form, credential_form(params))
         |> put_flash(:error, "Choose a name and at least one valid permission.")}

      {:error, :unconfirmed_email} ->
        {:noreply,
         put_flash(socket, :error, "Confirm your email address before creating credentials.")}

      {:error, _reason} ->
        {:noreply, put_flash(socket, :error, "The credential could not be created.")}
    end
  end

  def handle_event("generate_api_token", _params, socket) do
    handle_event(
      "generate_api_token",
      %{"credential" => socket.assigns.credential_form.params},
      socket
    )
  end

  def handle_event("revoke_api_credential", %{"id" => credential_id}, socket) do
    with :ok <- ensure_sudo(socket),
         account <- socket.assigns.current_scope.account,
         {:ok, _credential} <- ApiCredentials.revoke(account, credential_id) do
      {:noreply,
       socket
       |> assign(:api_credentials, ApiCredentials.list(account))
       |> put_flash(:info, "Client credential revoked.")}
    else
      {:error, :sudo_required} ->
        reauth_settings(socket)

      {:error, :not_found} ->
        {:noreply, put_flash(socket, :error, "Client credential not found.")}
    end
  end

  def handle_event("add_ssh_key", %{"ssh_key" => params}, socket) do
    with :ok <- ensure_sudo(socket),
         account <- socket.assigns.current_scope.account do
      case SshKeys.add_key(account, params) do
        {:ok, _key} ->
          {:noreply,
           socket
           |> assign(:ssh_keys, SshKeys.list_for_account(account))
           |> assign(:ssh_key_form, ssh_key_form())
           |> put_flash(:info, "SSH key added.")}

        {:error, :key_limit} ->
          {:noreply,
           socket
           |> assign(:ssh_key_form, ssh_key_form(params))
           |> put_flash(:error, "Remove an existing key before adding another one.")}

        {:error, :unconfirmed_email} ->
          {:noreply,
           put_flash(socket, :error, "Confirm your email address before adding SSH keys.")}

        {:error, %Ecto.Changeset{} = changeset} ->
          {:noreply, assign(socket, :ssh_key_form, to_form(changeset, as: :ssh_key))}
      end
    else
      {:error, :sudo_required} -> reauth_settings(socket)
    end
  end

  def handle_event("delete_ssh_key", %{"id" => key_id}, socket) do
    with :ok <- ensure_sudo(socket),
         account <- socket.assigns.current_scope.account,
         {:ok, _key} <- SshKeys.delete_key(account, key_id) do
      {:noreply,
       socket
       |> assign(:ssh_keys, SshKeys.list_for_account(account))
       |> put_flash(:info, "SSH key removed.")}
    else
      {:error, :sudo_required} -> reauth_settings(socket)
      {:error, :not_found} -> {:noreply, put_flash(socket, :error, "SSH key not found.")}
    end
  end

  def handle_event("update_password", params, socket) do
    %{"account" => account_params} = params

    with :ok <- ensure_sudo(socket),
         account <- socket.assigns.current_scope.account do
      case Accounts.change_account_password(account, account_params) do
        %{valid?: true} = changeset ->
          {:noreply, assign(socket, trigger_submit: true, password_form: to_form(changeset))}

        changeset ->
          {:noreply, assign(socket, password_form: to_form(changeset, action: :insert))}
      end
    else
      {:error, :sudo_required} -> reauth_settings(socket)
    end
  end

  defp sections, do: @sections

  defp settings_path(nil), do: ~p"/accounts/settings"
  defp settings_path(slug), do: ~p"/accounts/settings/#{slug}"

  # An unknown slug falls back to the first panel rather than 404ing: the URL
  # is a convenience for deep links, not a resource.
  defp section_from_params(%{"section" => slug}) do
    case Enum.find(@sections, &(&1.slug == slug)) do
      %{id: id} -> id
      nil -> :account
    end
  end

  defp section_from_params(_params), do: :account

  defp section_slug(id) do
    case Enum.find(@sections, &(&1.id == id)) do
      %{slug: slug} -> slug
      nil -> nil
    end
  end

  defp section_badge(:account, %{email_confirmed?: false}), do: "unconfirmed"
  defp section_badge(:account, _assigns), do: nil

  defp section_badge(:credentials, %{api_credentials: credentials}),
    do: credentials |> Enum.count(&ApiCredential.active?/1) |> Integer.to_string()

  defp section_badge(:ssh, %{ssh_keys: keys}), do: keys |> length() |> Integer.to_string()

  defp section_badge(:agents, %{model_scoreboard: scoreboard}),
    do: scoreboard |> length() |> Integer.to_string()

  defp section_badge_class(:account, %{email_confirmed?: false}), do: "text-signal"
  defp section_badge_class(_section, _assigns), do: "text-ink-faint"

  defp ensure_sudo(socket) do
    if Accounts.sudo_mode?(socket.assigns.current_scope.account),
      do: :ok,
      else: {:error, :sudo_required}
  end

  # Come back to the panel the action was attempted from, not to the first one.
  defp reauth_settings(socket) do
    return_to = socket.assigns.section |> section_slug() |> settings_path()

    {:noreply,
     socket
     |> put_flash(
       :error,
       "Confirm it's you with a magic link before changing sensitive settings."
     )
     |> redirect(to: AccountAuth.reauth_path(return_to))}
  end

  defp credential_status(%ApiCredential{revoked_at: %DateTime{}}), do: "revoked"

  defp credential_status(%ApiCredential{expires_at: expires_at}) do
    if ApiCredential.active?(%ApiCredential{expires_at: expires_at}) do
      "expires " <> Calendar.strftime(expires_at, "%Y-%m-%d")
    else
      "expired"
    end
  end

  defp credential_form(params \\ %{}) do
    defaults = %{
      "name" => "Tarakan Client",
      "repository" => "",
      "scopes" => ApiCredentials.default_scopes()
    }

    to_form(Map.merge(defaults, params), as: :credential)
  end

  defp credential_repository_id(_socket, repository)
       when repository in [nil, ""],
       do: {:ok, nil}

  defp credential_repository_id(socket, reference) when is_binary(reference) do
    with {:ok, %{owner: owner, name: name}} <- Repositories.parse_github_repository(reference),
         %{} = repository <-
           Repositories.get_visible_github_repository(
             owner,
             name,
             socket.assigns.current_scope
           ) do
      {:ok, repository.id}
    else
      _invalid -> {:error, :repository_not_found}
    end
  end

  defp credential_scope_label("tasks:read"), do: "Read visible jobs"
  defp credential_scope_label("tasks:claim"), do: "Claim and release jobs"
  defp credential_scope_label("contributions:write"), do: "Submit task evidence"
  defp credential_scope_label("findings:submit"), do: "Submit quarantined scan results"
  defp credential_scope_label("reports:write"), do: "Report abuse and view your reports"

  defp credential_scope_label("reviews:read"),
    do: "Read restricted review findings (reviewer tier)"

  defp credential_scope_label("reviews:verify"), do: "Record checks on reports (reviewer tier)"
  defp credential_scope_label("repo:read"), do: "Clone your visible hosted repositories"
  defp credential_scope_label("repo:write"), do: "Push to hosted repositories you steward"
  defp credential_scope_label(scope), do: scope

  defp ssh_key_form(params \\ %{}) do
    to_form(Map.merge(%{"name" => "", "public_key" => ""}, params), as: :ssh_key)
  end

  defp ssh_key_last_used(%{last_used_at: nil}), do: "never used"

  defp ssh_key_last_used(%{last_used_at: last_used_at}),
    do: "last used " <> Calendar.strftime(last_used_at, "%Y-%m-%d")
end
