lib/squid_sonar_web/components/core_components.ex

defmodule SquidSonarWeb.CoreComponents do
  @moduledoc false

  use Phoenix.Component

  alias SquidSonarWeb.WorkflowGraphLayout

  attr :status, :atom, required: true

  def status_badge(assigns) do
    ~H"""
    <span class={["squid-sonar-badge", "squid-sonar-badge-#{@status}"]}>
      {@status}
    </span>
    """
  end

  attr :status, :atom, required: true
  attr :count, :integer, required: true
  attr :active, :boolean, default: false

  def status_nav_item(assigns) do
    ~H"""
    <label class={["squid-sonar-nav-item", @active && "is-active"]}>
      <input type="radio" name="filters[status]" value={@status} checked={@active} />
      <span class="squid-sonar-nav-label">
        <span>{human_status(@status)}</span>
      </span>
      <strong>{@count}</strong>
    </label>
    """
  end

  attr :theme, :atom, required: true

  def theme_switcher(assigns) do
    ~H"""
    <div class="squid-sonar-theme-switcher" aria-label="Theme">
      <.theme_button theme={@theme} value={:system} label="Use system theme">
        <rect x="3" y="4" width="18" height="12" rx="2" />
        <path d="M8 20h8" />
        <path d="M12 16v4" />
      </.theme_button>
      <.theme_button theme={@theme} value={:light} label="Use light theme">
        <path d="M12 3v2" />
        <path d="M12 19v2" />
        <path d="m5.6 5.6 1.4 1.4" />
        <path d="m17 17 1.4 1.4" />
        <path d="M3 12h2" />
        <path d="M19 12h2" />
        <path d="m5.6 18.4 1.4-1.4" />
        <path d="m17 7 1.4-1.4" />
        <circle cx="12" cy="12" r="4" />
      </.theme_button>
      <.theme_button theme={@theme} value={:dark} label="Use dark theme">
        <path d="M20 14.4A7.8 7.8 0 0 1 9.6 4a8 8 0 1 0 10.4 10.4Z" />
      </.theme_button>
    </div>
    """
  end

  def refresh_button(assigns) do
    ~H"""
    <button
      class="squid-sonar-icon-button squid-sonar-refresh"
      type="button"
      phx-click="refresh"
      title="Refresh runs"
      aria-label="Refresh runs"
    >
      <svg
        aria-hidden="true"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="1.8"
        stroke-linecap="round"
        stroke-linejoin="round"
      >
        <path d="M20 11a8.1 8.1 0 0 0-15.5-2" />
        <path d="M4 5v4h4" />
        <path d="M4 13a8.1 8.1 0 0 0 15.5 2" />
        <path d="M20 19v-4h-4" />
      </svg>
    </button>
    """
  end

  attr :theme, :atom, required: true
  attr :value, :atom, required: true
  attr :label, :string, required: true
  slot :inner_block, required: true

  defp theme_button(assigns) do
    ~H"""
    <button
      class={["squid-sonar-icon-button", @theme == @value && "is-active"]}
      type="button"
      phx-click="set_theme"
      phx-value-theme={@value}
      data-squid-sonar-theme={@value}
      title={@label}
      aria-label={@label}
    >
      <svg
        aria-hidden="true"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="1.8"
        stroke-linecap="round"
        stroke-linejoin="round"
      >
        {render_slot(@inner_block)}
      </svg>
    </button>
    """
  end

  attr :error, :any, required: true

  def dashboard_error(assigns) do
    ~H"""
    <section class="squid-sonar-alert" role="alert">
      <h2>Unable to load runs</h2>
      <p>Check the host application's Squid Mesh configuration and logs.</p>
    </section>
    """
  end

  def empty_runs(assigns) do
    ~H"""
    <div class="squid-sonar-empty">
      <h3>No runs found</h3>
    </div>
    """
  end

  attr :dashboard, :map, required: true
  attr :prefix, :string, default: ""

  def runs_panel(assigns) do
    ~H"""
    <section class="squid-sonar-panel">
      <div class="squid-sonar-panel-heading">
        <div class="squid-sonar-panel-actions">
          <label class="squid-sonar-search">
            <span>Search</span>
            <input
              type="search"
              name="filters[query]"
              value={@dashboard.filters.query}
              placeholder="Workflow, trigger, step, run ID"
              phx-debounce="250"
            />
          </label>
        </div>

        <div class="squid-sonar-panel-tools">
          <.refresh_button />
        </div>
      </div>

      <%= if @dashboard.runs == [] do %>
        <.empty_runs />
      <% else %>
        <.runs_table runs={@dashboard.runs} prefix={@prefix} />
        <.pagination
          page={@dashboard.page}
          total_pages={@dashboard.total_pages}
          filtered_count={@dashboard.filtered_count}
          loaded_at={@dashboard.loaded_at}
          page_size={@dashboard.page_size}
          page_sizes={@dashboard.page_sizes}
        />
      <% end %>
    </section>
    """
  end

  attr :runs, :list, required: true
  attr :prefix, :string, default: ""

  def runs_table(assigns) do
    ~H"""
    <div class="squid-sonar-table-wrap">
      <table class="squid-sonar-table">
        <thead>
          <tr>
            <th>Workflow</th>
            <th>Trigger</th>
            <th>Status</th>
            <th>Current step</th>
            <th>Updated</th>
          </tr>
        </thead>
        <tbody>
          <tr :for={run <- @runs}>
            <td>
              <div class="squid-sonar-run-title">
                <.link navigate={run_path(@prefix, run.id)} class="squid-sonar-run-link">
                  <span class="squid-sonar-primary">{format_workflow(run.workflow)}</span>
                  <span class="squid-sonar-secondary">{run.id}</span>
                </.link>
              </div>
            </td>
            <td>{format_value(run.trigger)}</td>
            <td><.status_badge status={run.status} /></td>
            <td>{format_step(run.current_step)}</td>
            <td><.timestamp value={run.updated_at} /></td>
          </tr>
        </tbody>
      </table>
    </div>
    """
  end

  attr :page, :integer, required: true
  attr :total_pages, :integer, required: true
  attr :filtered_count, :integer, required: true
  attr :loaded_at, :any, required: true
  attr :page_size, :integer, required: true
  attr :page_sizes, :list, required: true

  def pagination(assigns) do
    assigns =
      assigns
      |> assign(:previous_page, max(assigns.page - 1, 1))
      |> assign(:next_page, min(assigns.page + 1, assigns.total_pages))

    ~H"""
    <nav class="squid-sonar-pagination" aria-label="Runs pagination">
      <span class="squid-sonar-pagination-summary">
        <strong>Recent runs</strong>
        <span>{@filtered_count} matching</span>
        <span>Updated <.timestamp value={@loaded_at} /></span>
      </span>
      <div class="squid-sonar-pagination-controls">
        <label class="squid-sonar-page-size">
          <span>Page size</span>
          <select name="page_size">
            <option
              :for={page_size <- @page_sizes}
              value={page_size}
              selected={page_size == @page_size}
            >
              {page_size}
            </option>
          </select>
        </label>
        <button
          type="button"
          phx-click="paginate"
          phx-value-page={@previous_page}
          disabled={@page <= 1}
        >
          Previous
        </button>
        <strong>{@page} / {@total_pages}</strong>
        <button
          type="button"
          phx-click="paginate"
          phx-value-page={@next_page}
          disabled={@page >= @total_pages}
        >
          Next
        </button>
      </div>
    </nav>
    """
  end

  attr :detail, :map, required: true
  attr :prefix, :string, default: ""

  def run_detail(assigns) do
    ~H"""
    <section class="squid-sonar-detail">
      <header class="squid-sonar-detail-header">
        <div>
          <.link navigate={@prefix <> "/"} class="squid-sonar-back-link">Back to runs</.link>
          <h2>{format_workflow(@detail.summary.workflow)}</h2>
          <p>{@detail.summary.id}</p>
        </div>
        <.status_badge status={@detail.summary.status} />
      </header>

      <div class="squid-sonar-detail-grid">
        <.detail_item label="Trigger" value={format_value(@detail.summary.trigger)} />
        <.detail_item label="Current step" value={format_step(@detail.summary.current_step)} />
        <.detail_item label="Inserted" value={format_time(@detail.summary.inserted_at)} />
        <.detail_item label="Updated" value={format_time(@detail.summary.updated_at)} />
      </div>

      <div class="squid-sonar-detail-columns">
        <section class="squid-sonar-detail-panel">
          <h3>Diagnosis</h3>
          <.detail_item label="Reason" value={explanation_reason(@detail.explanation)} />
          <.detail_item label="Suggested actions" value={next_actions(@detail.explanation)} />
          <.detail_item label="Last error" value={last_error(@detail.last_error)} variant={:code} />
        </section>

        <section class="squid-sonar-detail-panel">
          <h3>History</h3>
          <.detail_item label="Step records" value={length(@detail.step_runs)} />
          <.detail_item label="Attempts" value={length(@detail.step_attempts)} />
          <.detail_item label="Audit events" value={length(@detail.audit_events)} />
        </section>
      </div>

      <section class="squid-sonar-detail-panel">
        <h3>Workflow</h3>
        <%= if @detail.workflow_graph.nodes == [] do %>
          <p class="squid-sonar-muted-line">No workflow graph loaded.</p>
        <% else %>
          <div class="squid-sonar-workflow-graph">
            <% layout = workflow_graph_layout(@detail.workflow_graph) %>

            <div class="squid-sonar-workflow-graph-heading">
              <div>
                <strong>{format_workflow(@detail.summary.workflow)}</strong>
                <span>{format_value(@detail.summary.trigger)}</span>
              </div>
            </div>

            <div
              class="squid-sonar-workflow-stage"
              style={workflow_stage_style(layout)}
            >
              <span
                :for={segment <- layout.segments}
                class={[
                  "squid-sonar-workflow-edge-segment",
                  "squid-sonar-workflow-edge-segment-#{segment.orientation}"
                ]}
                style={workflow_segment_style(segment)}
              />
              <span
                :for={port <- layout.ports}
                class="squid-sonar-workflow-port"
                style={workflow_port_style(port)}
              />

              <article
                :for={item <- layout.nodes}
                class={[
                  "squid-sonar-workflow-node",
                  "squid-sonar-workflow-node-#{item.node.status}",
                  item.node.current? && "squid-sonar-workflow-node-current",
                  item.node.terminal? && "squid-sonar-workflow-node-terminal"
                ]}
                style={workflow_node_style(item)}
              >
                <div class="squid-sonar-workflow-node-main">
                  <span class={[
                    "squid-sonar-workflow-status-icon",
                    "squid-sonar-workflow-status-icon-#{item.node.status}"
                  ]} />
                  <strong>{item.node.label}</strong>
                </div>
                <span class="squid-sonar-workflow-node-status">
                  {format_graph_status(item.node.status)}
                </span>
              </article>
            </div>
          </div>
        <% end %>
      </section>
    </section>
    """
  end

  attr :label, :string, required: true
  attr :value, :any, required: true
  attr :variant, :atom, default: :strong

  def detail_item(assigns) do
    ~H"""
    <div class="squid-sonar-detail-item">
      <span>{@label}</span>
      <%= if @variant == :code do %>
        <code>{@value}</code>
      <% else %>
        <strong>{@value}</strong>
      <% end %>
    </div>
    """
  end

  attr :value, :any, required: true

  def timestamp(assigns) do
    ~H"""
    <time>{@value |> format_time()}</time>
    """
  end

  defp human_status(status) do
    status
    |> to_string()
    |> String.replace("_", " ")
    |> String.capitalize()
  end

  defp format_workflow(nil), do: "Unknown workflow"

  defp format_workflow(workflow) when is_atom(workflow) do
    workflow
    |> Atom.to_string()
    |> String.replace_prefix("Elixir.", "")
  end

  defp format_workflow(workflow), do: to_string(workflow)

  defp format_step(nil), do: "None"
  defp format_step(step), do: format_value(step)

  defp format_value(nil), do: "None"
  defp format_value(value) when is_atom(value), do: Atom.to_string(value)
  defp format_value(value), do: to_string(value)

  defp format_time(nil), do: "Unknown"

  defp format_time(%DateTime{} = datetime) do
    datetime
    |> DateTime.truncate(:second)
    |> DateTime.to_iso8601()
  end

  defp format_time(%NaiveDateTime{} = datetime) do
    datetime
    |> NaiveDateTime.truncate(:second)
    |> NaiveDateTime.to_iso8601()
  end

  defp format_time(value), do: to_string(value)

  defp run_path(prefix, run_id), do: "#{prefix}/runs/#{run_id}"

  defp explanation_reason(nil), do: "Unknown"
  defp explanation_reason(%{reason: reason}), do: format_value(reason)
  defp explanation_reason(_explanation), do: "Unknown"

  defp next_actions(nil), do: "None"

  defp next_actions(%{next_actions: actions}) do
    case List.wrap(actions) do
      [] -> "None"
      actions -> Enum.map_join(actions, ", ", &format_value/1)
    end
  end

  defp next_actions(_explanation), do: "None"

  defp last_error(nil), do: "None"

  defp last_error(error) when is_map(error) do
    error
    |> Map.take([:code, :message, "code", "message"])
    |> case do
      empty when empty == %{} -> "Present"
      safe_error -> inspect(safe_error)
    end
  end

  defp last_error(_error), do: "Present"

  defp workflow_graph_layout(graph), do: WorkflowGraphLayout.build(graph)

  defp workflow_stage_style(%{width: width, height: height}) do
    "width: #{round(width)}px; height: #{round(height)}px;"
  end

  defp workflow_node_style(%{x: x, y: y, width: width, height: height}) do
    "left: #{round(x)}px; top: #{round(y)}px; width: #{round(width)}px; min-height: #{round(height)}px;"
  end

  defp workflow_segment_style(%{x: x, y: y, width: width, height: height}) do
    "left: #{round(x)}px; top: #{round(y)}px; width: #{round(width)}px; height: #{round(height)}px;"
  end

  defp workflow_port_style(%{x: x, y: y}) do
    "left: #{round(x)}px; top: #{round(y)}px;"
  end

  defp format_graph_status(:completed), do: "done"
  defp format_graph_status(:failed), do: "failed"
  defp format_graph_status(:retrying), do: "retrying"
  defp format_graph_status(:running), do: "running"
  defp format_graph_status(:paused), do: "paused"
  defp format_graph_status(:cancelled), do: "cancelled"
  defp format_graph_status(:waiting), do: "waiting"
  defp format_graph_status(:pending), do: "pending"
  defp format_graph_status(status), do: format_value(status)
end