<template>
  <div
    class="vm-crud-table"
    :class="{
      'is-border': border,
      'is-auto-height': autoHeight,
    }"
  >
    <div v-if="crud.loading.value" class="vm-crud-table__loading">
      {{ crudLabel('loading', crud.dict.label) }}
    </div>
    <div ref="scrollRef" class="vm-crud-table__scroll vm-scroll">
      <Table>
        <TableHeader>
          <TableRow class="vm-crud-table__head-row">
            <TableHead v-if="showSelection" class="vm-crud-table__th vm-crud-table__check">
              <Checkbox :model-value="allChecked" @update:model-value="toggleAll" />
            </TableHead>
            <TableHead
              v-for="(col, i) in dataColumns"
              :key="col.prop || col.type || i"
              class="vm-crud-table__th"
              :class="{
                'vm-crud-table__op': col.type === 'op',
                'is-sortable': isSortable(col),
                'is-fixed-right': colFixed(col) === 'right',
                'is-fixed-left': colFixed(col) === 'left',
              }"
              :style="colStyle(col, i)"
              @click="onSortClick(col)"
            >
              <span class="vm-crud-table__th-inner">
                {{ columnLabel(col) }}
                <i
                  v-if="isSortable(col) && col.prop"
                  class="vm-crud-table__sort"
                  :class="sortIconClass(col.prop)"
                />
              </span>
            </TableHead>
            <TableHead
              v-if="showSlotOp"
              class="vm-crud-table__th vm-crud-table__op"
              :style="{ width: `${opWidth}px`, minWidth: `${opWidth}px` }"
            >
              {{ crudLabel('op', crud.dict.label) }}
            </TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          <!-- empty-block：表体内 sticky，宽度=可视区，不盖表头、不跟总表宽跑 -->
          <TableRow
            v-if="!displayRows.length && !crud.loading.value"
            class="vm-crud-table__empty-row"
          >
            <TableCell :colspan="colSpan" class="vm-crud-table__empty-cell">
              <div class="vm-crud-table__empty-block" :style="emptyBlockStyle">
                <vm-empty kind="暂无数据" />
              </div>
            </TableCell>
          </TableRow>
          <TableRow
            v-for="(row, rowIndex) in displayRows"
            :key="vueRowKey(row, rowIndex)"
            class="vm-crud-table__row"
            :class="{ 'is-current': highlight && String(row[rowKey]) === currentId }"
            @click="onRowClick(row)"
            @contextmenu="(e: MouseEvent) => onContextMenu(e, row)"
          >
          <TableCell v-if="showSelection" class="vm-crud-table__check">
            <Checkbox
              :model-value="selectedIds.has(String(row[rowKey]))"
              @update:model-value="(v) => toggleRow(row, v)"
            />
          </TableCell>
          <TableCell
            v-for="(col, i) in dataColumns"
            :key="col.prop || col.type || i"
            class="vm-crud-table__td"
            :class="{
              'vm-crud-table__op': col.type === 'op',
              'is-fixed-right': colFixed(col) === 'right',
              'is-fixed-left': colFixed(col) === 'left',
            }"
            :style="colStyle(col, i)"
          >
            <template v-if="col.type === 'index'">
              {{ displayIndex(rowIndex) }}
            </template>
            <template v-else-if="col.type === 'op'">
              <div
                v-if="crud.trashMode.value === 'onlyTrashed'"
                class="vm-crud-table__ops"
              >
                <Button
                  v-if="crud.getPermission('restore')"
                  size="xs"
                  variant="softPrimary"
                  @click="crud.rowRestore([row])"
                >
                  {{ crudLabel('restore', crud.dict.label) }}
                </Button>
                <Button
                  v-if="crud.getPermission('delete')"
                  size="xs"
                  variant="softDestructive"
                  @click="crud.rowDelete(deleteRowTargets(row), true)"
                >
                  {{ crudLabel('forceDelete', crud.dict.label) }}
                </Button>
              </div>
              <div
                v-else-if="crud.trashMode.value === 'dataI18n'"
                class="vm-crud-table__ops"
              >
                <Button
                  type="button"
                  size="xs"
                  variant="softPrimary"
                  @click.stop="openDataI18nEdit(row)"
                >
                  {{ crudLabel('update', crud.dict.label) }}
                </Button>
                <Button
                  type="button"
                  size="xs"
                  variant="softDestructive"
                  @click.stop="deleteDataI18nEntry(row)"
                >
                  {{ crudLabel('delete', crud.dict.label) }}
                </Button>
              </div>
              <div v-else class="vm-crud-table__ops">
                <template v-for="(btn, bi) in resolveButtons(col, row)" :key="bi">
                  <Button
                    v-if="btn === 'edit' && crud.getPermission('update')"
                    type="button"
                    size="xs"
                    variant="softPrimary"
                    @click.stop="crud.rowEdit(row)"
                  >
                    {{ crudLabel('update', crud.dict.label) }}
                  </Button>
                  <Button
                    v-else-if="btn === 'delete' && crud.getPermission('delete')"
                    type="button"
                    size="xs"
                    variant="softDestructive"
                    @click.stop="crud.rowDelete(deleteRowTargets(row))"
                  >
                    {{ crudLabel('delete', crud.dict.label) }}
                  </Button>
                  <Button
                    v-else-if="btn === 'info' && crud.getPermission('info')"
                    type="button"
                    size="xs"
                    variant="ghost"
                    @click.stop="crud.rowInfo(row)"
                  >
                    {{ crudLabel('info', crud.dict.label) }}
                  </Button>
                  <Button
                    v-else-if="typeof btn === 'object' && !btn.hidden"
                    type="button"
                    size="xs"
                    :disabled="btn.disabled"
                    :variant="btn.variant || 'softSuccess'"
                    @click.stop="btn.onClick(row)"
                  >
                    {{ btn.label }}
                  </Button>
                </template>
              </div>
            </template>
            <div
              v-else-if="isTree && i === 0"
              class="vm-crud-table__tree-cell"
              :style="{ paddingLeft: `${(row.depth || 0) * 18}px` }"
            >
              <button
                v-if="row.hasChildren"
                type="button"
                class="vm-crud-table__fold"
                @click="toggleExpand(row[rowKey] as string | number)"
              >
                <i
                  :class="
                    expanded.has(row[rowKey] as string | number)
                      ? 'ri-arrow-down-s-line'
                      : 'ri-arrow-right-s-line'
                  "
                />
              </button>
              <span v-else class="vm-crud-table__fold is-spacer" />
              <slot
                :name="col.slot || `cell-${col.prop}`"
                :row="row"
                :value="col.prop ? row[col.prop] : undefined"
              >
                <component
                  :is="cellComp(col)"
                  v-if="cellComp(col)"
                  :model-value="col.prop ? row[col.prop] : undefined"
                  :scope="row"
                  :column="{ property: col.prop, prop: col.prop }"
                  :options="resolveCellOptions(col)"
                  v-bind="resolveCellProps(col)"
                />
                <vm-dict-tag
                  v-else-if="resolveDict(col).length"
                  :model-value="col.prop ? (row[col.prop] as string | number) : null"
                  :options="resolveDict(col)"
                />
                <template v-else>{{ cellText(col, row) }}</template>
              </slot>
            </div>
            <slot
              v-else
              :name="col.slot || `cell-${col.prop}`"
              :row="row"
              :value="col.prop ? row[col.prop] : undefined"
            >
              <component
                :is="cellComp(col)"
                v-if="cellComp(col)"
                :model-value="col.prop ? row[col.prop] : undefined"
                :scope="row"
                :column="{ property: col.prop, prop: col.prop }"
                :options="resolveCellOptions(col)"
                v-bind="resolveCellProps(col)"
              />
              <vm-dict-tag
                v-else-if="resolveDict(col).length"
                :model-value="col.prop ? (row[col.prop] as string | number) : null"
                :options="resolveDict(col)"
              />
              <template v-else>{{ cellText(col, row) }}</template>
            </slot>
          </TableCell>
          <TableCell v-if="showSlotOp" class="vm-crud-table__td">
            <div
              v-if="crud.trashMode.value === 'onlyTrashed'"
              class="vm-crud-table__ops"
            >
              <Button
                v-if="crud.getPermission('restore')"
                size="xs"
                variant="softPrimary"
                @click="crud.rowRestore([row])"
              >
                {{ crudLabel('restore', crud.dict.label) }}
              </Button>
              <Button
                v-if="crud.getPermission('delete')"
                size="xs"
                variant="softDestructive"
                @click="crud.rowDelete(deleteRowTargets(row), true)"
              >
                {{ crudLabel('forceDelete', crud.dict.label) }}
              </Button>
            </div>
            <slot v-else name="op" :row="row" />
          </TableCell>
        </TableRow>
      </TableBody>
    </Table>
    </div>
    <vm-context-menu ref="ctxRef" :items="ctxItems" @select="onCtxSelect" />

    <Dialog :open="dataI18nEditOpen" @update:open="onDataI18nEditOpenChange">
      <DialogContent
        shell="crud"
        :show-close-button="false"
        class="vm-data-i18n-edit__dialog"
        @pointer-down-outside="preventDialogOutsideClose"
        @focus-outside="preventDialogOutsideClose"
        @interact-outside="preventDialogOutsideClose"
      >
        <DialogHeader class="vm-crud-shell__head">
          <DialogTitle class="vm-crud-shell__title">
            {{ crudLabel('dataI18nEdit', crud.dict.label) }}
          </DialogTitle>
          <DialogClose class="vm-crud-shell__close">
            <i class="ri-close-line" />
            <span class="sr-only">Close</span>
          </DialogClose>
        </DialogHeader>
        <div class="vm-crud-shell__body vm-data-i18n-edit__body">
          <div
            class="vm-data-i18n-edit__field"
            :style="{ gridColumn: `span ${dataI18nEditSpan}` }"
          >
            <Label class="vm-data-i18n-edit__label">ID</Label>
            <Input
              class="vm-data-i18n-edit__control is-readonly"
              :model-value="String(dataI18nEditForm.id ?? '')"
              disabled
            />
          </div>
          <div
            v-for="f in dataI18nEditFields"
            :key="f.name"
            class="vm-data-i18n-edit__field"
            :style="{ gridColumn: `span ${dataI18nEditSpan}` }"
          >
            <Label class="vm-data-i18n-edit__label">{{ f.name }}</Label>
            <Input
              class="vm-data-i18n-edit__control"
              v-model="dataI18nEditForm.values[f.name]"
            />
          </div>
        </div>
        <DialogFooter class="vm-crud-shell__foot">
          <button
            type="button"
            class="vm-crud-shell__btn is-ghost"
            :disabled="dataI18nEditSubmitting"
            @click="dataI18nEditOpen = false"
          >
            {{ crudLabel('close', crud.dict.label) }}
          </button>
          <button
            type="button"
            class="vm-crud-shell__btn is-primary"
            :disabled="dataI18nEditSubmitting"
            @click="saveDataI18nEdit"
          >
            {{
              dataI18nEditSubmitting
                ? crudLabel('saving', crud.dict.label)
                : crudLabel('save', crud.dict.label)
            }}
          </button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch, onMounted, onBeforeUnmount, provide, useSlots, toValue } from 'vue'
import { toast } from 'vue-sonner'
import { useCrud } from './useCrud'
import { injectTableOptions, TABLE_API_KEY } from './key'
import { flattenTree } from '../lib/tree'
import { resolveCrudComponent } from './registry'
import { getCrudStyle, crudLabel, crudT, crudFormat, crudLocaleRev } from './style'
import { findEpsEntity } from '../lib/eps'
import { useVome } from '../hooks/useVome'
import { vmConfirm } from './confirm'
import { preventDialogOutsideClose } from '../lib/dialog-float'
import { resolveFormSpan, useFormCols } from './span'
import {
  canUseDataI18n,
  dataPackFromService,
} from './data-i18n'
import { buildAutoTableColumns, mergeByProp } from './auto-fields'
import type {
  CrudColumn,
  CrudOpButton,
  CtxMenuItem,
} from '../../../typings/admin/comm/crud'
defineOptions({ name: 'vm-table' })

const props = withDefaults(
  defineProps<{
    columns?: CrudColumn[]
    selection?: boolean
    rowKey?: string
  }>(),
  {
    selection: undefined,
    rowKey: 'id',
  },
)

const slots = useSlots()
const crud = useCrud()
const tableOpts = injectTableOptions()
const style = getCrudStyle().table

const scrollRef = ref<HTMLElement | null>(null)
/** empty-block：宽度取滚动容器可视宽，不跟表内容总宽 */
const emptyWidth = ref(0)

function syncEmptyWidth() {
  const el = scrollRef.value
  if (!el) return
  emptyWidth.value = el.clientWidth
}

const emptyBlockStyle = computed(() => ({
  width: emptyWidth.value ? `${emptyWidth.value}px` : '100%',
}))

let emptyRo: ResizeObserver | null = null

const overrideColumns = ref<CrudColumn[] | null>(null)

const DATA_I18N_ID_COL: CrudColumn = {
  prop: 'id',
  label: 'ID',
  minWidth: 100,
}

const DATA_I18N_OP_COL: CrudColumn = {
  type: 'op',
  buttons: ['edit', 'delete'],
  width: 140,
}

const isDataI18nView = computed(() => crud.trashMode.value === 'dataI18n')

/** 语言 Tab：按表头/当前页内容估 minWidth（限幅） */
function dataI18nColMinWidth(header: string, samples: string[]) {
  let maxChars = [...header].length
  for (const s of samples) {
    const n = [...String(s || '')].length
    if (n > maxChars) maxChars = n
  }
  return Math.min(360, Math.max(108, maxChars * 14 + 28))
}

function resolveManualColumns(): CrudColumn[] {
  return props.columns?.length ? props.columns : tableOpts?.columns || []
}

function resolveMergedColumns(): CrudColumn[] {
  const manual = resolveManualColumns()
  const tablePlugins = (getCrudStyle().table.plugins || []) as Array<{
    __plugin?: string
    ignoreFields?: string[]
  }>
  const autoPlugin = tablePlugins.find((p) => p?.__plugin === 'setAuto')
  const autoEnabled = tableOpts?.auto !== false && Boolean(autoPlugin)
  if (!autoEnabled) return manual
  const pluginIgnore = autoPlugin?.ignoreFields || []
  const pageIgnore = tableOpts?.ignoreFields || []
  const auto = buildAutoTableColumns(crud.service, {
    ignoreFields: [...pluginIgnore, ...pageIgnore],
  })
  return mergeByProp(auto, manual)
}

const columns = computed((): CrudColumn[] => {
  if (isDataI18nView.value) {
    void crudLocaleRev.value
    const fields = crud.dataI18nFields?.value || []
    const rows = crud.list.value || []
    const origSuffix = crudLabel('dataI18nOrigSuffix', crud.dict.label)
    const fieldCols: CrudColumn[] = []
    for (const f of fields) {
      const fieldName = f.name
      const srcProp = `__src_${fieldName}`
      const srcHeader = `${fieldName}${origSuffix}`
      const srcSamples = rows.map((r) => String(r[srcProp] ?? ''))
      const valSamples = rows.map((r) => String(r[fieldName] ?? ''))
      fieldCols.push({
        prop: srcProp,
        label: srcHeader,
        minWidth: dataI18nColMinWidth(srcHeader, srcSamples),
      })
      fieldCols.push({
        prop: fieldName,
        label: fieldName,
        minWidth: dataI18nColMinWidth(fieldName, valSamples),
      })
    }
    return [DATA_I18N_ID_COL, ...fieldCols, DATA_I18N_OP_COL]
  }
  const raw = overrideColumns.value || resolveMergedColumns()
  return raw.filter((c) => !c.hidden)
})

const hasSelectionCol = computed(() => columns.value.some((c) => c.type === 'selection'))
const hasOpCol = computed(() => columns.value.some((c) => c.type === 'op'))
const showSelection = computed(() => {
  if (isDataI18nView.value) return false
  return props.selection !== undefined ? props.selection : hasSelectionCol.value
})
const dataColumns = computed(() =>
  columns.value.filter((c) => c.type !== 'selection'),
)
const showSlotOp = computed(
  () => Boolean(slots.op) && !hasOpCol.value && !isDataI18nView.value,
)

/** EPS 列 → field.{table}.{prop} */
function epsFieldKey(prop: string): string | undefined {
  const ns = (crud.service as { namespace?: string } | undefined)?.namespace
  if (!ns || !prop) return undefined
  const entity = findEpsEntity(`/${ns}`)
  if (!entity) return undefined
  const cols = [
    ...((entity as { columns?: Array<{ propertyName?: string; table?: string; source?: string }> }).columns || []),
    ...((entity as { pageColumns?: Array<{ propertyName?: string; table?: string; source?: string }> }).pageColumns || []),
  ]
  const meta =
    cols.find((c) => c.propertyName === prop) ||
    cols.find((c) => c.source === `a.${prop}`) ||
    cols.find((c) => c.source?.endsWith(`.${prop}`))
  const table = meta?.table?.trim()
  const name = (meta?.propertyName || prop).trim()
  if (!table || !name) return undefined
  return `${table}.${name}`
}

/** 表头文案：labelKey → field.* → label / op；语言 Tab 用实体字段名，不走 field.* */
function columnLabel(col: CrudColumn): string {
  void crudLocaleRev.value
  if (col.type === 'op') {
    return col.labelKey
      ? crudT(col.labelKey, col.label || crudLabel('op', crud.dict.label))
      : col.label || crudLabel('op', crud.dict.label)
  }
  const fb = col.label || col.prop || ''
  if (isDataI18nView.value && col.prop && col.prop !== 'id') {
    return fb
  }
  if (col.labelKey) return crudT(col.labelKey, fb)
  const fk = col.fieldKey || (col.prop ? epsFieldKey(col.prop) : undefined)
  if (fk) return crudT(`field.${fk}`, fb)
  return fb
}

const colSpan = computed(
  () =>
    dataColumns.value.length +
    (showSelection.value ? 1 : 0) +
    (showSlotOp.value ? 1 : 0),
)

const rowKey = computed(() =>
  isDataI18nView.value ? '__rowKey' : tableOpts?.rowKey || props.rowKey,
)

/** 行 Vue key：缺 rowKey 时用下标兜底，避免 Duplicate keys "undefined" */
function vueRowKey(row: Record<string, unknown>, index: number) {
  const k = row[rowKey.value]
  if (k != null && String(k) !== '') return String(k)
  return `idx-${index}`
}

const isTree = computed(() => Boolean(tableOpts?.tree))
const border = computed(() => tableOpts?.border ?? style.border)
const autoHeight = computed(() => tableOpts?.autoHeight ?? style.autoHeight)
const highlight = computed(() => style.highlightCurrentRow)
const opWidth = computed(() => style.column.opWidth)
const defaultAlign = computed(() => style.column.align)
const sortRefresh = computed(() => tableOpts?.sortRefresh !== false)

function normalizeSortDir(
  order?: string,
): 'asc' | 'desc' | undefined {
  if (order === 'asc' || order === 'ascending') return 'asc'
  if (order === 'desc' || order === 'descending') return 'desc'
  return undefined
}

function initSortState() {
  const ds = tableOpts?.defaultSort
  if (!ds?.prop) return null
  return {
    prop: ds.prop,
    order: normalizeSortDir(ds.order) || ('asc' as const),
  }
}

const sortState = ref<{ prop: string; order: 'asc' | 'desc' } | null>(
  initSortState(),
)

/** dict.sort → 本项目为 { order: 字段, sort: 方向 } */
function sortPayload(prop: string, order: 'asc' | 'desc') {
  return {
    [crud.dict.sort.prop]: prop,
    [crud.dict.sort.order]: order,
  }
}

function isSortable(col: CrudColumn) {
  if (!col.prop || col.type === 'op' || col.type === 'selection' || col.type === 'index') {
    return false
  }
  return Boolean(col.sortable)
}

function sortIconClass(prop: string) {
  if (sortState.value?.prop !== prop) return 'ri-arrow-up-down-line is-muted'
  return sortState.value.order === 'asc'
    ? 'ri-arrow-up-s-fill is-active'
    : 'ri-arrow-down-s-fill is-active'
}

function displayIndex(idx: string | number) {
  return Number(idx) + 1
}

function onSortClick(col: CrudColumn) {
  if (!isSortable(col) || !col.prop) return
  const prop = col.prop
  let next: 'asc' | 'desc' = 'asc'
  const current = sortState.value
  if (current && current.prop === prop) {
    next = current.order === 'asc' ? 'desc' : 'asc'
  } else {
    const hint = normalizeSortDir(
      typeof col.sortable === 'string' ? col.sortable : undefined,
    )
    next = hint || 'asc'
  }
  sortState.value = { prop, order: next }
  if (sortRefresh.value) void crud.refresh(sortPayload(prop, next))
}

/** col.width / col.minWidth 优先；op 列未设 width 时回退全局 opWidth */
function toPx(v: string | number) {
  return typeof v === 'number' ? `${v}px` : v
}

function colFixed(col: CrudColumn): 'left' | 'right' | undefined {
  if (col.fixed === 'left' || col.fixed === 'right') return col.fixed
  if (col.type === 'op') return 'right'
  return undefined
}

function colWidthPx(col: CrudColumn): number {
  if (col.type === 'op') {
    const w = col.width ?? opWidth.value
    return typeof w === 'number' ? w : Number.parseInt(String(w), 10) || 140
  }
  if (col.width != null) {
    return typeof col.width === 'number'
      ? col.width
      : Number.parseInt(String(col.width), 10) || 100
  }
  if (col.minWidth != null) {
    return typeof col.minWidth === 'number'
      ? col.minWidth
      : Number.parseInt(String(col.minWidth), 10) || 100
  }
  return 100
}

/** 自右向左累加 sticky right 偏移（多列 fixed:right 堆叠） */
const fixedRightOffset = computed(() => {
  const map = new Map<number, number>()
  let acc = 0
  const cols = dataColumns.value
  for (let i = cols.length - 1; i >= 0; i--) {
    if (colFixed(cols[i]!) !== 'right') continue
    map.set(i, acc)
    acc += colWidthPx(cols[i]!)
  }
  return map
})

function colStyle(col: CrudColumn, index?: number) {
  const base: Record<string, string | undefined> = {
    textAlign: (col.align || defaultAlign.value) as string | undefined,
  }
  if (col.type === 'op') {
    const w = col.width ?? opWidth.value
    base.width = toPx(w)
    base.minWidth = toPx(w)
    base.whiteSpace = 'nowrap'
  } else {
    if (col.width != null) base.width = toPx(col.width)
    if (col.minWidth != null) base.minWidth = toPx(col.minWidth)
  }
  const fixed = colFixed(col)
  if (fixed === 'right' && index != null) {
    base.position = 'sticky'
    base.right = `${fixedRightOffset.value.get(index) ?? 0}px`
    base.zIndex = col.type === 'op' ? '4' : '3'
  } else if (fixed === 'left') {
    base.position = 'sticky'
    base.left = '0'
    base.zIndex = '3'
  }
  return base
}
const ctxEnabled = computed(() => {
  if (isDataI18nView.value) return false
  const c = tableOpts?.contextMenu ?? style.contextMenu
  return c !== false && !(Array.isArray(c) && c.length === 0)
})

const expanded = ref(new Set<string | number>())
const currentId = ref('')
const ctxRef = ref<{ open: (e: MouseEvent) => void } | null>(null)
const ctxRow = ref<Record<string, unknown> | null>(null)

const ctxItems = computed<CtxMenuItem[]>(() => {
  const conf = tableOpts?.contextMenu ?? style.contextMenu
  const keys = Array.isArray(conf)
    ? conf
    : ['refresh', 'check', 'edit', 'delete', 'order-asc', 'order-desc']
  const map: Record<string, string> = {
    refresh: crudLabel('refresh', crud.dict.label),
    check: crudLabel('select', crud.dict.label),
    edit: crudLabel('update', crud.dict.label),
    delete: crudLabel('delete', crud.dict.label),
    'order-asc': crudLabel('asc', crud.dict.label),
    'order-desc': crudLabel('desc', crud.dict.label),
  }
  return keys.map((k) => ({
    key: k,
    label: map[k] || k,
    disabled:
      (k === 'edit' && !crud.getPermission('update')) ||
      (k === 'delete' && !crud.getPermission('delete')),
  }))
})

/** 树表默认仅展示一级（不展开）；defaultExpandAll / defaultExpandLevel 可覆盖 */
watch(
  () => crud.list.value,
  (rows) => {
    if (!isTree.value || expanded.value.size) return
    const next = new Set<string | number>()
    if (tableOpts?.defaultExpandAll) {
      for (const r of rows) {
        if (Array.isArray(r.children) && (r.children as unknown[]).length) {
          next.add(r[rowKey.value] as string | number)
          const stack = [...(r.children as Record<string, unknown>[])]
          while (stack.length) {
            const n = stack.pop()!
            if (Array.isArray(n.children) && (n.children as unknown[]).length) {
              next.add(n[rowKey.value] as string | number)
              stack.push(...(n.children as Record<string, unknown>[]))
            }
          }
        }
      }
    } else {
      const level = Math.max(1, tableOpts?.defaultExpandLevel ?? 1)
      const walk = (nodes: Record<string, unknown>[], depth: number) => {
        if (depth >= level - 1) return
        for (const n of nodes) {
          const kids = n.children as Record<string, unknown>[] | undefined
          if (!kids?.length) continue
          next.add(n[rowKey.value] as string | number)
          walk(kids, depth + 1)
        }
      }
      walk(rows, 0)
    }
    if (next.size) expanded.value = next
  },
  { immediate: true },
)

type FlatRow = Record<string, unknown> & {
  depth?: number
  hasChildren?: boolean
}

const displayRows = computed<FlatRow[]>(() => {
  const rows = crud.list.value as FlatRow[]
  if (!isTree.value) return rows
  return flattenTree(rows as never, expanded.value) as FlatRow[]
})

const selectedIds = computed(
  () => new Set(crud.selection.value.map((r) => String(r[rowKey.value]))),
)

const allChecked = computed(() => {
  const rows = displayRows.value
  return (
    rows.length > 0 &&
    rows.every((r) => selectedIds.value.has(String(r[rowKey.value])))
  )
})

/** 自身 + 全部子孙（树勾选 / 行删级联） */
function collectSubtree(row: Record<string, unknown>): Record<string, unknown>[] {
  const out: Record<string, unknown>[] = [row]
  const kids = row.children as Record<string, unknown>[] | undefined
  if (!kids?.length) return out
  for (const c of kids) out.push(...collectSubtree(c))
  return out
}

/** 树表行删带上子集；普通表仍只删当前行 */
function deleteRowTargets(row: Record<string, unknown>) {
  return isTree.value ? collectSubtree(row) : [row]
}

const { service: hostService } = useVome()
const dataI18nEditOpen = ref(false)
const dataI18nEditSubmitting = ref(false)
const dataI18nEditForm = ref<{
  id: string
  values: Record<string, string>
  sources: Record<string, string>
}>({ id: '', values: {}, sources: {} })
const dataI18nEditFormCols = useFormCols()
const dataI18nEditSpan = computed(() =>
  resolveFormSpan(12, dataI18nEditFormCols.value),
)
const dataI18nEditFields = computed(
  () => crud.dataI18nFields?.value || [],
)

function onDataI18nEditOpenChange(v: boolean) {
  dataI18nEditOpen.value = v
}

function openDataI18nEdit(row: Record<string, unknown>) {
  const fields = crud.dataI18nFields?.value || []
  const sources = (row.__sources as Record<string, string>) || {}
  const values: Record<string, string> = {}
  const src: Record<string, string> = {}
  for (const f of fields) {
    values[f.name] = String(row[f.name] ?? '')
    src[f.name] = String(sources[f.name] ?? '')
  }
  dataI18nEditForm.value = {
    id: String(row.id ?? ''),
    values,
    sources: src,
  }
  dataI18nEditOpen.value = true
}

async function saveDataI18nEdit() {
  if (dataI18nEditSubmitting.value) return
  const meta = canUseDataI18n(crud.service, hostService)
  const dataPack = dataPackFromService(hostService)
  const lang = crud.dataI18nLangCode?.value
  if (!meta.ok || !dataPack?.updateEntry || !lang) {
    toast.error(crudLabel('dataI18nNoService', crud.dict.label))
    return
  }
  dataI18nEditSubmitting.value = true
  try {
    await dataPack.updateEntry({
      tableName: meta.tableName,
      langCode: lang,
      id: dataI18nEditForm.value.id,
      values: { ...dataI18nEditForm.value.values },
    })
    toast.success(crudLabel('saveSuccess', crud.dict.label))
    dataI18nEditOpen.value = false
    await crud.refresh()
  } catch (e) {
    toast.error(e instanceof Error ? e.message : crudLabel('saveFailed'))
  } finally {
    dataI18nEditSubmitting.value = false
  }
}

async function deleteDataI18nEntry(row: Record<string, unknown>) {
  const meta = canUseDataI18n(crud.service, hostService)
  const dataPack = dataPackFromService(hostService)
  const lang = crud.dataI18nLangCode?.value
  if (!meta.ok || !dataPack?.deleteEntry || !lang) {
    toast.error(crudLabel('dataI18nNoService', crud.dict.label))
    return
  }
  const id = String(row.id ?? '')
  if (!id) return
  const ok = await vmConfirm({
    title: crudLabel('tips', crud.dict.label),
    message: crudFormat(
      crudLabel('dataI18nDeleteConfirm', crud.dict.label),
      id,
    ),
    confirmText: crudLabel('confirm', crud.dict.label),
    cancelText: crudLabel('close', crud.dict.label),
  })
  if (!ok) return
  try {
    await dataPack.deleteEntry({
      tableName: meta.tableName,
      langCode: lang,
      id,
    })
    toast.success(crudLabel('deleteSuccess', crud.dict.label))
    await crud.refresh()
  } catch (e) {
    toast.error(e instanceof Error ? e.message : crudLabel('deleteFailed'))
  }
}

function toggleAll(checked: boolean | 'indeterminate') {
  if (checked === true) {
    // 树表：含折叠未展示的子孙
    if (isTree.value) {
      const all: Record<string, unknown>[] = []
      for (const r of crud.list.value) all.push(...collectSubtree(r))
      crud.setSelection(all)
      return
    }
    crud.setSelection([...displayRows.value])
  } else {
    crud.setSelection([])
  }
}

function toggleRow(row: Record<string, unknown>, checked: boolean | 'indeterminate') {
  const key = rowKey.value
  if (isTree.value) {
    const subtree = collectSubtree(row)
    const ids = new Set(subtree.map((r) => String(r[key])))
    if (checked === true) {
      const map = new Map(
        crud.selection.value.map((r) => [String(r[key]), r] as const),
      )
      for (const r of subtree) map.set(String(r[key]), r)
      crud.setSelection([...map.values()])
    } else {
      crud.setSelection(
        crud.selection.value.filter((r) => !ids.has(String(r[key]))),
      )
    }
    return
  }
  const id = String(row[key])
  if (checked === true) {
    if (!selectedIds.value.has(id)) crud.setSelection([...crud.selection.value, row])
  } else {
    crud.setSelection(
      crud.selection.value.filter((r) => String(r[key]) !== id),
    )
  }
}

function toggleExpand(id: string | number) {
  const next = new Set(expanded.value)
  if (next.has(id)) next.delete(id)
  else next.add(id)
  expanded.value = next
}

function resolveButtons(col: CrudColumn, row: Record<string, unknown>): CrudOpButton[] {
  const raw = col.buttons
  if (typeof raw === 'function') return raw(row)
  return raw?.length ? raw : (['edit', 'delete'] as CrudOpButton[])
}

function cellComp(col: CrudColumn) {
  return resolveCrudComponent(col.component?.name)
}

/** dict: dict.options('x') → 彩色标签；props 可为 dict.get(...) → switch 等 */
function resolveDict(col: CrudColumn) {
  const list = col.dict == null ? [] : toValue(col.dict)
  return Array.isArray(list) ? list : []
}

/** props 可为 dict.get(...)（ComputedRef / 数组）→ 作 options */
function resolveCellOptions(col: CrudColumn) {
  const fromDict = resolveDict(col)
  if (fromDict.length) return fromDict
  const raw = toValue(col.component?.props)
  return Array.isArray(raw) ? raw : []
}

function resolveCellProps(col: CrudColumn) {
  const raw = col.component?.props
  const val = toValue(raw)
  if (val == null || Array.isArray(val)) return {}
  if (typeof val === 'object') return val as Record<string, unknown>
  return {}
}

function cellText(col: CrudColumn, row: Record<string, unknown>) {
  if (!col.prop) return ''
  const val = row[col.prop]
  if (col.formatter) return col.formatter(row, val)
  if (val == null) return ''
  return String(val)
}

function onRowClick(row: Record<string, unknown>) {
  if (highlight.value) currentId.value = String(row[rowKey.value])
}

function onContextMenu(e: MouseEvent, row: Record<string, unknown>) {
  if (!ctxEnabled.value) return
  ctxRow.value = row
  ctxRef.value?.open(e)
}

function onCtxSelect(key: string) {
  const row = ctxRow.value
  if (!row) return
  if (key === 'refresh') void crud.refresh()
  if (key === 'check') toggleRow(row, true)
  if (key === 'edit') void crud.rowEdit(row)
  if (key === 'delete') void crud.rowDelete(deleteRowTargets(row))
  if (key === 'order-asc') {
    sortState.value = { prop: 'orderNum', order: 'asc' }
    void crud.refresh(sortPayload('orderNum', 'asc'))
  }
  if (key === 'order-desc') {
    sortState.value = { prop: 'orderNum', order: 'desc' }
    void crud.refresh(sortPayload('orderNum', 'desc'))
  }
}

function baseColumns(): CrudColumn[] {
  return overrideColumns.value || resolveMergedColumns()
}

function setColumns(cols: CrudColumn[]) {
  overrideColumns.value = cols
}

function showColumn(prop: string) {
  setColumns(
    baseColumns().map((c) =>
      c.prop === prop ? { ...c, hidden: false } : c,
    ),
  )
}

function hideColumn(prop: string) {
  setColumns(
    baseColumns().map((c) =>
      c.prop === prop ? { ...c, hidden: true } : c,
    ),
  )
}

function getColumns() {
  return baseColumns()
}

function columnCustomKey() {
  return `column-custom__${typeof location !== 'undefined' ? location.pathname : 'default'}`
}

function applyColumnCustomMap(
  cols: CrudColumn[],
  map: Record<string, { checked: boolean; order?: number }>,
) {
  const ordered = [...cols].sort((a, b) => {
    const ao = a.prop != null ? map[a.prop]?.order ?? 999 : 999
    const bo = b.prop != null ? map[b.prop]?.order ?? 999 : 999
    return ao - bo
  })
  return ordered.map((c) => {
    if (!c.prop || c.type === 'selection' || c.type === 'op') return c
    return { ...c, hidden: !(map[c.prop]?.checked ?? true) }
  })
}

onMounted(() => {
  syncEmptyWidth()
  if (typeof ResizeObserver !== 'undefined' && scrollRef.value) {
    emptyRo = new ResizeObserver(() => syncEmptyWidth())
    emptyRo.observe(scrollRef.value)
  }

  try {
    const raw = localStorage.getItem(columnCustomKey())
    if (!raw) return
    const map = JSON.parse(raw) as Record<
      string,
      { checked: boolean; order?: number }
    >
    const base = resolveMergedColumns()
    if (!base.length) return
    overrideColumns.value = applyColumnCustomMap(base, map)
  } catch {
    /* ignore */
  }
})

onBeforeUnmount(() => {
  emptyRo?.disconnect()
  emptyRo = null
})

watch(
  () => displayRows.value.length,
  () => {
    requestAnimationFrame(syncEmptyWidth)
  },
)

provide(TABLE_API_KEY, {
  setColumns,
  showColumn,
  hideColumn,
  getColumns,
})

defineExpose({
  showColumn,
  hideColumn,
  setColumns,
  data: displayRows,
})
</script>

<style lang="scss" scoped>
.vm-crud-table {
  position: relative;
  display: flex;
  width: 100%;
  min-width: 0;
  min-height: 0;
  flex: 1 1 auto;
  flex-direction: column;
  overflow: hidden;
  border-radius: 16px;

  &.is-border {
    border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
  }

  &.is-auto-height {
    min-height: 0;
  }
}

.vm-crud-table__scroll {
  width: 100%;
  min-width: 0;
  min-height: 0;
  flex: 1 1 auto;
  overflow: auto;
  overscroll-behavior: contain;
}

/* 表铺满容器；列 minWidth 过大时由 __scroll 横向滚动 */
:deep([data-slot='table-container']) {
  width: 100%;
  min-width: 100%;
  overflow: visible !important;
}

:deep([data-slot='table']) {
  width: 100%;
}

.vm-crud-table__loading {
  position: absolute;
  inset: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--card) 70%, transparent);
  color: var(--muted-foreground);
  font-size: 12px;
  backdrop-filter: blur(1px);
}

/* empty-block：在表体内，sticky 贴可视左缘，不盖表头 */
:deep(.vm-crud-table__empty-row) {
  &:hover {
    background: transparent !important;
  }
}

:deep(.vm-crud-table__empty-cell) {
  height: 240px;
  padding: 0 !important;
  border-bottom: none !important;
}

.vm-crud-table__empty-block {
  position: sticky;
  left: 0;
  display: flex;
  height: 240px;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

:deep(.vm-crud-table__head-row) {
  border-color: color-mix(in srgb, var(--border) 70%, transparent);
  background: var(--card);

  &:hover {
    background: var(--card) !important;
  }
}

/* 纵向滚动时表头固定；横向 fixed 列 z-index 更高 */
:deep(.vm-crud-table__th),
:deep(.vm-crud-table__head-row .vm-crud-table__check) {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--card);
}

:deep(.vm-crud-table__th.is-fixed-left) {
  z-index: 5;
}

:deep(.vm-crud-table__th.is-fixed-right) {
  z-index: 6;
}

:deep(.vm-crud-table__th),
:deep(.vm-crud-table__td) {
  &.is-fixed-right {
    box-shadow: -4px 0 8px color-mix(in srgb, var(--foreground) 6%, transparent);
  }

  &.is-fixed-left {
    box-shadow: 4px 0 8px color-mix(in srgb, var(--foreground) 6%, transparent);
  }
}

/* 表头 sticky 自带实底；正文 fixed 继承行底，禁止写死 --card 再靠 hover 改色盖住 */
:deep(.vm-crud-table__th.is-fixed-right),
:deep(.vm-crud-table__th.is-fixed-left) {
  background: var(--card);
}

:deep(.vm-crud-table__td.is-fixed-right),
:deep(.vm-crud-table__td.is-fixed-left) {
  background: inherit;
}

:deep(.vm-crud-table__th) {
  height: 42px;
  color: var(--muted-foreground);
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;

  &.is-sortable {
    cursor: pointer;
    user-select: none;

    &:hover {
      color: var(--foreground);
    }
  }
}

.vm-crud-table__th-inner {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.vm-crud-table__sort {
  font-size: 14px;
  line-height: 1;
  opacity: 0.45;

  &.is-active {
    color: var(--brand);
    opacity: 1;
  }

  &.is-muted {
    opacity: 0.35;
  }
}

.vm-crud-table__ops {
  display: inline-flex;
  flex-wrap: nowrap;
  gap: 8px;
  white-space: nowrap;
}

:deep(.vm-crud-table__op) {
  white-space: nowrap;
}

:deep(.vm-crud-table__check) {
  width: 40px;
  padding-left: 4px;
}

:deep(.vm-crud-table__row) {
  height: 48px;
  border-color: color-mix(in srgb, var(--border) 55%, transparent);
  /* 行自带实底，fixed td inherit 才不透横滚；高亮只改 tr，不改子节点色 */
  background: var(--card);
  transition: background 0.15s ease;

  &:hover,
  &.is-current {
    background: var(--brand-soft);
  }
}

:deep(.vm-crud-table__td) {
  padding-top: 10px;
  padding-bottom: 10px;
  font-size: 13px;
}

.vm-crud-table__tree-cell {
  display: flex;
  align-items: center;
  gap: 4px;
}

.vm-crud-table__fold {
  display: inline-flex;
  width: 22px;
  height: 22px;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 8px;
  background: transparent;
  color: var(--muted-foreground);
  cursor: pointer;

  &.is-spacer {
    cursor: default;
  }

  &:not(.is-spacer):hover {
    background: var(--muted);
    color: var(--foreground);
  }
}

.vm-data-i18n-edit__body {
  /* 与业务翻译弹窗一致：24 栅格 + span12；继承 shell body 的 flex:1 */
  display: grid;
  grid-template-columns: repeat(24, minmax(0, 1fr));
  gap: 12px 16px;
  align-content: start;
  padding: 18px 22px 16px;

  @media (max-width: 768px) {
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: 10px;
    padding: 12px;
  }
}

.vm-data-i18n-edit__field {
  min-width: 0;
}

.vm-data-i18n-edit__label {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  margin-bottom: 6px;
  font-size: 12px;
  color: var(--muted-foreground);
}

.vm-data-i18n-edit__req {
  color: var(--destructive);
}

.vm-data-i18n-edit__control {
  width: 100%;
  height: var(--vm-control-height) !important;
  min-height: var(--vm-control-height);
  border-radius: var(--vm-control-radius) !important;
  font-size: var(--vm-control-font-size) !important;

  &.is-readonly {
    background: var(--muted) !important;
    color: var(--muted-foreground) !important;
    opacity: 1 !important;
    cursor: default;
  }
}

.vm-data-i18n-edit__source {
  margin-top: 6px;
  font-size: 12px;
  line-height: 1.4;
  color: var(--muted-foreground);
  word-break: break-word;
}
</style>
