defmodule MicrowavepropWeb.SubmitLive do
  @moduledoc false
  use MicrowavepropWeb, :live_view
  use LiveStash

  alias Microwaveprop.Propagation.BandConfig
  alias Microwaveprop.Radio
  alias Microwaveprop.Radio.AdifImport
  alias Microwaveprop.Radio.Contact
  alias Microwaveprop.Radio.CsvImport
  alias Microwaveprop.Workers.ContactWeatherEnqueueWorker

  @band_options BandConfig.band_options()

  @mode_options ~w(CW SSB FM FT8 FT4 Q65)

  @impl true
  def mount(_params, _session, socket) do
    socket =
      socket
      |> allow_upload(:csv,
        accept: ~w(.csv),
        max_entries: 1,
        max_file_size: 10_000_000,
        auto_upload: false
      )
      |> allow_upload(:adif,
        accept: :any,
        max_entries: 1,
        max_file_size: 10_000_000,
        auto_upload: false
      )

    changeset = Radio.change_contact(%Contact{})

    socket =
      assign(socket,
        page_title: "Submit Contact",
        form: to_form(changeset),
        band_options: @band_options,
        mode_options: @mode_options,
        submitted_at: nil,
        active_tab: :single,
        csv_preview: nil,
        csv_email: ""
      )

    case LiveStash.recover_state(socket) do
      {:recovered, socket} ->
        {:ok, socket}

      _ ->
        {:ok, socket}
    end
  end

  @impl true
  def handle_event("switch_tab", %{"tab" => tab}, socket) do
    socket = assign(socket, active_tab: String.to_existing_atom(tab))
    {:noreply, LiveStash.stash_assigns(socket, [:active_tab])}
  end

  def handle_event("validate", %{"contact" => contact_params}, socket) do
    changeset =
      %Contact{}
      |> Radio.change_contact(contact_params)
      |> Map.put(:action, :validate)

    {:noreply, assign(socket, form: to_form(changeset))}
  end

  def handle_event("validate_csv", _params, socket) do
    {:noreply, socket}
  end

  def handle_event("validate_adif", _params, socket) do
    {:noreply, socket}
  end

  # Minimum milliseconds between submissions per LiveView session.
  # For broader IP-based rate limiting, use a reverse proxy (nginx limit_req, Cloudflare).
  @submission_cooldown_ms 30_000

  def handle_event("save", %{"contact" => contact_params}, socket) do
    if recently_submitted?(socket) do
      {:noreply, put_flash(socket, :error, "Please wait before submitting another contact.")}
    else
      {contact_params, user_id} = merge_user_params(contact_params, socket)

      case Radio.create_contact(contact_params, user_id) do
        {:ok, contact} ->
          ContactWeatherEnqueueWorker.enqueue_for_contact(contact)

          {:noreply,
           socket
           |> assign(submitted_at: System.monotonic_time(:millisecond))
           |> put_flash(:info, "Contact submitted successfully!")
           |> push_navigate(to: ~p"/contacts/#{contact.id}")}

        {:error, :duplicate, existing} ->
          {:noreply,
           socket
           |> put_flash(
             :error,
             "A matching contact already exists: #{existing.station1} ↔ #{existing.station2} on #{existing.band} MHz"
           )
           |> push_navigate(to: ~p"/contacts/#{existing.id}")}

        {:error, changeset} ->
          {:noreply, assign(socket, form: to_form(changeset))}
      end
    end
  end

  def handle_event("upload_csv", params, socket) do
    email =
      case current_user(socket.assigns) do
        %{email: user_email} -> user_email
        _ -> params |> Map.get("submitter_email", "") |> String.trim()
      end

    private = Map.get(params, "private") == "true"

    if email == "" do
      {:noreply, put_flash(socket, :error, "Email is required for CSV upload")}
    else
      uploaded_contents =
        consume_uploaded_entries(socket, :csv, fn %{path: path}, _entry ->
          {:ok, File.read!(path)}
        end)

      case uploaded_contents do
        [content] ->
          handle_csv_preview(content, email, private, socket)

        [] ->
          {:noreply, put_flash(socket, :error, "Please select a CSV file")}
      end
    end
  end

  def handle_event("cancel_csv_upload", %{"ref" => ref}, socket) do
    {:noreply, cancel_upload(socket, :csv, ref)}
  end

  def handle_event("upload_adif", params, socket) do
    email =
      case current_user(socket.assigns) do
        %{email: user_email} -> user_email
        _ -> params |> Map.get("submitter_email", "") |> String.trim()
      end

    private = Map.get(params, "private") == "true"

    if email == "" do
      {:noreply, put_flash(socket, :error, "Email is required for ADIF upload")}
    else
      uploaded_contents =
        consume_uploaded_entries(socket, :adif, fn %{path: path}, _entry ->
          {:ok, File.read!(path)}
        end)

      case uploaded_contents do
        [content] ->
          handle_adif_preview(content, email, private, socket)

        [] ->
          {:noreply, put_flash(socket, :error, "Please select an ADIF file")}
      end
    end
  end

  def handle_event("cancel_adif_upload", %{"ref" => ref}, socket) do
    {:noreply, cancel_upload(socket, :adif, ref)}
  end

  def handle_event("confirm_csv", _params, socket) do
    case socket.assigns.csv_preview do
      %{valid: valid_rows, refinements: refinements} = preview
      when valid_rows != [] or refinements != [] ->
        private = socket.assigns[:csv_private] || false
        {:ok, run_id} = CsvImport.enqueue(preview, socket.assigns.csv_email, private: private)

        {:noreply,
         socket
         |> assign(csv_preview: nil, csv_private: false)
         |> push_navigate(to: ~p"/imports/#{run_id}")}

      _ ->
        {:noreply, put_flash(socket, :error, "Nothing to import.")}
    end
  end

  def handle_event("cancel_csv", _params, socket) do
    {:noreply, assign(socket, csv_preview: nil, csv_private: false)}
  end

  defp handle_adif_preview(content, email, private, socket) do
    case AdifImport.preview(content, email) do
      {:ok, preview} ->
        {:noreply, assign(socket, csv_preview: preview, csv_email: email, csv_private: private)}

      {:error, :no_records} ->
        {:noreply, put_flash(socket, :error, "ADIF file contains no records")}
    end
  end

  defp handle_csv_preview(content, email, private, socket) do
    case CsvImport.preview(content, email) do
      {:ok, preview} ->
        {:noreply, assign(socket, csv_preview: preview, csv_email: email, csv_private: private)}

      {:error, :empty_csv} ->
        {:noreply, put_flash(socket, :error, "CSV file is empty")}

      {:error, :no_data_rows} ->
        {:noreply, put_flash(socket, :error, "CSV file has no data rows")}
    end
  end

  defp recently_submitted?(socket) do
    case socket.assigns.submitted_at do
      nil -> false
      ts -> System.monotonic_time(:millisecond) - ts < @submission_cooldown_ms
    end
  end

  defp merge_user_params(params, socket) do
    case current_user(socket.assigns) do
      %{id: user_id, email: user_email} ->
        {Map.put(params, "submitter_email", user_email), user_id}

      _ ->
        {params, nil}
    end
  end

  defp error_to_string(:too_large), do: "File is too large"
  defp error_to_string(:too_many_files), do: "Only one file allowed"
  defp error_to_string(:not_accepted), do: "File type not accepted"
  defp error_to_string(other), do: to_string(other)

  @impl true
  def render(assigns) do
    ~H"""
    <Layouts.app flash={@flash} current_scope={@current_scope}>
      <.header>
        Submit Contact
        <:subtitle>Help us build a better propagation model</:subtitle>
      </.header>

      <div class="alert alert-info mb-6 text-sm leading-relaxed">
        <div>
          <p class="font-semibold mb-1">Every contact matters</p>
          <p>
            Our propagation model improves with real-world data. Each verified contact
            you submit is matched against atmospheric conditions at the time of your contact,
            helping us calibrate predictions for all amateur bands from 50 MHz and up. The
            more contacts we have, the better our forecasts get for everyone.
          </p>
        </div>
      </div>

      <div class="alert mb-6 text-sm leading-relaxed">
        <p>
          Your contact will be available on the
          <.link navigate="/contacts" class="link link-primary">contacts page</.link>
          shortly after submission, with terrain analysis, atmospheric data, and propagation scoring
          added automatically.
        </p>
      </div>

      <div role="tablist" class="tabs tabs-box mb-6">
        <button
          role="tab"
          class={["tab", @active_tab == :single && "tab-active"]}
          phx-click="switch_tab"
          phx-value-tab="single"
        >
          Single Contact
        </button>
        <button
          role="tab"
          class={["tab", @active_tab == :csv && "tab-active"]}
          phx-click="switch_tab"
          phx-value-tab="csv"
        >
          Upload CSV
        </button>
        <button
          role="tab"
          class={["tab", @active_tab == :adif && "tab-active"]}
          phx-click="switch_tab"
          phx-value-tab="adif"
        >
          Upload ADIF
        </button>
      </div>

      <%= case @active_tab do %>
        <% :single -> %>
          <.single_contact_form
            form={@form}
            band_options={@band_options}
            mode_options={@mode_options}
            current_user={current_user(assigns)}
          />
        <% :csv -> %>
          <%= if @csv_preview do %>
            <.csv_preview preview={@csv_preview} />
          <% else %>
            <.csv_upload_form uploads={@uploads} current_user={current_user(assigns)} />
          <% end %>
        <% :adif -> %>
          <%= if @csv_preview do %>
            <.csv_preview preview={@csv_preview} />
          <% else %>
            <.adif_upload_form uploads={@uploads} current_user={current_user(assigns)} />
          <% end %>
      <% end %>
    </Layouts.app>
    """
  end

  # In-template variant — takes an assigns map directly (not a socket).
  defp current_user(%{current_scope: %{user: %{} = user}}), do: user
  defp current_user(_), do: nil

  defp single_contact_form(assigns) do
    ~H"""
    <.form for={@form} id="contact-form" phx-change="validate" phx-submit="save" class="space-y-4">
      <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
        <.input field={@form[:station1]} type="text" label="Station 1" placeholder="W5XD" required />
        <.input
          field={@form[:grid1]}
          type="text"
          label="Grid 1"
          placeholder="EM12kp"
          phx-debounce="blur"
          required
        />
        <.input
          field={@form[:height1_ft]}
          type="number"
          label="Height 1 (ft AGL)"
          min="0"
          max="1000"
          placeholder="Optional"
        />
        <.input field={@form[:station2]} type="text" label="Station 2" placeholder="K5TR" required />
        <.input
          field={@form[:grid2]}
          type="text"
          label="Grid 2"
          placeholder="EM00cd"
          phx-debounce="blur"
          required
        />
        <.input
          field={@form[:height2_ft]}
          type="number"
          label="Height 2 (ft AGL)"
          min="0"
          max="1000"
          placeholder="Optional"
        />
      </div>

      <p class="text-sm text-base-content/60 -mt-2">
        Be as specific as possible with grid squares (8 characters preferred, e.g. EM12kp37).
        Antenna heights are optional but improve terrain-clearance analysis.
      </p>

      <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
        <.input
          field={@form[:band]}
          type="select"
          label="Band"
          prompt="Select band"
          options={@band_options}
          required
        />
        <.input
          field={@form[:mode]}
          type="select"
          label="Mode"
          prompt="Optional"
          options={@mode_options}
        />
        <.input
          field={@form[:qso_timestamp]}
          type="text"
          label="Timestamp (UTC, 24h)"
          placeholder="YYYY-MM-DD HH:MM"
          pattern="\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2})?Z?"
          required
        />
      </div>

      <%= if @current_user do %>
        <input
          type="hidden"
          name="contact[submitter_email]"
          value={@current_user.email}
        />
      <% else %>
        <.input
          field={@form[:submitter_email]}
          type="email"
          label="Your Email"
          placeholder="you@example.com"
          required
        />
      <% end %>

      <div class="mt-4">
        <.input
          field={@form[:private]}
          type="checkbox"
          label="Private — only visible to me and administrators"
        />
      </div>

      <div class="mt-6">
        <.button phx-disable-with="Submitting..." class="btn btn-primary btn-lg w-full sm:w-auto">
          <.icon name="hero-arrow-up-tray" class="w-5 h-5" /> Submit Contact
        </.button>
      </div>
    </.form>
    """
  end

  defp csv_upload_form(assigns) do
    ~H"""
    <div class="space-y-6">
      <div class="alert text-sm">
        <div>
          <p class="mb-2">Upload a CSV file with multiple contacts. Columns:</p>
          <code class="text-xs">station1, station2, grid1, grid2, band, mode, qso_timestamp</code>
          <ul class="list-disc list-outside pl-5 mt-2 space-y-1 text-base-content/60">
            <li>
              Timestamps in most formats accepted (e.g. <code>2024-06-15T14:30:00Z</code>, <code>6/15/2024 2:30 PM</code>, <code>2024-06-15 14:30</code>). All times assumed UTC.
            </li>
            <li>
              Grid squares should be as detailed as possible (8 characters preferred, e.g. EM12kp37)
            </li>
            <li>Band in MHz (e.g. 10000, 24000)</li>
            <li>
              Mode is <strong>optional</strong> — omit the <code>mode</code> column entirely
              (6 columns total) or leave its value blank.
            </li>
          </ul>
          <p class="mt-2">
            <a href="/downloads/sample_contacts.csv" download class="link link-primary">
              <.icon name="hero-arrow-down-tray" class="w-4 h-4" /> Download sample CSV
            </a>
          </p>
        </div>
      </div>

      <form id="csv-upload-form" phx-submit="upload_csv" phx-change="validate_csv" class="space-y-4">
        <div class="fieldset mb-2">
          <label>
            <span class="label mb-1">CSV File</span>
            <.live_file_input upload={@uploads.csv} class="file-input file-input-bordered w-full" />
          </label>
        </div>

        <div :for={entry <- @uploads.csv.entries} class="space-y-1">
          <div class="flex items-center justify-between text-xs tabular-nums">
            <span class="truncate pr-2">{entry.client_name}</span>
            <span class="flex items-center gap-2 shrink-0">
              <span :if={uploading?(entry)}>{entry.progress}%</span>
              <button
                type="button"
                class="btn btn-ghost btn-xs"
                phx-click="cancel_csv_upload"
                phx-value-ref={entry.ref}
                aria-label="Cancel upload"
              >
                <.icon name="hero-x-mark" class="w-4 h-4" />
              </button>
            </span>
          </div>
          <progress
            :if={uploading?(entry)}
            class="progress progress-primary w-full"
            value={entry.progress}
            max="100"
          >
          </progress>
          <div :for={err <- upload_errors(@uploads.csv, entry)} class="text-xs text-error">
            {error_to_string(err)}
          </div>
        </div>

        <div :for={err <- upload_errors(@uploads.csv)} class="text-xs text-error">
          {error_to_string(err)}
        </div>

        <%= if @current_user do %>
          <input type="hidden" name="submitter_email" value={@current_user.email} />
        <% else %>
          <div class="fieldset mb-2">
            <label>
              <span class="label mb-1">Your Email</span>
              <input
                type="email"
                name="submitter_email"
                class="w-full input"
                placeholder="you@example.com"
                value=""
              />
            </label>
          </div>
        <% end %>

        <label class="flex items-center gap-2 cursor-pointer mt-2">
          <input type="checkbox" name="private" value="true" class="checkbox checkbox-sm" />
          <span class="text-sm">Private — only visible to me and administrators</span>
        </label>

        <div class="mt-6">
          <button type="submit" class="btn btn-primary btn-lg w-full sm:w-auto">
            <.icon name="hero-arrow-up-tray" class="w-5 h-5" /> Upload Contacts
          </button>
        </div>
      </form>
    </div>
    """
  end

  defp adif_upload_form(assigns) do
    assigns = assign(assigns, :mode_mapping, AdifImport.mode_mapping_reference())

    ~H"""
    <div class="space-y-6">
      <div class="alert text-sm">
        <div>
          <p class="mb-2">
            Upload an ADIF (.adi / .adif) file exported from your logging program.
          </p>
          <ul class="list-disc list-outside pl-5 mt-2 space-y-1 text-base-content/60">
            <li>
              Required fields: <code>CALL</code>, <code>STATION_CALLSIGN</code>
              (or <code>OPERATOR</code>), <code>GRIDSQUARE</code>, <code>MY_GRIDSQUARE</code>, <code>QSO_DATE</code>, <code>TIME_ON</code>,
              and <code>FREQ</code>
              or <code>BAND</code>
            </li>
            <li>Contacts on amateur bands from 50 MHz and up will be imported</li>
            <li>HF contacts (below 50 MHz) are silently skipped</li>
            <li>Frequencies are fuzzy-matched to the nearest amateur band</li>
          </ul>
        </div>
      </div>

      <div class="alert text-sm">
        <div class="w-full">
          <p class="font-semibold mb-2">Mode handling</p>
          <p class="mb-2 text-base-content/70">
            ADIF modes are normalized to the six modes we track. SUBMODE takes
            precedence when present (e.g. <code>MODE=MFSK, SUBMODE=FT8</code> imports as FT8).
          </p>
          <table class="table table-xs mt-2">
            <thead>
              <tr>
                <th>ADIF value</th>
                <th>Stored as</th>
              </tr>
            </thead>
            <tbody>
              <tr :for={{adif, mapped} <- @mode_mapping}>
                <td><code>{adif}</code></td>
                <td><code>{mapped}</code></td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>

      <form
        id="adif-upload-form"
        phx-submit="upload_adif"
        phx-change="validate_adif"
        class="space-y-4"
      >
        <div class="fieldset mb-2">
          <label>
            <span class="label mb-1">ADIF File</span>
            <.live_file_input upload={@uploads.adif} class="file-input file-input-bordered w-full" />
          </label>
        </div>

        <div :for={entry <- @uploads.adif.entries} class="space-y-1">
          <div class="flex items-center justify-between text-xs tabular-nums">
            <span class="truncate pr-2">{entry.client_name}</span>
            <span class="flex items-center gap-2 shrink-0">
              <span :if={uploading?(entry)}>{entry.progress}%</span>
              <button
                type="button"
                class="btn btn-ghost btn-xs"
                phx-click="cancel_adif_upload"
                phx-value-ref={entry.ref}
                aria-label="Cancel upload"
              >
                <.icon name="hero-x-mark" class="w-4 h-4" />
              </button>
            </span>
          </div>
          <progress
            :if={uploading?(entry)}
            class="progress progress-primary w-full"
            value={entry.progress}
            max="100"
          >
          </progress>
          <div :for={err <- upload_errors(@uploads.adif, entry)} class="text-xs text-error">
            {error_to_string(err)}
          </div>
        </div>

        <div :for={err <- upload_errors(@uploads.adif)} class="text-xs text-error">
          {error_to_string(err)}
        </div>

        <%= if @current_user do %>
          <input type="hidden" name="submitter_email" value={@current_user.email} />
        <% else %>
          <div class="fieldset mb-2">
            <label>
              <span class="label mb-1">Your Email</span>
              <input
                type="email"
                name="submitter_email"
                class="w-full input"
                placeholder="you@example.com"
                value=""
              />
            </label>
          </div>
        <% end %>

        <label class="flex items-center gap-2 cursor-pointer mt-2">
          <input type="checkbox" name="private" value="true" class="checkbox checkbox-sm" />
          <span class="text-sm">Private — only visible to me and administrators</span>
        </label>

        <div class="mt-6">
          <button type="submit" class="btn btn-primary btn-lg w-full sm:w-auto">
            <.icon name="hero-arrow-up-tray" class="w-5 h-5" /> Upload Contacts
          </button>
        </div>
      </form>
    </div>
    """
  end

  defp csv_preview(assigns) do
    assigns =
      assign(assigns,
        valid_count: length(assigns.preview.valid),
        invalid_count: length(assigns.preview.invalid),
        duplicate_count: length(assigns.preview.duplicates),
        refinement_count: length(assigns.preview.refinements || []),
        valid_sample: Enum.take(assigns.preview.valid, 20),
        refinement_sample: Enum.take(assigns.preview.refinements || [], 50)
      )

    ~H"""
    <div class="space-y-6">
      <div>
        <h2 class="text-xl font-bold mb-2">Review before submitting</h2>
        <p class="text-sm opacity-70">
          Processed {@preview.total_rows} {if @preview.total_rows == 1, do: "row", else: "rows"}.
          Nothing has been inserted yet — confirm below to import.
        </p>
      </div>

      <div class="grid grid-cols-2 lg:grid-cols-4 gap-3">
        <.summary_card
          tone="success"
          label="Valid"
          value={@valid_count}
          hint="Will be inserted"
        />
        <.summary_card
          tone="info"
          label="Refinements"
          value={@refinement_count}
          hint="Will update existing contacts"
        />
        <.summary_card
          tone="warning"
          label="Duplicates"
          value={@duplicate_count}
          hint="Match existing or earlier rows"
        />
        <.summary_card
          tone="error"
          label="Invalid"
          value={@invalid_count}
          hint="Skipped — see errors"
        />
      </div>

      <div :if={@invalid_count > 0}>
        <h3 class="font-semibold mb-2">Invalid rows ({@invalid_count})</h3>
        <div class="overflow-x-auto rounded-box border border-base-300">
          <table class="table table-sm">
            <thead>
              <tr>
                <th class="w-24">Row</th>
                <th>Errors</th>
              </tr>
            </thead>
            <tbody>
              <tr :for={row <- Enum.take(@preview.invalid, 100)}>
                <td class="font-mono">Row {row.row_num}</td>
                <td>
                  <div :for={msg <- row.messages}>{msg}</div>
                </td>
              </tr>
            </tbody>
          </table>
        </div>
        <p :if={@invalid_count > 100} class="text-xs opacity-60 mt-1">
          Showing first 100 of {@invalid_count} invalid rows.
        </p>
      </div>

      <div :if={@refinement_count > 0}>
        <h3 class="font-semibold mb-2">Refinements ({@refinement_count})</h3>
        <p class="text-xs opacity-60 mb-2">
          These rows match an existing contact but add more precise data.
          Confirming will update the existing contact in place.
        </p>
        <div class="overflow-x-auto rounded-box border border-base-300">
          <table class="table table-sm">
            <thead>
              <tr>
                <th class="w-20">Row</th>
                <th>Station 1</th>
                <th>Station 2</th>
                <th>Band</th>
                <th>Changes</th>
              </tr>
            </thead>
            <tbody>
              <tr :for={row <- @refinement_sample}>
                <td class="font-mono">Row {row.row_num}</td>
                <td>{row.attrs["station1"]} <span class="opacity-60">{row.attrs["grid1"]}</span></td>
                <td>{row.attrs["station2"]} <span class="opacity-60">{row.attrs["grid2"]}</span></td>
                <td class="tabular-nums">{row.attrs["band"]}</td>
                <td class="text-xs">
                  <div :for={{field, value} <- Enum.sort(Map.to_list(row.changes))}>
                    <span class="opacity-60">{field}:</span>
                    <code class="text-xs">{value}</code>
                  </div>
                </td>
              </tr>
            </tbody>
          </table>
        </div>
        <p :if={@refinement_count > length(@refinement_sample)} class="text-xs opacity-60 mt-1">
          Showing first {length(@refinement_sample)} of {@refinement_count} refinements.
        </p>
      </div>

      <div :if={@duplicate_count > 0}>
        <h3 class="font-semibold mb-2">Duplicates ({@duplicate_count})</h3>
        <p class="text-xs opacity-60 mb-2">
          Same two callsigns at the same grids on the same band within an hour.
        </p>
        <div class="overflow-x-auto rounded-box border border-base-300">
          <table class="table table-sm">
            <thead>
              <tr>
                <th class="w-20">Row</th>
                <th>Station 1</th>
                <th>Station 2</th>
                <th>Band</th>
                <th>Timestamp</th>
                <th>Source</th>
              </tr>
            </thead>
            <tbody>
              <tr :for={row <- Enum.take(@preview.duplicates, 100)}>
                <td class="font-mono">Row {row.row_num}</td>
                <td>{row.attrs["station1"]} <span class="opacity-60">{row.attrs["grid1"]}</span></td>
                <td>{row.attrs["station2"]} <span class="opacity-60">{row.attrs["grid2"]}</span></td>
                <td class="tabular-nums">{row.attrs["band"]}</td>
                <td class="font-mono text-xs">
                  {Calendar.strftime(row.timestamp, "%Y-%m-%d %H:%M UTC")}
                </td>
                <td class="text-xs opacity-70">{duplicate_source(row.reason)}</td>
              </tr>
            </tbody>
          </table>
        </div>
        <p :if={@duplicate_count > 100} class="text-xs opacity-60 mt-1">
          Showing first 100 of {@duplicate_count} duplicates.
        </p>
      </div>

      <div :if={@valid_count > 0}>
        <h3 class="font-semibold mb-2">Valid rows ready to import ({@valid_count})</h3>
        <div class="overflow-x-auto rounded-box border border-base-300">
          <table class="table table-sm">
            <thead>
              <tr>
                <th class="w-20">Row</th>
                <th>Station 1</th>
                <th>Station 2</th>
                <th>Band</th>
                <th>Mode</th>
                <th>Timestamp</th>
              </tr>
            </thead>
            <tbody>
              <tr :for={row <- @valid_sample}>
                <td class="font-mono">Row {row.row_num}</td>
                <td>{row.attrs["station1"]} <span class="opacity-60">{row.attrs["grid1"]}</span></td>
                <td>{row.attrs["station2"]} <span class="opacity-60">{row.attrs["grid2"]}</span></td>
                <td class="tabular-nums">{row.attrs["band"]}</td>
                <td>{row.attrs["mode"]}</td>
                <td class="font-mono text-xs">
                  {Calendar.strftime(row.timestamp, "%Y-%m-%d %H:%M UTC")}
                </td>
              </tr>
            </tbody>
          </table>
        </div>
        <p :if={@valid_count > length(@valid_sample)} class="text-xs opacity-60 mt-1">
          Showing first {length(@valid_sample)} of {@valid_count} valid rows.
        </p>
      </div>

      <div class="flex flex-wrap gap-2 pt-4 border-t border-base-300">
        <button
          :if={@valid_count > 0 or @refinement_count > 0}
          type="button"
          class="btn btn-primary btn-lg"
          phx-click="confirm_csv"
          phx-disable-with="Importing..."
          data-confirm={confirm_prompt(@valid_count, @refinement_count)}
        >
          <.icon name="hero-check" class="w-5 h-5" />
          {confirm_button_label(@valid_count, @refinement_count)}
        </button>
        <button type="button" class="btn btn-ghost" phx-click="cancel_csv">
          Cancel
        </button>
      </div>
    </div>
    """
  end

  attr :tone, :string, required: true
  attr :label, :string, required: true
  attr :value, :integer, required: true
  attr :hint, :string, required: true

  defp summary_card(assigns) do
    ~H"""
    <div class={[
      "rounded-box border p-4",
      case @tone do
        "success" -> "border-success/30 bg-success/10"
        "warning" -> "border-warning/30 bg-warning/10"
        "error" -> "border-error/30 bg-error/10"
        "info" -> "border-info/30 bg-info/10"
        _ -> "border-base-300 bg-base-200"
      end
    ]}>
      <div class="text-xs uppercase tracking-wider opacity-70">{@label}</div>
      <div class="text-3xl font-bold tabular-nums">{@value}</div>
      <div class="text-xs opacity-60">{@hint}</div>
    </div>
    """
  end

  defp duplicate_source(:existing_contact), do: "Already in database"
  defp duplicate_source(:earlier_in_upload), do: "Earlier row in this upload"
  defp duplicate_source(_), do: "Duplicate"

  defp pluralize(1, word), do: word
  defp pluralize(_, word), do: word <> "s"

  # An upload entry is actively streaming when its progress is partway
  # between 0 and 100. With `auto_upload: false` entries sit at
  # `progress: 0` after the user picks a file and only start streaming
  # when the form is submitted, so gating the progress bar and percent
  # text on the 0 < progress < 100 window keeps them hidden until the
  # upload is actually in flight.
  defp uploading?(%{progress: progress}), do: progress > 0 and progress < 100
  defp uploading?(_), do: false

  defp confirm_button_label(valid, 0) do
    "Looks good — submit #{valid} #{pluralize(valid, "contact")}"
  end

  defp confirm_button_label(0, refined) do
    "Looks good — refine #{refined} existing #{pluralize(refined, "contact")}"
  end

  defp confirm_button_label(valid, refined) do
    "Looks good — submit #{valid} and refine #{refined}"
  end

  defp confirm_prompt(valid, 0), do: "Import #{valid} contacts?"
  defp confirm_prompt(0, refined), do: "Refine #{refined} existing contacts?"

  defp confirm_prompt(valid, refined) do
    "Import #{valid} contacts and refine #{refined} existing?"
  end
end
