<template>
  <div class="bitable-grid-scope">
    <!-- U5: Unified section rendering — grouping disabled produces a single
         data section (node=null); grouping enabled produces interleaved
         header + data sections. The vxe-grid declaration + all slots are
         written ONCE here (no duplication). -->
    <template v-for="(section, idx) in groupSections" :key="`sec_${idx}`">
      <!-- Group header (grouping mode only) -->
      <div
        v-if="section.type === 'header'"
        class="bitable-grid-scope__group-header"
        :style="{ paddingLeft: `${section.node.depth * 24 + 8}px` }"
        @click="toggleGroup(section.node)"
      >
        <span class="bitable-grid-scope__caret">{{ isCollapsed(section.node) ? '▸' : '▾' }}</span>
        <span class="bitable-grid-scope__group-key">{{ section.node.key || '(空)' }}</span>
        <span class="bitable-grid-scope__group-count">{{ section.node.records.length }} 条</span>
        <template v-for="(agg, fid) in section.node.aggregations" :key="fid">
          <span class="bitable-grid-scope__group-agg">
            {{ fieldName(String(fid)) }}：<template v-if="agg.sum != null">合计 {{ formatNum(agg.sum) }}</template><template v-if="agg.avg != null"> · 均值 {{ formatNum(agg.avg) }}</template>
          </span>
        </template>
      </div>

      <!-- Data section: vxe-grid for this group's records (or all records) -->
      <div
        v-else
        v-show="section.node === null || !isCollapsed(section.node)"
        class="bitable-grid-scope__group-grid"
      >
        <vxe-grid
          :ref="(el: unknown) => onGridRef(idx, el)"
          :data="section.node ? rowsForGroup(section.node) : rows"
          :columns="gridColumns"
          :height="groupingEnabled ? 'auto' : height"
          :loading="loading"
          :row-config="rowConfig"
          :column-config="{ resizable: true }"
          :virtual-y-config="{ enabled: !groupingEnabled, gt: 60 }"
          :virtual-x-config="{ enabled: true, gt: 20 }"
          :edit-config="{
            trigger: 'click',
            mode: 'cell',
            showStatus: true,
            autoClear: false,
          }"
          @edit-closed="onEditClosed"
          @cell-click="onCellClick"
        >
          <template #empty>
            <a-empty :description="emptyText" />
          </template>
          <!-- Custom cell renderers for attachment/image fields (U6) -->
          <template
            v-for="f in attachmentFields"
            :key="f.id"
            #[`cell_${f.id}`]="{ row }"
          >
            <AttachmentCell
              v-if="f.field_type === 'attachment'"
              :files="(row[f.id] as IAttachmentMeta[] | null | undefined)"
            />
            <ImageCell
              v-else-if="f.field_type === 'image'"
              :images="(row[f.id] as IAttachmentMeta[] | null | undefined)"
            />
          </template>
          <!-- Column header dropdown menus (U4) + inline field config (U2) -->
          <template
            v-for="f in fields"
            :key="`hdr_${f.id}`"
            #[`header_${f.id}`]
          >
            <a-popover
              :open="editingFieldId === f.id"
              :trigger="[]"
              placement="bottomLeft"
              overlay-class-name="bitable-inline-config-popover"
              :overlay-style="{ width: '340px' }"
            >
              <ColumnHeaderMenu
                :field="f"
                @edit-inline="startInlineEdit(f.id)"
                @open-batch-panel="emit('config-field', $event)"
                @hide="emit('hide-field', $event)"
                @delete="emit('delete-field', $event)"
              />
              <template #content>
                <InlineFieldConfigurator
                  v-if="editingFieldId === f.id"
                  :field="f"
                  @saved="onInlineSaved"
                  @cancel="onInlineCancel"
                />
              </template>
            </a-popover>
          </template>
          <!-- Select / Multiselect edit slots (U5) -->
          <template
            v-for="f in selectFields"
            :key="`edit_${f.id}`"
            #[`edit_${f.id}`]="{ row }"
          >
            <SelectCellEditor
              :model-value="(row[f.id] as string | string[] | null | undefined)"
              :options="(f.config.options as ISelectOption[] | string[] | undefined)"
              :multiple="f.field_type === 'multiselect'"
              @update:model-value="row[f.id] = $event"
            />
          </template>
          <!-- Select / Multiselect display slots (U5) -->
          <template
            v-for="f in selectFields"
            :key="`cell_sel_${f.id}`"
            #[`cell_sel_${f.id}`]="{ row }"
          >
            <SelectDisplay
              :value="(row[f.id] as string | string[] | null | undefined)"
              :options="(f.config.options as ISelectOption[] | string[] | undefined)"
              :multiple="f.field_type === 'multiselect'"
            />
          </template>
          <!-- Add-field column header -->
          <template #header__add_field>
            <div class="bitable-grid-scope__add-col" @click="emit('add-field')">
              <PlusOutlined /> 新增字段
            </div>
          </template>
        </vxe-grid>
      </div>
    </template>

    <!-- U3: Record detail drawer — opened by clicking the row's seq cell.
         Rendered here so the grid is self-contained; reads state from the
         bitable store (currentRecordId / currentRecord / fields). -->
    <RecordDetailDrawer
      :record-id="store.currentRecordId"
      :open="!!store.currentRecordId"
      @close="store.closeRecordDetail"
      @retry="onDrawerRetry"
    />
  </div>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue'
import { VxeGrid } from 'vxe-table'
import { Empty as AEmpty, Popover as APopover } from 'ant-design-vue'
import { PlusOutlined } from '@ant-design/icons-vue'
import type { VxeGridProps, VxeGridEvents } from 'vxe-table'
import type {
  IBitableField,
  IBitableRecord,
  IAttachmentMeta,
  FieldType,
} from '@/api/bitable'
import { useBitableStore } from '@/stores/bitable'
import AttachmentCell from './AttachmentCell.vue'
import ImageCell from './ImageCell.vue'
import ColumnHeaderMenu from './ColumnHeaderMenu.vue'
import SelectCellEditor from './SelectCellEditor.vue'
import SelectDisplay from './SelectDisplay.vue'
import InlineFieldConfigurator from './InlineFieldConfigurator.vue'
import RecordDetailDrawer from './RecordDetailDrawer.vue'
import type { ISelectOption } from './SelectCellEditor.vue'
import {
  computeGroupingLevels,
  matchConditionalFormatRule,
  type GroupByItem,
  type ConditionalFormatRule,
  type GroupNode,
  type ViewConfigU5,
} from '@/helpers/groupingRulesUtils'

type GridRow = Record<string, unknown> & { _rowId: string; _recordId: string }
type GridColumn = NonNullable<VxeGridProps['columns']>[number]

const props = withDefaults(
  defineProps<{
    fields: IBitableField[]
    records: IBitableRecord[]
    loading?: boolean
    height?: string | number
    emptyText?: string
  }>(),
  {
    loading: false,
    height: 'auto',
    emptyText: '暂无数据',
  },
)

const emit = defineEmits<{
  (e: 'edit-cell', payload: { recordId: string; fieldId: string; value: unknown }): void
  (e: 'add-field'): void
  (e: 'config-field', field: IBitableField): void
  (e: 'hide-field', fieldId: string): void
  (e: 'delete-field', field: IBitableField): void
}>()

// Track vxe-grid instances across sections (for refreshColumn on inline save).
// ponytail: Map keyed by section idx — simplest stable approach for a v-for.
// Ceiling: if sections reorder (e.g. via sort), idx-based keys may stale;
// upgrade path: key by groupNodeKey instead.
const gridInstanceMap = new Map<number, InstanceType<typeof VxeGrid>>()
function onGridRef(idx: number, el: unknown): void {
  if (el) {
    gridInstanceMap.set(idx, el as InstanceType<typeof VxeGrid>)
  } else {
    gridInstanceMap.delete(idx)
  }
}

// U3: bitable store — used for the record detail drawer (currentRecordId) AND
// U5: reading the current view's group_by + conditional_formatting config.
// ponytail: reading view config from the store avoids adding a new prop.
// Ceiling: BitableGrid is coupled to store.currentView shape; if reused
// outside the bitable detail view, pass viewConfig as a prop instead.
const store = useBitableStore()

// ── U5: Grouping + Conditional Formatting ──────────────────────────────────

const viewConfig = computed<ViewConfigU5>(() => {
  const config = store.currentView?.config as ViewConfigU5 | undefined
  return config ?? {}
})

const groupByItems = computed<GroupByItem[]>(() => {
  const raw = viewConfig.value.group_by
  if (!Array.isArray(raw)) return []
  return raw as GroupByItem[]
})

const cfRules = computed<ConditionalFormatRule[]>(() => {
  const raw = viewConfig.value.conditional_formatting
  if (!Array.isArray(raw)) return []
  return raw as ConditionalFormatRule[]
})

const groupingEnabled = computed(
  () => groupByItems.value.length > 0 && props.records.length > 0,
)

// Build the nested group tree. Uses store.fields (all fields, not just
// visible ones) so number-field aggregations work even when the field is
// hidden by the view's hidden_fields config.
const groupTree = computed<GroupNode[]>(() => {
  if (!groupingEnabled.value) return []
  return computeGroupingLevels(props.records, groupByItems.value, store.fields)
})

type GroupSection =
  | { type: 'header'; node: GroupNode }
  | { type: 'data'; node: GroupNode | null }

// Flatten the group tree into header + data sections. When grouping is
// disabled, returns a single data section with node=null (the grid renders
// all records in one block, preserving the original non-grouped UX).
const groupSections = computed<GroupSection[]>(() => {
  if (!groupingEnabled.value) {
    return [{ type: 'data', node: null }]
  }
  return flattenGroupTree(groupTree.value)
})

function flattenGroupTree(nodes: GroupNode[]): GroupSection[] {
  const result: GroupSection[] = []
  for (const node of nodes) {
    result.push({ type: 'header', node })
    if (node.children.length > 0) {
      // Intermediate node: recurse into children (no direct data grid).
      result.push(...flattenGroupTree(node.children))
    } else {
      // Leaf node: data section with this group's records.
      result.push({ type: 'data', node })
    }
  }
  return result
}

// Collapse state — keyed by `${depth}:${fieldId}:${key}` for tree-wide uniqueness.
// Default expanded (empty Set = nothing collapsed).
const collapsedKeys = ref<Set<string>>(new Set())

function groupNodeKey(node: GroupNode): string {
  return `${node.depth}:${node.fieldId}:${node.key}`
}

function isCollapsed(node: GroupNode): boolean {
  return collapsedKeys.value.has(groupNodeKey(node))
}

function toggleGroup(node: GroupNode): void {
  const key = groupNodeKey(node)
  const next = new Set(collapsedKeys.value)
  if (next.has(key)) {
    next.delete(key)
  } else {
    next.add(key)
  }
  collapsedKeys.value = next
}

// Map a group's records to grid rows (same shape as the `rows` computed).
function rowsForGroup(node: GroupNode): GridRow[] {
  return node.records.map((r) => ({
    _rowId: r.id,
    _recordId: r.id,
    ...r.values,
  }))
}

// CF row class — first matching rule colors the entire row.
// Group headers are separate <div> elements (not vxe-grid rows), so they
// are naturally unaffected by CF (组合态约定: 条件格式仅作用于数据单元格).
function rowClassName({ row }: { row: unknown; rowIndex: number }): string {
  if (cfRules.value.length === 0) return ''
  const gridRow = row as GridRow
  for (const rule of cfRules.value) {
    if (!rule.enabled) continue
    const value = gridRow[rule.field_id]
    if (matchConditionalFormatRule(value, rule)) {
      const classes = [`bitable-cf--${rule.color_key}`]
      if (rule.bold) classes.push('bitable-cf--bold')
      return classes.join(' ')
    }
  }
  return ''
}

// row-config object — passed to vxe-grid as :row-config. Includes the CF
// className function so vxe-table calls it for every data row.
const rowConfig = computed(() => ({
  keyField: '_recordId',
  className: rowClassName as unknown as string,
}))

function fieldName(fieldId: string): string {
  return store.fields.find((f) => f.id === fieldId)?.name ?? fieldId
}

function formatNum(n: number): string {
  return Number.isInteger(n) ? String(n) : n.toFixed(2)
}

// ── End U5 ─────────────────────────────────────────────────────────────────

// U3: clicking the row's seq cell (#) opens the record detail drawer.
const onCellClick: VxeGridEvents.CellClick = (params) => {
  const { row, column } = params
  if (!column || column.type !== 'seq') return
  const recordId = (row as GridRow)._recordId
  if (!recordId) return
  store.openRecordDetail(recordId)
}

function onDrawerRetry(recordId: string): void {
  store.fetchRecordDetail(recordId)
}

// U2: inline field config — only one column edits at a time.
const editingFieldId = ref<string | null>(null)

function startInlineEdit(fieldId: string): void {
  editingFieldId.value = fieldId
}

function onInlineSaved(_field: IBitableField): void {
  editingFieldId.value = null
  // Refresh all grid instances so a type change picks up the new editRender.
  gridInstanceMap.forEach((g) => g?.refreshColumn?.())
}

function onInlineCancel(): void {
  editingFieldId.value = null
}

// Fields that use custom slot renderers (attachment/image)
const attachmentFields = computed(() =>
  props.fields.filter(
    (f) => f.field_type === 'attachment' || f.field_type === 'image',
  ),
)

// Fields that use select/multiselect editors (U5)
const selectFields = computed(() =>
  props.fields.filter(
    (f) => f.field_type === 'select' || f.field_type === 'multiselect',
  ),
)

// Map records to grid rows with a stable _rowId (record.id)
const rows = computed<GridRow[]>(() =>
  props.records.map((r) => ({
    _rowId: r.id,
    _recordId: r.id,
    ...r.values,
  })),
)

// Build vxe-grid columns from field definitions
const gridColumns = computed<GridColumn[]>(() => {
  const cols: GridColumn[] = [
    {
      type: 'seq',
      width: 56,
      fixed: 'left',
      title: '#',
    },
  ]

  for (const f of props.fields) {
    cols.push(buildColumn(f))
  }

  // Add-field column (U4)
  cols.push({
    field: '__add_field',
    title: '',
    width: 120,
    fixed: 'right',
    resizable: false,
    editRender: { enabled: false },
    slots: { header: 'header__add_field' },
  })

  return cols
})

function buildColumn(f: IBitableField): GridColumn {
  const isFormula = f.field_type === 'formula'
  const isAttachment = f.field_type === 'attachment' || f.field_type === 'image'
  const base: GridColumn = {
    field: f.id,
    title: f.name,
    minWidth: 120,
    width: 160,
    resizable: true,
    showOverflow: 'tooltip',
    // Use header slot for column dropdown menu (U4)
    slots: { header: `header_${f.id}` },
  }

  // Formula fields are read-only
  if (isFormula) {
    return { ...base, editRender: { enabled: false } }
  }

  // Attachment/image fields use custom slot renderers, read-only
  if (isAttachment) {
    return {
      ...base,
      editRender: { enabled: false },
      slots: { ...base.slots, default: `cell_${f.id}` },
    }
  }

  // Editable cells by field type
  switch (f.field_type as FieldType) {
    case 'number':
      return {
        ...base,
        editRender: {
          enabled: true,
          name: 'VxeNumberInput',
        },
      }
    case 'date':
      return {
        ...base,
        editRender: {
          enabled: true,
          name: 'VxeDatePicker',
        },
      }
    case 'select':
    case 'multiselect':
      // U5: custom dropdown editor via slots
      return {
        ...base,
        editRender: { enabled: true },
        slots: {
          ...base.slots,
          edit: `edit_${f.id}`,
          default: `cell_sel_${f.id}`,
        },
      }
    default:
      return {
        ...base,
        editRender: { enabled: true, name: 'VxeInput' },
      }
  }
}

// Handle edit-closed event — emit cell update
const onEditClosed: VxeGridEvents.EditClosed = (params) => {
  const { row, column } = params
  const field = column.field
  if (!field) return
  const recordId = (row as GridRow)._recordId
  if (!recordId) return

  // Only emit if value actually changed (deep-equal for arrays / multiselect)
  const newValue = (row as Record<string, unknown>)[field]
  const original = props.records.find((r) => r.id === recordId)
  if (!original) return
  const oldValue = original.values[field]
  const sameValue =
    Array.isArray(newValue) && Array.isArray(oldValue)
      ? newValue.length === oldValue.length &&
        newValue.every((v, i) => v === oldValue[i])
      : newValue === oldValue
  if (sameValue) return

  emit('edit-cell', {
    recordId,
    fieldId: field,
    value: newValue,
  })
}

// Expose refresh for parent (refreshes all grid instances)
defineExpose({
  refresh: () => {
    gridInstanceMap.forEach((g) => g?.refreshColumn?.())
  },
})
</script>

<style scoped>
.bitable-grid-scope {
  width: 100%;
  height: 100%;
  overflow: hidden;
}

/* KTD10: CSS isolation — all vxe-table style overrides scoped to
   .bitable-grid-scope. Use :deep() to reach vxe-table internals. */
.bitable-grid-scope :deep(.vxe-table) {
  font-size: var(--bitable-font-sm);
}

.bitable-grid-scope :deep(.vxe-header--column) {
  background: var(--bitable-color-bg-secondary);
  font-weight: 600;
}

.bitable-grid-scope :deep(.vxe-body--column.is--dirty) {
  background: var(--bitable-color-bg-tertiary);
}

.bitable-grid-scope :deep(.vxe-cell--dirty) {
  color: var(--bitable-color-primary);
}

/* U3: seq column (#) is the row-detail affordance — pointer cursor signals
   clickability without a dedicated expand icon. */
.bitable-grid-scope :deep(.vxe-header--column.col--seq),
.bitable-grid-scope :deep(.vxe-body--column.col--seq) {
  cursor: pointer;
}

.bitable-grid-scope__add-col {
  display: flex;
  align-items: center;
  gap: var(--bitable-spacing-xs);
  cursor: pointer;
  color: var(--bitable-color-text-secondary);
  font-size: var(--bitable-font-xs);
  padding: 0 var(--bitable-spacing-sm);
  height: 100%;
  user-select: none;
}

.bitable-grid-scope__add-col:hover {
  color: var(--bitable-color-primary);
}

/* ── U5: Grouping ─────────────────────────────────────────────────────── */

.bitable-grid-scope__group-header {
  display: flex;
  align-items: center;
  gap: var(--bitable-spacing-sm);
  padding: var(--bitable-spacing-xs) var(--bitable-spacing-sm);
  background: var(--bitable-color-bg-secondary);
  border-bottom: 1px solid var(--bitable-color-border);
  cursor: pointer;
  user-select: none;
  font-size: var(--bitable-font-sm);
  color: var(--bitable-color-text);
}

.bitable-grid-scope__group-header:hover {
  background: var(--bitable-color-bg-tertiary);
}

.bitable-grid-scope__caret {
  display: inline-flex;
  width: 16px;
  justify-content: center;
  color: var(--bitable-color-text-secondary);
  font-size: var(--bitable-font-sm);
}

.bitable-grid-scope__group-key {
  font-weight: 600;
  color: var(--bitable-color-text);
}

.bitable-grid-scope__group-count {
  color: var(--bitable-color-text-secondary);
  font-size: var(--bitable-font-xs);
}

.bitable-grid-scope__group-agg {
  color: var(--bitable-color-text-tertiary);
  font-size: var(--bitable-font-xs);
  margin-left: var(--bitable-spacing-sm);
}

.bitable-grid-scope__group-grid {
  border-bottom: 1px solid var(--bitable-color-border);
}

/* ── U5: Conditional Formatting (8 colors + bold) ─────────────────────── */
/* Row-level classes applied via row-config.className. Group headers are
   separate <div> elements, so they're naturally unaffected by CF. */

.bitable-grid-scope :deep(.bitable-cf--red) .vxe-body--column {
  background: var(--bitable-cf-red-bg);
}
.bitable-grid-scope :deep(.bitable-cf--red) .vxe-cell {
  color: var(--bitable-cf-red-fg);
}

.bitable-grid-scope :deep(.bitable-cf--orange) .vxe-body--column {
  background: var(--bitable-cf-orange-bg);
}
.bitable-grid-scope :deep(.bitable-cf--orange) .vxe-cell {
  color: var(--bitable-cf-orange-fg);
}

.bitable-grid-scope :deep(.bitable-cf--yellow) .vxe-body--column {
  background: var(--bitable-cf-yellow-bg);
}
.bitable-grid-scope :deep(.bitable-cf--yellow) .vxe-cell {
  color: var(--bitable-cf-yellow-fg);
}

.bitable-grid-scope :deep(.bitable-cf--green) .vxe-body--column {
  background: var(--bitable-cf-green-bg);
}
.bitable-grid-scope :deep(.bitable-cf--green) .vxe-cell {
  color: var(--bitable-cf-green-fg);
}

.bitable-grid-scope :deep(.bitable-cf--blue) .vxe-body--column {
  background: var(--bitable-cf-blue-bg);
}
.bitable-grid-scope :deep(.bitable-cf--blue) .vxe-cell {
  color: var(--bitable-cf-blue-fg);
}

.bitable-grid-scope :deep(.bitable-cf--purple) .vxe-body--column {
  background: var(--bitable-cf-purple-bg);
}
.bitable-grid-scope :deep(.bitable-cf--purple) .vxe-cell {
  color: var(--bitable-cf-purple-fg);
}

.bitable-grid-scope :deep(.bitable-cf--gray) .vxe-body--column {
  background: var(--bitable-cf-gray-bg);
}
.bitable-grid-scope :deep(.bitable-cf--gray) .vxe-cell {
  color: var(--bitable-cf-gray-fg);
}

.bitable-grid-scope :deep(.bitable-cf--neutral) .vxe-body--column {
  background: var(--bitable-cf-neutral-bg);
}
.bitable-grid-scope :deep(.bitable-cf--neutral) .vxe-cell {
  color: var(--bitable-cf-neutral-fg);
}

.bitable-grid-scope :deep(.bitable-cf--bold) .vxe-cell {
  font-weight: 700;
}
</style>
