<!-- #region Dashboard.DataGrid [C:3] [TYPE Component] [SEMANTICS dashboard,grid,selection,sort,filter,pagination] -->
<!-- @ingroup Dashboard -->
<!-- @BRIEF Configurable dashboard data grid with optional selection, sorting, filtering, and pagination. -->
<!-- @LAYER UI -->
<!-- @RATIONALE Replaces DashboardGrid — consolidation of 3 dashboard grids (migration, git, dashboards) into one configurable component. Reduces ~1080 lines of duplicated sort/filter/paginate/select logic to a single component with opt-in features. -->
<!-- @RELATION DEPENDS_ON -> [$lib/ui/Input] -->
<!-- @RELATION DEPENDS_ON -> [$lib/ui/Button] -->
<!-- @RELATION USED_BY -> [frontend/src/routes/migration/+page.svelte] -->
<!-- @UX_STATE Loading  -> Animated skeleton rows -->
<!-- @UX_STATE Empty    -> Centered dashed-border message with muted text -->
<!-- @UX_STATE Table    -> Data rows with optional selection, sort headers, filter, pagination -->
<!-- @UX_STATE Selected -> Checkboxes checked + optional children() snippet for bulk actions -->
<!-- @UX_STATE Filtered -> Text filter active, pagination recalculated, empty state if no matches -->
<!-- @UX_FEEDBACK Sort indicator (↑/↓/⇅) on column headers -->
<!-- @UX_FEEDBACK Select-all checkbox with indeterminate state -->
<!-- @UX_FEEDBACK Pagination "Showing X-Y of Z" with prev/next buttons -->
<!-- @UX_FEEDBACK Loading skeleton with animated pulse -->
<!-- @UX_RECOVERY Clear filter text to restore full results, click sort header to change sort, navigate pagination prev/next -->
<!-- @UX_RECOVERY Refresh data source to clear stale selection or filter state -->
<!-- @INVARIANT Features are opt-in via boolean props (selectable, sortable, filterable, paginated) -->
<!-- @INVARIANT State via $bindable() — parent MAY bind props for external state ownership -->
<!-- @INVARIANT Filter is case-insensitive across all visible columns (using render() if defined, else raw value) -->
<!-- @INVARIANT Sorting operates on item[key] raw value, independent of display rendering -->
<!-- @INVARIANT Page resets to 0 on filter change -->
<!-- @PRE data is array of items, columns is array of Column definitions -->
<!-- @POST Renders empty state when data is empty, loading skeleton when loading is true -->
<!-- @POST Renders checkboxes when selectable, filter input when filterable, pagination when paginated -->
<!-- @POST Selected items tracked via bind:selectedIds regardless of which page they are on -->
<!-- @SIDE_EFFECT Modifies selectedIds array via bind: when user checks/unchecks -->
<!-- @SIDE_EFFECT Modifies page/filterText/sortColumn/sortDirection via bind: when user interacts -->
<script lang="ts">
  // [SECTION: IMPORTS]
  import { t } from '$lib/i18n/index.svelte.js';
  import { Button, Input } from "$lib/ui";

  // [SECTION: TYPE EXPORTS]
  export interface Column {
    key: string;
    label: string;
    sortable?: boolean;
    width?: string;
    class?: string;
    /** Custom cell render — returns display string */
    render?: (item: any) => string;
  }

  // [SECTION: PROPS]
  let {
    // Data
    data = [] as any[],
    keyField = "id" as string,

    // Columns
    columns = [] as Column[],

    // Selection
    selectable = false,
    selectedIds = $bindable([] as any[]),

    // Sorting
    sortable = false,
    sortColumn = $bindable("" as string),
    sortDirection = $bindable("asc" as "asc" | "desc"),

    // Filtering
    filterable = false,
    filterText = $bindable("" as string),
    filterPlaceholder = "",

    // Pagination
    paginated = false,
    page = $bindable(0),
    pageSize = 20,

    // UX
    loading = false,
    loadingRows = 5,
    emptyText = "",

    // Snippet for bulk actions bar (shown above table when items are selected)
    children = undefined,
  } = $props();

  // [SECTION: HELPERS]
  function getCellText(item: any, column: Column): string {
    if (column.render) return column.render(item);
    return String(item[column.key] ?? "");
  }

  function itemMatchesFilter(item: any): boolean {
    if (!filterable || !filterText) return true;
    const q = filterText.toLowerCase();
    return columns.some(col => {
      return getCellText(item, col).toLowerCase().includes(q);
    });
  }

  // [SECTION: DERIVED]
  let filteredData = $derived(
    filterable && filterText ? data.filter(itemMatchesFilter) : data,
  );

  let sortedData = $derived.by<any[]>(() => {
    if (!sortable || !sortColumn) return filteredData;
    return [...filteredData].sort((a: any, b: any) => {
      let aVal = a[sortColumn];
      let bVal = b[sortColumn];
      if (aVal == null) aVal = "";
      if (bVal == null) bVal = "";
      if (aVal < bVal) return sortDirection === "asc" ? -1 : 1;
      if (aVal > bVal) return sortDirection === "asc" ? 1 : -1;
      return 0;
    });
  });

  let displayData = $derived(
    paginated
      ? sortedData.slice(page * pageSize, (page + 1) * pageSize)
      : sortedData,
  );

  let totalCount = $derived(sortedData.length);
  let totalPages = $derived(paginated ? Math.ceil(totalCount / pageSize) : 1);

  let allSelected = $derived(
    displayData.length > 0 && displayData.every((d: any) => selectedIds.includes(d[keyField])),
  );
  let someSelected = $derived(
    displayData.some((d: any) => selectedIds.includes(d[keyField])),
  );

  // [SECTION: HANDLERS]
  function handleSort(key: string) {
    if (sortColumn === key) {
      sortDirection = sortDirection === "asc" ? "desc" : "asc";
    } else {
      sortColumn = key;
      sortDirection = "asc";
    }
  }

  function handleSelectionChange(id: any, checked: boolean) {
    if (checked) {
      if (!selectedIds.includes(id)) {
        selectedIds = [...selectedIds, id];
      }
    } else {
      selectedIds = selectedIds.filter((sid: any) => sid !== id);
    }
  }

  function handleSelectAll(checked: boolean) {
    if (checked) {
      const currentIds = displayData.map((d: any) => d[keyField]);
      const newSet = new Set([...selectedIds, ...currentIds]);
      selectedIds = Array.from(newSet);
    } else {
      const pageIdSet = new Set(displayData.map((d: any) => d[keyField]));
      selectedIds = selectedIds.filter((sid: any) => !pageIdSet.has(sid));
    }
  }

  function goToPage(p: number) {
    if (p >= 0 && p < totalPages) page = p;
  }

  // [SECTION: REACTIVE]
  // Reset page to 0 when filter text changes
  $effect(() => {
    filterText; // track
    if (paginated) page = 0;
  });
</script>

<!-- [SECTION: TEMPLATE] -->
<div class="dashboard-data-grid">
  <!-- ── Filter Input ── -->
  {#if filterable}
    <div class="mb-4">
      <Input
        bind:value={filterText}
        placeholder={filterPlaceholder || $t.dashboard?.search || "Search..."}
      />
    </div>
  {/if}

  <!-- ── Bulk Actions Bar ── -->
  {#if selectable && selectedIds.length > 0 && children}
    <div class="mb-4">
      {@render children()}
    </div>
  {/if}

  <!-- ── Loading Skeleton ── -->
  {#if loading}
    <div class="animate-pulse rounded-lg border border-border overflow-hidden">
      <div class="bg-surface-muted h-10 border-b border-border"></div>
      {#each Array(loadingRows) as _}
        <div class="flex items-center gap-4 px-6 py-4 border-b border-border last:border-b-0">
          {#if selectable}
            <div class="w-4 h-4 bg-surface-muted rounded shrink-0"></div>
          {/if}
          {#each columns as col}
            <div class="h-4 bg-surface-muted rounded" class:flex-1={!col.width} style={col.width ? `width:${col.width}` : ""}></div>
          {/each}
        </div>
      {/each}
    </div>

  <!-- ── Empty State ── -->
  {:else if !loading && displayData.length === 0}
    <div class="flex flex-col items-center justify-center py-12 text-text-subtle rounded-lg border border-dashed border-border">
      <svg class="w-12 h-12 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
      </svg>
      <p class="text-sm">{emptyText || $t.common?.no_data || "No items"}</p>
    </div>

  <!-- ── Table ── -->
  {:else}
    <div class="overflow-x-auto rounded-lg border border-border">
      <table class="min-w-full divide-y divide-border">
        <thead class="bg-surface-muted">
          <tr>
            {#if selectable}
              <th class="px-6 py-3 text-left w-12" scope="col">
                <input
                  type="checkbox"
                  checked={allSelected}
                  indeterminate={someSelected && !allSelected}
                  onchange={(e) => handleSelectAll((e.target as HTMLInputElement).checked)}
                  class="h-4 w-4 text-primary border-border-strong rounded focus-visible:ring-primary-ring"
                  aria-label="Select all"
                />
              </th>
            {/if}
            {#each columns as col (col.key)}
              <th
                scope="col"
                class="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider
                  {col.sortable ? 'cursor-pointer hover:text-text transition-colors select-none' : ''}
                  {col.class || ''}"
                style={col.width ? `width:${col.width}` : ""}
                onclick={col.sortable ? () => handleSort(col.key) : undefined}
                aria-sort={col.sortable && sortColumn === col.key ? (sortDirection === 'asc' ? 'ascending' : 'descending') : undefined}
              >
                {col.label}
                {#if col.sortable}
                  {#if sortColumn === col.key}
                    <span class="ml-0.5">{sortDirection === "asc" ? "↑" : "↓"}</span>
                  {:else}
                    <span class="ml-0.5 text-text-subtle opacity-40">⇅</span>
                  {/if}
                {/if}
              </th>
            {/each}
          </tr>
        </thead>
        <tbody class="bg-surface-card divide-y divide-border">
          {#each displayData as item (item[keyField])}
            <tr class="hover:bg-surface-muted transition-colors">
              {#if selectable}
                <td class="px-6 py-4 whitespace-nowrap">
                  <input
                    type="checkbox"
                    checked={selectedIds.includes(item[keyField])}
                    onchange={(e) => handleSelectionChange(item[keyField], (e.target as HTMLInputElement).checked)}
                    class="h-4 w-4 text-primary border-border-strong rounded focus-visible:ring-primary-ring"
                  />
                </td>
              {/if}
              {#each columns as col (col.key)}
                <td
                  class="px-6 py-4 whitespace-nowrap text-sm {col.class || ''}"
                  style={col.width ? `width:${col.width}` : ""}
                >
                  {col.render ? col.render(item) : item[col.key]}
                </td>
              {/each}
            </tr>
          {/each}
        </tbody>
      </table>
    </div>

    <!-- ── Pagination Controls ── -->
    {#if paginated && sortedData.length > 0}
      <div class="flex items-center justify-between mt-4">
        <div class="text-sm text-text-muted">
          {($t.dashboard?.showing ?? "")
            .replace("{start}", String(page * pageSize + 1))
            .replace("{end}", String(Math.min((page + 1) * pageSize, totalCount)))
            .replace("{total}", String(totalCount))}
        </div>
        <div class="flex gap-2">
          <Button
            variant="secondary"
            size="sm"
            disabled={page === 0}
            onclick={() => goToPage(page - 1)}
          >
            {$t.dashboard?.previous || "Prev"}
          </Button>
          <Button
            variant="secondary"
            size="sm"
            disabled={page >= totalPages - 1}
            onclick={() => goToPage(page + 1)}
          >
            {$t.dashboard?.next || "Next"}
          </Button>
        </div>
      </div>
    {/if}
  {/if}
</div>

<style>
  .dashboard-data-grid {
    width: 100%;
  }
</style>
<!-- #endregion Dashboard.DataGrid -->
