<script setup>
import { cel as $cel } from "../../utils/cel";
import { Icon } from "@iconify/vue";
import {
  FlexRender,
  getCoreRowModel,
  getExpandedRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useVueTable
} from "@tanstack/vue-table";
import { useVirtualizer } from "@tanstack/vue-virtual";
import { Effect, Fiber, Option } from "effect";
import { Fetch } from "fx-fetch";
import { Pagination } from "reka-ui/namespaced";
import { computed, h, onMounted, ref, toRaw, watch } from "vue";
import { fetchJsonOption } from "../../share/request";
import { useI18n } from "vue-i18n";
import { celBindings, provideCELContext, injectCELContext } from "../../utils/cel-context";
import TableRowProvider from "./row-provider.vue";
import { getLocalizedText } from "../../share/locale";
import { Button } from "../ui/button";
import { Markdown } from "../ui/markdown";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
import { NumberField, NumberFieldInput } from "../ui/number-field";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import ShwfedActions from "../actions/components/group.vue";
import ShwfedForm from "../form/index.vue";
import { evaluateInitial } from "../form/utils/initial";
import { provideTableInstanceId } from "./utils/instance";
import { provideSharedFetchLayer } from "./utils/shared-fetch";
import { provideEventTarget } from "../../share/event-bus";
import { usePersistedQuery } from "../../share/use-persisted-query";
import { findColumn } from "./utils/resolve";
import { interpolateMarkdown } from "./utils/runtime";
import { carrySymbolId, createRowKeyResolver } from "./utils/row-key";
import { provideReorderContext } from "./utils/reorder";
import { applyReorder, useRowReorder } from "./utils/row-reorder";
import { type as DRAG_HANDLE_COLUMN_TYPE } from "./columns/2026-05-21/com.shwfed.table.column.drag-handle/schema";
defineOptions({ name: "ShwfedTable" });
const config = defineModel("config", { type: Object });
const rowData = defineModel("rows", { type: Array, ...{ default: () => [] } });
const serverTotal = ref(void 0);
const isFetching = ref(false);
const queryState = ref({});
const persistedQuery = usePersistedQuery(
  config.value?.id,
  queryState,
  () => config.value?.persistQuery === true
);
const { t, locale } = useI18n({
  inheritLocale: true,
  messages: {
    zh: {
      "total": "\u5171 {count} \u6761",
      "pagination-page-size": "\u6BCF\u9875\u6761\u6570",
      "goto": "\u524D\u5F80 {page} \u9875",
      "sort-asc": "\u5347\u5E8F",
      "sort-desc": "\u964D\u5E8F",
      "sort-unsorted": "\u672A\u6392\u5E8F",
      "column-tooltip": "\u67E5\u770B\u5217\u63D0\u793A\uFF1A{column}",
      "reorder-disabled-sort": "\u5DF2\u6392\u5E8F\u65F6\u4E0D\u53EF\u62D6\u62FD\uFF0C\u8BF7\u5148\u53D6\u6D88\u6392\u5E8F",
      "reorder-disabled-filter": "\u5DF2\u7B5B\u9009\u65F6\u4E0D\u53EF\u62D6\u62FD\uFF0C\u8BF7\u5148\u6E05\u9664\u7B5B\u9009",
      "reorder-disabled-grouping": "\u5DF2\u5206\u7EC4\u65F6\u4E0D\u53EF\u62D6\u62FD\uFF0C\u8BF7\u5148\u53D6\u6D88\u5206\u7EC4",
      "reorder-disabled-pinning": "\u5DF2\u56FA\u5B9A\u884C\u65F6\u4E0D\u53EF\u62D6\u62FD\uFF0C\u8BF7\u5148\u53D6\u6D88\u56FA\u5B9A",
      "reorder-disabled-manual-pagination": "\u670D\u52A1\u7AEF\u5206\u9875\u4E0B\u4E0D\u53EF\u672C\u5730\u62D6\u62FD\u6392\u5E8F"
    },
    en: {
      "total": "Total {count} items",
      "pagination-page-size": "Page size",
      "goto": "Go to page {page}",
      "sort-asc": "Ascending",
      "sort-desc": "Descending",
      "sort-unsorted": "Unsorted",
      "column-tooltip": "Show column tooltip: {column}",
      "reorder-disabled-sort": "Drag disabled while sorted \u2014 clear sorting first",
      "reorder-disabled-filter": "Drag disabled while filtered \u2014 clear filters first",
      "reorder-disabled-grouping": "Drag disabled while grouped \u2014 clear grouping first",
      "reorder-disabled-pinning": "Drag disabled while rows are pinned \u2014 unpin first",
      "reorder-disabled-manual-pagination": "Drag disabled with server-side pagination"
    },
    ja: {
      "total": "\u5408\u8A08 {count} \u4EF6",
      "pagination-page-size": "1\u30DA\u30FC\u30B8\u3042\u305F\u308A\u306E\u4EF6\u6570",
      "goto": "\u30DA\u30FC\u30B8 {page} \u306B\u79FB\u52D5",
      "sort-asc": "\u6607\u9806",
      "sort-desc": "\u964D\u9806",
      "sort-unsorted": "\u672A\u30BD\u30FC\u30C8",
      "column-tooltip": "\u5217\u30C4\u30FC\u30EB\u30C1\u30C3\u30D7\u3092\u8868\u793A: {column}",
      "reorder-disabled-sort": "\u30BD\u30FC\u30C8\u4E2D\u306F\u30C9\u30E9\u30C3\u30B0\u3067\u304D\u307E\u305B\u3093\u3002\u30BD\u30FC\u30C8\u3092\u89E3\u9664\u3057\u3066\u304F\u3060\u3055\u3044",
      "reorder-disabled-filter": "\u30D5\u30A3\u30EB\u30BF\u30FC\u4E2D\u306F\u30C9\u30E9\u30C3\u30B0\u3067\u304D\u307E\u305B\u3093\u3002\u30D5\u30A3\u30EB\u30BF\u30FC\u3092\u89E3\u9664\u3057\u3066\u304F\u3060\u3055\u3044",
      "reorder-disabled-grouping": "\u30B0\u30EB\u30FC\u30D7\u5316\u4E2D\u306F\u30C9\u30E9\u30C3\u30B0\u3067\u304D\u307E\u305B\u3093\u3002\u30B0\u30EB\u30FC\u30D7\u5316\u3092\u89E3\u9664\u3057\u3066\u304F\u3060\u3055\u3044",
      "reorder-disabled-pinning": "\u56FA\u5B9A\u884C\u304C\u3042\u308B\u305F\u3081\u30C9\u30E9\u30C3\u30B0\u3067\u304D\u307E\u305B\u3093\u3002\u56FA\u5B9A\u3092\u89E3\u9664\u3057\u3066\u304F\u3060\u3055\u3044",
      "reorder-disabled-manual-pagination": "\u30B5\u30FC\u30D0\u30FC\u30B5\u30A4\u30C9\u30DA\u30FC\u30B8\u30F3\u30B0\u3067\u306F\u30C9\u30E9\u30C3\u30B0\u4E26\u3079\u66FF\u3048\u306F\u3067\u304D\u307E\u305B\u3093"
    }
  }
});
const inheritedContext = injectCELContext();
const containerRef = ref(null);
const appliedInitialStateKey = ref("");
function getLocaleText(value) {
  return getLocalizedText(value, locale.value);
}
function mergeCelContext(local) {
  return { ...celBindings(inheritedContext), query: queryState.value, sorts: currentSorts(), ...local };
}
function currentSorts() {
  const sortKeyById = new Map(
    (config.value?.columns ?? []).map((c) => [c.id, c.sortKey]).filter((e) => typeof e[1] === "string" && e[1].length > 0)
  );
  return tableApi.getState().sorting.map((s) => ({ key: sortKeyById.get(s.id), mode: s.desc ? "desc" : "asc" })).filter((s) => s.key !== void 0);
}
function translateColumn(column) {
  const entry = findColumn(column.type, column.compatibilityDate);
  if (!entry) {
    throw new Error(`[shwfed-table] no column registered for ${column.type}@${column.compatibilityDate}`);
  }
  const contribution = entry.toColumnDef(column, { getLocaleText, $cel, inheritedContext: celBindings(inheritedContext) });
  return {
    ...contribution,
    id: column.id,
    // `ctx` is the functional-component props object Vue mutates in place on
    // every update, so its identity stays stable across data refetches.
    // Forwarding it unchanged makes `entry.runtime` see reference-equal props,
    // so `shouldUpdateComponent` skips the update and the cell (and its CEL
    // evaluation) never refreshes after the row data changes. Spread into a
    // fresh object so the child re-renders whenever the context does.
    cell: (ctx) => h(entry.runtime, { column, ctx: { ...ctx } })
  };
}
function makeGroupColumn(group, children) {
  return {
    id: group.id,
    header: getLocaleText(group.title) ?? "",
    columns: children,
    meta: {
      tooltip: getLocaleText(group.tooltip),
      grow: false
    }
  };
}
const columns = computed(() => {
  const cfg = config.value;
  if (!cfg) return [];
  const groupById = new Map(cfg.groups?.map((g) => [g.id, g]) ?? []);
  const out = [];
  let current = null;
  for (const col of cfg.columns) {
    const def = translateColumn(col);
    const group = col.groupId ? groupById.get(col.groupId) : void 0;
    if (!group) {
      if (current) {
        out.push(makeGroupColumn(current.group, current.children));
        current = null;
      }
      out.push(def);
      continue;
    }
    if (current && current.group.id === group.id) {
      current.children.push(def);
    } else {
      if (current) out.push(makeGroupColumn(current.group, current.children));
      current = { group, children: [def] };
    }
  }
  if (current) out.push(makeGroupColumn(current.group, current.children));
  return out;
});
const tableActionsConfig = computed(() => {
  const a = config.value?.actions;
  if (!a) return void 0;
  if ((a.groups?.length ?? 0) === 0 && (a.items?.length ?? 0) === 0) return void 0;
  return {
    kind: "shwfed.component.action",
    size: a.size ?? "default",
    gap: a.gap ?? 4,
    style: a.style,
    groups: a.groups.map((g) => ({ ...g, variant: g.variant ?? "default" })),
    items: a.items
  };
});
const isPaginationEnabled = computed(() => config.value?.pagination !== void 0);
const isManualPagination = computed(() => isPaginationEnabled.value && config.value?.dataSource?.total !== void 0);
const totalItems = computed(() => isManualPagination.value ? serverTotal.value ?? 0 : rowData.value.length);
const paginationConfig = computed(() => config.value?.pagination);
const paginationPageSizes = computed(() => paginationConfig.value?.pageSizes ?? []);
const paginationLeftSource = computed(() => {
  const source = getLocaleText(paginationConfig.value?.left);
  if (!source) return "";
  return interpolateMarkdown(source, $cel, {
    selected: tableApi.getSelectedRowModel().rows.map((row) => row.original)
  });
});
const paginationRightSource = computed(() => {
  const source = getLocaleText(paginationConfig.value?.right);
  if (!source) return "";
  return interpolateMarkdown(source, $cel, {
    selected: tableApi.getSelectedRowModel().rows.map((row) => row.original)
  });
});
function getRowAt(i) {
  return rowData.value[i];
}
function setRowAt(i, next) {
  const rows2 = rowData.value;
  if (i < 0 || i >= rows2.length) return;
  carrySymbolId(rows2[i], next);
  rowData.value = rows2.map((r, j) => j === i ? next : r);
}
const derivedEditableColumns = computed(
  () => (config.value?.columns ?? []).filter((c) => typeof c.binding === "string" && !!c.derived)
);
const getRowId = createRowKeyResolver({
  evaluateKey: (row, index) => {
    const expr = config.value?.rowKey;
    if (!expr) return void 0;
    try {
      const result = Effect.runSync($cel(expr, mergeCelContext({ row, index })));
      if (typeof result === "string") return result;
      if (typeof result === "number" && Number.isFinite(result)) return result;
      return void 0;
    } catch (e) {
      console.error("[shwfed-table] rowKey expression failed", e);
      return void 0;
    }
  }
});
const tableApi = useVueTable({
  get columns() {
    return columns.value;
  },
  get data() {
    return rowData.value;
  },
  get manualPagination() {
    return isManualPagination.value;
  },
  // Defer to server order under server pagination — the page only holds one
  // server-sorted slice, so client re-sorting it would be wrong.
  get manualSorting() {
    return isManualPagination.value;
  },
  get rowCount() {
    return isManualPagination.value ? serverTotal.value : void 0;
  },
  get initialState() {
    return {
      pagination: {
        pageIndex: paginationConfig.value?.pageIndex ?? 0,
        pageSize: paginationConfig.value?.pageSize ?? 10
      }
    };
  },
  getRowId,
  getCoreRowModel: getCoreRowModel(),
  getExpandedRowModel: getExpandedRowModel(),
  getPaginationRowModel: getPaginationRowModel(),
  getSortedRowModel: getSortedRowModel(),
  columnResizeMode: "onChange"
});
provideCELContext({
  selected: {
    type: "list<dyn>",
    label: "\u5DF2\u9009\u884C",
    description: "\u5F53\u524D\u5DF2\u52FE\u9009\u7684\u6240\u6709\u884C\uFF08`row` \u7684\u6570\u7EC4\uFF09\u3002",
    get value() {
      return tableApi.getSelectedRowModel().rows.map((r) => r.original);
    }
  },
  query: {
    type: "dyn",
    label: "\u641C\u7D22\u6761\u4EF6",
    description: "\u6E32\u67D3\u5728\u8868\u683C\u4E0A\u65B9\u7684\u641C\u7D22\u6761\u4EF6\u8868\u5355\u5F53\u524D\u72B6\u6001\uFF1B\u672A\u914D\u7F6E\u65F6\u4E3A\u7A7A\u5BF9\u8C61\u3002",
    get value() {
      return queryState.value;
    }
  },
  sorts: {
    type: "list<map<string, string>>",
    label: "\u6392\u5E8F\u72B6\u6001",
    description: '\u5F53\u524D\u6392\u5E8F\u5217\u8868\uFF0C\u6BCF\u9879\u4E3A `{key, mode}`\uFF1B`key` \u53D6\u81EA\u5217\u7684\u300C\u6392\u5E8F\u5B57\u6BB5\u300D(sortKey)\uFF0C`mode` \u4E3A `"asc"`/`"desc"`\uFF1B\u672A\u6392\u5E8F\u65F6\u4E3A\u7A7A\u6570\u7EC4\u3002',
    get value() {
      return currentSorts();
    }
  }
});
provideSharedFetchLayer();
const hasDragHandleColumn = computed(() => (config.value?.columns ?? []).some((c) => c.type === DRAG_HANDLE_COLUMN_TYPE));
const reorderDisabledReason = computed(() => {
  if (!hasDragHandleColumn.value) return null;
  const state = tableApi.getState();
  if (state.sorting.length > 0) return t("reorder-disabled-sort");
  if (state.columnFilters.length > 0) return t("reorder-disabled-filter");
  const gf = state.globalFilter;
  if (typeof gf === "string" && gf.length > 0) return t("reorder-disabled-filter");
  if (state.grouping.length > 0) return t("reorder-disabled-grouping");
  const rp = state.rowPinning;
  if ((rp?.top?.length ?? 0) > 0 || (rp?.bottom?.length ?? 0) > 0) {
    return t("reorder-disabled-pinning");
  }
  if (isManualPagination.value) return t("reorder-disabled-manual-pagination");
  return null;
});
const isReorderable = computed(() => hasDragHandleColumn.value && reorderDisabledReason.value === null);
const rowReorder = useRowReorder({
  isEnabled: () => isReorderable.value,
  onReorder: (sourceId, targetId, position) => reorderRow(sourceId, targetId, position)
});
function reorderRow(sourceId, targetId, position) {
  if (sourceId === targetId) return;
  const flat = tableApi.getCoreRowModel().rows;
  const src = flat.find((r) => r.id === sourceId);
  const tgt = flat.find((r) => r.id === targetId);
  if (!src || !tgt) return;
  const next = applyReorder(rowData.value, src.index, tgt.index, position);
  if (next === rowData.value) return;
  rowData.value = next;
}
provideReorderContext({
  enabled: isReorderable,
  disabledReason: reorderDisabledReason,
  registerHandle: rowReorder.setHandle,
  instructionFor: rowReorder.instructionFor
});
const rowRefCache = /* @__PURE__ */ new Map();
function getRowRefFn(rowId) {
  let fn = rowRefCache.get(rowId);
  if (!fn) {
    fn = (el) => {
      measureRow(el);
      rowReorder.setRow(rowId, el instanceof HTMLElement ? el : null);
    };
    rowRefCache.set(rowId, fn);
  }
  return fn;
}
async function fetchDataSource() {
  const dataSource = config.value?.dataSource;
  if (!dataSource) return;
  const pageIndex = tableApi.getState().pagination.pageIndex;
  const pageSize = tableApi.getState().pagination.pageSize;
  const wantsTotal = dataSource.total !== void 0;
  const effect = Effect.gen(function* () {
    let jsonOpt = Option.none();
    if (dataSource.request) {
      jsonOpt = yield* fetchJsonOption(yield* $cel(
        dataSource.request,
        mergeCelContext({ pageIndex, pageSize })
      ));
    }
    const nextRowsRaw = yield* $cel(
      dataSource.data,
      mergeCelContext({ json: jsonOpt })
    );
    const nextRows = nextRowsRaw ?? [];
    let nextTotal;
    if (wantsTotal) {
      const totalRaw = yield* $cel(
        dataSource.total,
        mergeCelContext({ json: jsonOpt })
      );
      const total = totalRaw ?? 0;
      nextTotal = typeof total === "number" ? total : Number(total) || 0;
    }
    return { nextRows, nextTotal };
  });
  const program = Effect.gen(function* () {
    const showMaskFiber = yield* Effect.fork(
      Effect.sync(() => {
        isFetching.value = true;
      }).pipe(Effect.delay("30 millis"))
    );
    return yield* Effect.ensuring(effect, Fiber.interrupt(showMaskFiber));
  }).pipe(
    Effect.ensuring(Effect.sync(() => {
      isFetching.value = false;
    }))
  );
  try {
    const result = await Effect.runPromise(Effect.provide(program, Fetch.layer));
    rowData.value = Array.isArray(result.nextRows) ? result.nextRows : [];
    serverTotal.value = wantsTotal ? result.nextTotal : void 0;
  } catch (e) {
    console.warn("[shwfed-table] dataSource fetch failed", e);
  }
}
async function resetQuery() {
  persistedQuery.clear();
  const initial = config.value?.query?.initial;
  if (!initial) {
    queryState.value = {};
    return;
  }
  try {
    const program = evaluateInitial(initial, $cel, { query: {} });
    const result = await Effect.runPromise(Effect.provide(program, Fetch.layer));
    queryState.value = result && typeof result === "object" && !Array.isArray(result) ? { ...result } : {};
  } catch (e) {
    console.error("[shwfed-table] resetQuery failed", e);
    queryState.value = {};
  }
}
function cloneRow(row) {
  if (!row || typeof row !== "object") return row;
  try {
    return structuredClone(toRaw(row));
  } catch {
    return { ...row };
  }
}
const tableInstanceId = config.value?.id ?? crypto.randomUUID();
provideTableInstanceId(tableInstanceId);
provideEventTarget(tableInstanceId, {
  "reset-query": () => Effect.promise(resetQuery),
  "reset-pagination": () => Effect.promise(async () => {
    tableApi.setPageIndex(0);
    await fetchDataSource();
  }),
  "search": () => Effect.promise(fetchDataSource),
  "clear-selection": () => Effect.sync(() => {
    tableApi.resetRowSelection(true);
  }),
  // Local row edits — mutate `rowData` in place, no server round-trip. Each
  // re-assigns the array so the `rows` model re-emits and a host (the
  // `table` form field) sees the live list. Intended for client-side data:
  // with server pagination (`dataSource.total`) `rowData` is only the
  // current page, so a fetch will overwrite these edits.
  "add-row": () => Effect.sync(() => {
    rowData.value = [...rowData.value, {}];
  }),
  "duplicate-selected": () => Effect.sync(() => {
    const clones = tableApi.getSelectedRowModel().rows.map((r) => cloneRow(r.original));
    if (clones.length > 0) rowData.value = [...rowData.value, ...clones];
  }),
  "delete-selected": () => Effect.sync(() => {
    const drop = new Set(tableApi.getSelectedRowModel().rows.map((r) => r.index));
    if (drop.size === 0) return;
    rowData.value = rowData.value.filter((_, i) => !drop.has(i));
    tableApi.resetRowSelection(true);
  })
});
const queryRef = ref(null);
onMounted(async () => {
  await queryRef.value?.seeded;
  persistedQuery.restore();
  await fetchDataSource();
});
watch(
  () => {
    if (!isManualPagination.value) return null;
    const { pageIndex, pageSize } = tableApi.getState().pagination;
    const sorting = tableApi.getState().sorting;
    return `${pageIndex}:${pageSize}:${JSON.stringify(sorting)}`;
  },
  (next, prev) => {
    if (next !== null && next !== prev) void fetchDataSource();
  }
);
watch(
  () => {
    const ds = config.value?.dataSource;
    if (!ds) return "";
    return `${ds.request ?? ""}|${ds.data}|${ds.total ?? ""}`;
  },
  (next, prev) => {
    if (next && next !== prev) void fetchDataSource();
  }
);
const sortedRowIndexMap = computed(() => {
  const map = /* @__PURE__ */ new Map();
  tableApi.getSortedRowModel().flatRows.forEach((row, i) => {
    map.set(row.id, i);
  });
  return map;
});
function getDisplayIndex(row) {
  return sortedRowIndexMap.value.get(row.id) ?? row.index;
}
watch(config, (config2) => {
  if (!config2) return;
  const initialState = config2.initialState;
  const nextKey = initialState ? JSON.stringify(initialState) : "";
  if (appliedInitialStateKey.value === nextKey || !initialState) {
    appliedInitialStateKey.value = nextKey;
    syncPaginationPageSize();
    return;
  }
  tableApi.initialState = mergeInitialState(tableApi.initialState, initialState);
  tableApi.reset();
  appliedInitialStateKey.value = nextKey;
  syncPaginationPageSize();
}, { immediate: true });
defineExpose(tableApi);
const rows = computed(() => isPaginationEnabled.value ? tableApi.getRowModel().rows : tableApi.getPrePaginationRowModel().rows);
const maxPage = computed(() => isPaginationEnabled.value ? Math.max(tableApi.getPageCount(), 1) : 1);
const rowVirtualizer = useVirtualizer(
  computed(() => ({
    count: rows.value.length,
    estimateSize: () => 33,
    getScrollElement: () => containerRef.value,
    overscan: 30
  }))
);
const rowTotalSize = computed(() => rowVirtualizer.value.getTotalSize());
const rowWindow = computed(() => rowVirtualizer.value.getVirtualItems());
function measureRow(el) {
  if (!el || !(el instanceof Element)) return;
  rowVirtualizer.value.measureElement(el);
}
function isStyleRecord(x) {
  return typeof x === "object" && x !== null && !Array.isArray(x);
}
function getCellStyles(ctx) {
  const expression = config.value?.cellStyle;
  if (!expression) return {};
  try {
    const result = Effect.runSync($cel(expression, mergeCelContext({
      row: ctx.row.original,
      index: getDisplayIndex(ctx.row),
      id: ctx.column.id,
      selected: ctx.row.getIsSelected(),
      pinned: ctx.column.getIsPinned()
    })));
    return isStyleRecord(result) ? result : {};
  } catch (e) {
    console.error("[shwfed-table] cellStyle failed", e);
    return {};
  }
}
function shouldHaveRightBorder(column) {
  switch (column.getIsPinned()) {
    case "left":
      return true;
    case "right":
      return false;
    case false:
      return !column.getIsLastColumn("center");
  }
}
function shouldHaveLeftBorder(column) {
  switch (column.getIsPinned()) {
    case "left":
      return false;
    case "right":
      return true;
    case false:
      return false;
  }
}
function pinnedStyle(column) {
  const pinned = column.getIsPinned();
  if (!pinned) return {};
  const style = {
    [pinned]: pinned === "left" ? `${column.getStart("left")}px` : `${column.getAfter("right")}px`
  };
  if (column.getIsLastColumn("left"))
    style.boxShadow = "5px 5px 5px #00000005";
  if (column.getIsFirstColumn("right"))
    style.boxShadow = "-5px 5px 5px #00000005";
  return style;
}
function shouldShowResizeHandle(column) {
  const lastVisibleLeafColumn = tableApi.getVisibleLeafColumns().at(-1);
  if (!lastVisibleLeafColumn) return false;
  const lastLeafColumn = column.getLeafColumns().at(-1);
  return lastLeafColumn?.id !== lastVisibleLeafColumn.id;
}
const SORT_ICONS = {
  unsorted: "fluent:arrow-sort-16-regular",
  asc: "fluent:arrow-sort-up-16-regular",
  desc: "fluent:arrow-sort-down-16-regular"
};
function getSortIcon(column) {
  const dir = column.getIsSorted();
  return SORT_ICONS[dir === false ? "unsorted" : dir] ?? SORT_ICONS.unsorted;
}
function getSortLabel(column) {
  const direction = column.getIsSorted();
  if (direction === "asc") return t("sort-asc");
  if (direction === "desc") return t("sort-desc");
  return t("sort-unsorted");
}
const currentPageSize = computed({
  get: () => tableApi.getState().pagination.pageSize,
  set: (value) => {
    const nextPageSize = Number(value);
    if (!Number.isInteger(nextPageSize) || nextPageSize <= 0) return;
    tableApi.setPageSize(nextPageSize);
  }
});
function syncPaginationPageSize() {
  if (!isPaginationEnabled.value) return;
  const pageSizes = paginationPageSizes.value;
  if (pageSizes.length === 0) return;
  const currentPageSize2 = tableApi.getState().pagination.pageSize;
  if (pageSizes.includes(currentPageSize2)) return;
  const closest = pageSizes.reduce(
    (prev, curr) => Math.abs(curr - currentPageSize2) < Math.abs(prev - currentPageSize2) ? curr : prev
  );
  if (closest !== currentPageSize2) {
    tableApi.setPageSize(closest);
  }
}
function cloneItems(items) {
  return items ? [...items] : void 0;
}
function normalizeExpandedState(expanded, current) {
  if (expanded === void 0) return current;
  if (expanded === true) return true;
  if (expanded === false) return {};
  return expanded;
}
function mergeInitialState(current, initialState) {
  return {
    ...current,
    columnVisibility: initialState.columnVisibility ?? current.columnVisibility,
    columnOrder: cloneItems(initialState.columnOrder) ?? current.columnOrder,
    columnPinning: initialState.columnPinning ? {
      ...current.columnPinning,
      left: cloneItems(initialState.columnPinning.left) ?? current.columnPinning.left,
      right: cloneItems(initialState.columnPinning.right) ?? current.columnPinning.right
    } : current.columnPinning,
    rowPinning: initialState.rowPinning ? {
      ...current.rowPinning,
      top: cloneItems(initialState.rowPinning.top) ?? current.rowPinning.top,
      bottom: cloneItems(initialState.rowPinning.bottom) ?? current.rowPinning.bottom
    } : current.rowPinning,
    sorting: cloneItems(initialState.sorting) ?? current.sorting,
    expanded: normalizeExpandedState(initialState.expanded, current.expanded),
    grouping: cloneItems(initialState.grouping) ?? current.grouping,
    columnSizing: initialState.columnSizing ?? current.columnSizing,
    pagination: isPaginationEnabled.value ? {
      pageIndex: paginationConfig.value?.pageIndex ?? current.pagination.pageIndex,
      pageSize: paginationConfig.value?.pageSize ?? current.pagination.pageSize
    } : current.pagination,
    rowSelection: initialState.rowSelection ?? current.rowSelection
  };
}
</script>

<script>
export { TableConfig, createTableConfig, getColumnTechnicalKey } from "./schema";
</script>

<template>
  <div
    class="relative flex flex-col overflow-hidden gap-2"
    :style="config?.style"
  >
    <div
      v-if="config?.query"
      class="w-full py-1.5"
    >
      <ShwfedForm
        ref="queryRef"
        v-model="queryState"
        :config="config.query"
      />
    </div>
    <nav
      v-if="tableActionsConfig"
      class="flex items-center justify-between"
    >
      <ShwfedActions
        :config="tableActionsConfig"
      />
    </nav>
    <div
      :class="[
  'flex min-h-0 flex-1 flex-col relative'
]"
    >
      <div
        v-if="isFetching"
        class="absolute inset-0 bg-white/20 z-30 pointer-events-auto"
        aria-hidden="true"
      />
      <ClientOnly>
        <!-- Scrollable table container -->
        <div
          ref="containerRef"
          class="overflow-scroll overscroll-none flex-1 border border-zinc-200 rounded relative"
        >
          <div :style="{ height: `${rowTotalSize}px` }">
            <table class="grid">
              <!-- Header -->
              <thead
                v-if="columns.length > 0"
                class="grid sticky top-0 z-10 select-none"
              >
                <tr
                  v-for="group in tableApi.getHeaderGroups()"
                  :key="group.id"
                  class="flex w-full border-b border-zinc-200"
                >
                  <th
                    v-for="header in group.headers"
                    :key="header.id"
                    :colspan="header.colSpan"
                    :class="[
  header.column.columnDef.meta?.grow && 'flex-1',
  'flex items-center gap-2 border-zinc-300 py-2 text-zinc-600',
  'text-xs flex items-center justify-center relative',
  'bg-[color-mix(in_srgb,var(--primary)_7%,white)] group',
  header.column.getIsPinned() && 'sticky z-15',
  shouldHaveRightBorder(header.column) && 'border-r',
  shouldHaveLeftBorder(header.column) && 'border-l'
]"
                    :style="{
  width: `${header.getSize()}px`,
  ...pinnedStyle(header.column)
}"
                  >
                    <!-- Column header with tooltip: dashed underline + Markdown tooltip -->
                    <Tooltip
                      v-if="!header.isPlaceholder && header.column.columnDef.meta?.tooltip"
                      :delay-duration="180"
                    >
                      <TooltipTrigger as-child>
                        <span class="decoration-dashed underline decoration-(--primary) underline-offset-4 px-4 cursor-help">
                          <FlexRender
                            :render="header.column.columnDef.header"
                            :props="header.getContext()"
                          />
                        </span>
                      </TooltipTrigger>
                      <TooltipContent
                        align="center"
                        side="top"
                        class="max-w-xs text-xs"
                      >
                        <Markdown
                          :source="header.column.columnDef.meta.tooltip"
                          class="prose prose-xs prose-zinc"
                        />
                      </TooltipContent>
                    </Tooltip>

                    <!-- Column header without tooltip: plain render -->
                    <FlexRender
                      v-else-if="!header.isPlaceholder"
                      :render="header.column.columnDef.header"
                      :props="header.getContext()"
                    />

                    <!-- Sort toggle -->
                    <Tooltip
                      v-if="!header.isPlaceholder && header.column.getCanSort()"
                      :delay-duration="800"
                    >
                      <TooltipTrigger as-child>
                        <Button
                          variant="ghost"
                          size="xs"
                          :class="[
  'absolute hover:bg-transparent right-1 top-1/2 -translate-y-1/2 transform-3d transition-opacity duration-180',
  !header.column.getIsSorted() ? 'opacity-30 hover:opacity-60' : 'text-(--primary)/80 hover:text-(--primary)'
]"
                          as-child
                        >
                          <button
                            type="button"
                            :aria-label="getSortLabel(header.column)"
                            @click="header.column.getToggleSortingHandler()?.($event)"
                          >
                            <Icon :icon="getSortIcon(header.column)" />
                          </button>
                        </Button>
                      </TooltipTrigger>
                      <TooltipContent>
                        {{ getSortLabel(header.column) }}
                      </TooltipContent>
                    </Tooltip>

                    <!-- Resize handle -->
                    <div
                      v-if="!header.isPlaceholder && header.column.getCanResize() && shouldShowResizeHandle(header.column)"
                      :class="[
  'group',
  'absolute',
  'top-0',
  'bottom-0',
  'right-0',
  'px-2',
  'translate-x-2',
  'z-5',
  'opacity-0',
  'duration-150',
  'transition-opacity',
  'ease-out',
  'hover:opacity-100',
  'cursor-col-resize'
]"
                      @mousedown="header.getResizeHandler()($event)"
                    >
                      <div class="w-2pt h-full translate-x-1pt transform-3d bg-[color-mix(in_srgb,var(--primary)_80%,white)]" />
                    </div>
                  </th>
                </tr>
              </thead>

              <!-- Body -->
              <tbody
                v-if="columns.length > 0"
                class="grid relative"
                :style="{ height: `${rowTotalSize}px` }"
              >
                <template
                  v-for="r in rowWindow"
                  :key="rows[r.index]?.id ?? r.index"
                >
                  <TableRowProvider
                    v-if="rows[r.index]"
                    :source-index="rows[r.index].index"
                    :display-index="getDisplayIndex(rows[r.index])"
                    :derived-columns="derivedEditableColumns"
                    :get-row="getRowAt"
                    :set-row="setRowAt"
                    :scope-id="config?.id"
                  >
                    <tr
                      :ref="getRowRefFn(rows[r.index].id)"
                      class="flex absolute w-full border-b border-zinc-300"
                      :data-index="rows[r.index].index"
                      :style="{ transform: `translate3d(0, ${r.start}px, 0)` }"
                    >
                      <!--
                        Drop indicator. Absolutely positioned so it overlays
                        the row without affecting flex layout, and sits at
                        z-30 above sticky pinned cells (which use z-15) so
                        the line is never hidden behind a column. The bar is
                        offset by -1px so its 2px height is centered exactly
                        on the row boundary — for adjacent rows, "reorder-
                        below on i" and "reorder-above on i+1" then resolve
                        to the same pixel band, so the indicator doesn't
                        jitter as the hitbox flips between them.
                      -->
                      <span
                        v-if="rowReorder.instructionFor(rows[r.index].id) === 'reorder-above'"
                        aria-hidden="true"
                        class="absolute left-0 right-0 -top-px h-0.5 z-30 pointer-events-none bg-(--primary)"
                      />
                      <span
                        v-else-if="rowReorder.instructionFor(rows[r.index].id) === 'reorder-below'"
                        aria-hidden="true"
                        class="absolute left-0 right-0 -bottom-px h-0.5 z-30 pointer-events-none bg-(--primary)"
                      />
                      <td
                        v-for="cell in rows[r.index].getVisibleCells()"
                        :key="cell.id"
                        :class="[
  'border-zinc-300',
  cell.column.columnDef.meta?.grow && 'flex-1',
  cell.column.getIsPinned() && 'sticky z-15',
  shouldHaveRightBorder(cell.column) && 'border-r',
  shouldHaveLeftBorder(cell.column) && 'border-l'
]"
                        :style="{
  width: `${cell.column.getSize()}px`,
  ...pinnedStyle(cell.column),
  ...getCellStyles(cell.getContext())
}"
                      >
                        <FlexRender
                          :render="cell.column.columnDef.cell"
                          :props="cell.getContext()"
                        />
                      </td>
                    </tr>
                  </TableRowProvider>
                </template>
              </tbody>

              <!-- Footer -->
              <tfoot class="hidden has-data-footer:grid sticky bottom-0 z-10 select-none border-t border-zinc-200">
                <tr
                  v-for="group in tableApi.getFooterGroups()"
                  :key="group.id"
                  class="flex w-full border-zinc-200"
                >
                  <th
                    v-for="header in group.headers"
                    :key="header.id"
                    :colspan="header.colSpan"
                    :class="[
  header.column.columnDef.meta?.grow && 'flex-1',
  'flex items-center gap-2 border-zinc-300 text-zinc-600 p-0',
  'text-xs flex items-center justify-center relative',
  'bg-[color-mix(in_srgb,var(--primary)_7%,white)] group',
  header.column.getIsPinned() && 'sticky z-15',
  shouldHaveRightBorder(header.column) && 'border-r',
  shouldHaveLeftBorder(header.column) && 'border-l'
]"
                    :style="{
  width: `${header.getSize()}px`,
  ...pinnedStyle(header.column)
}"
                  >
                    <FlexRender
                      v-if="!header.isPlaceholder"
                      :render="header.column.columnDef.footer"
                      :props="header.getContext()"
                    />
                  </th>
                </tr>
              </tfoot>
            </table>
          </div>
        </div>
      </ClientOnly>

      <!-- Pagination -->
      <ClientOnly>
        <div
          v-if="isPaginationEnabled"
          class="flex items-center justify-between w-full py-2 gap-2 text-sm text-zinc-600 @container"
        >
          <!-- Left markdown slot -->
          <div class="relative p-1 flex-1 prose prose-zinc text-xs invisible @lg:visible">
            <Markdown :source="paginationLeftSource" />
            &nbsp;
          </div>

          <div class="flex items-center gap-4">
            <!-- Total count -->
            <span class="text-xs hidden @lg:inline">
              {{ t("total", { count: totalItems }) }}
            </span>

            <!-- Page size selector -->
            <label
              v-if="paginationPageSizes.length > 0"
              data-slot="table-pagination-page-size"
              class="items-center gap-2 text-xs text-zinc-500 hidden @lg:flex"
            >
              <Select v-model="currentPageSize">
                <SelectTrigger
                  size="sm"
                  :aria-label="t('pagination-page-size')"
                  class="h-7 w-32 text-xs"
                >
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem
                    v-for="pageSize in paginationPageSizes"
                    :key="pageSize"
                    :value="pageSize"
                  >
                    {{ pageSize }}
                  </SelectItem>
                </SelectContent>
              </Select>
            </label>

            <!-- Page navigation -->
            <Pagination.Root
              show-edges
              :total="totalItems"
              :items-per-page="tableApi.getState().pagination.pageSize"
              :page="tableApi.getState().pagination.pageIndex + 1"
              @update:page="(page2) => tableApi.setPageIndex(page2 - 1)"
            >
              <Pagination.List
                v-slot="{ items }"
                class="flex items-center gap-1"
              >
                <Pagination.First
                  :class="[
  'w-7 h-7 flex items-center justify-center bg-transparent hover:bg-zinc-100 transition disabled:opacity-50 rounded text-zinc-600',
  'data-disabled:opacity-50 data-disabled:cursor-not-allowed cursor-pointer'
]"
                >
                  <Icon
                    icon="radix-icons:double-arrow-left"
                    class="w-4 h-4"
                  />
                </Pagination.First>
                <Pagination.Prev
                  :class="[
  'w-7 h-7 flex items-center justify-center bg-transparent hover:bg-zinc-100 transition disabled:opacity-50 rounded text-zinc-600',
  'data-disabled:opacity-50 data-disabled:cursor-not-allowed cursor-pointer'
]"
                >
                  <Icon
                    icon="radix-icons:chevron-left"
                    class="w-4 h-4"
                  />
                </Pagination.Prev>

                <template
                  v-for="(page, pageItemIndex) in items"
                  :key="page.type === 'page' ? `${page.type}-${page.value}` : `${page.type}-${pageItemIndex}`"
                >
                  <Pagination.ListItem
                    v-if="page.type === 'page'"
                    :class="[
  'w-7 h-7 items-center justify-center rounded text-xs bg-transparent',
  'data-selected:text-(--primary) hover:bg-zinc-100 transition cursor-pointer',
  page.value - 1 === tableApi.getState().pagination.pageIndex ? '' : 'hidden @md:flex'
]"
                    :value="page.value"
                  >
                    {{ page.value }}
                  </Pagination.ListItem>
                  <Pagination.Ellipsis
                    v-else
                    class="w-7 h-7 items-center justify-center text-zinc-400 hidden @md:flex"
                  >
                    &#8230;
                  </Pagination.Ellipsis>
                </template>

                <Pagination.Next
                  :class="[
  'w-7 h-7 flex items-center justify-center bg-transparent hover:bg-zinc-100 transition disabled:opacity-50 rounded text-zinc-600',
  'data-disabled:opacity-50 data-disabled:cursor-not-allowed cursor-pointer'
]"
                >
                  <Icon
                    icon="radix-icons:chevron-right"
                    class="w-4 h-4"
                  />
                </Pagination.Next>
                <Pagination.Last
                  :class="[
  'w-7 h-7 flex items-center justify-center bg-transparent hover:bg-zinc-100 transition disabled:opacity-50 rounded text-zinc-600',
  'data-disabled:opacity-50 data-disabled:cursor-not-allowed cursor-pointer'
]"
                >
                  <Icon
                    icon="radix-icons:double-arrow-right"
                    class="w-4 h-4"
                  />
                </Pagination.Last>
              </Pagination.List>
            </Pagination.Root>

            <!-- Page jump -->
            <div class="text-xs items-center gap-2 hidden @lg:flex">
              {{ t("goto", { page: "" }) }}
              <NumberField
                :model-value="tableApi.getState().pagination.pageIndex + 1"
                :min="1"
                :max="maxPage"
                @update:model-value="(value) => tableApi.setPageIndex(value - 1)"
              >
                <NumberFieldInput class="h-6 w-16 text-xs" />
              </NumberField>
            </div>
          </div>

          <!-- Right markdown slot -->
          <div class="relative p-1 flex-1 prose prose-zinc text-xs text-right invisible @lg:visible">
            <Markdown :source="paginationRightSource" />
            &nbsp;
          </div>
        </div>
      </ClientOnly>
    </div>
  </div>
</template>
