<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, FirstDataRenderedEvent, GridApi, GridReadyEvent, GridSizeChangedEvent } from 'ag-grid-community'
import { myTheme, gridOptions, agGridWrapClass, agGridStyle } from '@/lib/diyAgGridOptions'
import { parseNumberOrNull } from '@/lib/number'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
import { useKvStore } from '@/pinia/kv'
import { syncContractFactorsToPricing } from '@/lib/zxFwPricingSync'
import { withReadonlyAutoHeight } from '@/lib/agGridReadonlyAutoHeight'

interface DictItem {
  code: string
  name: string
  defCoe: number | null
  desc?: string | null
  notshowByzxflxs?: boolean
  order?: number | null
}

interface FactorRow {
  id: string
  code: string
  name: string
  standardFactor: number | null
  budgetValue: number | null
  remark: string
  path: string[]
}

interface GridState {
  detailRows: FactorRow[]
}

interface FactorChangeState {
  changedRowIds: string[]
  updatedAt: number
}

type DictSource = Record<string, DictItem>

const props = defineProps<{
  title: string
  storageKey: string
  parentStorageKey?: string
  dict: DictSource
  disableBudgetEditWhenStandardNull?: boolean
  excludeNotshowByZxflxs?: boolean
  initBudgetValueFromStandard?: boolean
}>()
const { t } = useI18n()

const zxFwPricingStore = useZxFwPricingStore()
const kvStore = useKvStore()
const detailRows = ref<FactorRow[]>([])
const gridApi = ref<GridApi<FactorRow> | null>(null)
const CHANGE_STORAGE_KEY = computed(() => `${props.storageKey}-change`)

const formatReadonlyFactor = (value: unknown) => {
  if (value == null || value === '') return ''
  const parsed = parseNumberOrNull(value, { precision: 3 })
  if (parsed == null) return ''
  return String(Number(parsed))
}

const formatEditableFactor = (params: any) => {
  if (params.value == null || params.value === '') return t('xmFactorGrid.clickToInput')
  const parsed = parseNumberOrNull(params.value, { precision: 3 })
  if (parsed == null) return ''
  return String(Number(parsed))
}

const sortedDictEntries = () =>
  Object.entries(props.dict)
    .filter((entry): entry is [string, DictItem] => {
      const item = entry[1]
      if (!item?.code || !item?.name) return false
      if (props.excludeNotshowByZxflxs && item.notshowByzxflxs === true) return false
      return true
    })
    .sort((a, b) => {
      const aOrder = Number(a[1]?.order)
      const bOrder = Number(b[1]?.order)
      if (Number.isFinite(aOrder) && Number.isFinite(bOrder) && aOrder !== bOrder) return aOrder - bOrder
      if (Number.isFinite(aOrder) && !Number.isFinite(bOrder)) return -1
      if (!Number.isFinite(aOrder) && Number.isFinite(bOrder)) return 1
      return String(a[1]?.code || a[0]).localeCompare(String(b[1]?.code || b[0]))
    })

const buildCodePath = (code: string, selfId: string, codeIdMap: Map<string, string>) => {
  const parts = code.split('-').filter(Boolean)
  if (!parts.length) return [selfId]

  const path: string[] = []
  let currentCode = parts[0]
  const firstId = codeIdMap.get(currentCode)
  if (firstId) path.push(firstId)

  for (let i = 1; i < parts.length; i += 1) {
    currentCode = `${currentCode}-${parts[i]}`
    const id = codeIdMap.get(currentCode)
    if (id) path.push(id)
  }

  if (!path.length || path[path.length - 1] !== selfId) path.push(selfId)
  return path
}

const buildDefaultRows = (): FactorRow[] => {
  const entries = sortedDictEntries()
  const codeIdMap = new Map<string, string>()
  for (const [id, item] of entries) {
    codeIdMap.set(item.code, id)
  }

  return entries.map(([id, item]) => {
    const standardFactor = typeof item.defCoe === 'number' && Number.isFinite(item.defCoe) ? item.defCoe : null
    return {
      id,
      code: item.code,
      name: item.name,
      standardFactor,
      budgetValue: props.initBudgetValueFromStandard ? standardFactor : null,
      remark: '',
      path: buildCodePath(item.code, id, codeIdMap)
    }
  })
}

type SourceRow = Pick<FactorRow, 'id'> & Partial<Pick<FactorRow, 'budgetValue' | 'remark'>>
const hasMeaningfulFactorValue = (rows: SourceRow[] | undefined) =>
  Array.isArray(rows) &&
  rows.some(row => {
    const hasBudgetValue = typeof row?.budgetValue === 'number' && Number.isFinite(row.budgetValue)
    const hasRemark = typeof row?.remark === 'string' && row.remark.trim() !== ''
    return hasBudgetValue || hasRemark
  })

const mergeWithDictRows = (rowsFromDb: SourceRow[] | undefined): FactorRow[] => {
  const dbValueMap = new Map<string, SourceRow>()
  for (const row of rowsFromDb || []) {
    dbValueMap.set(row.id, row)
  }

  return buildDefaultRows().map(row => {
    const fromDb = dbValueMap.get(row.id)
    if (!fromDb) return row

    const hasBudgetValue = Object.prototype.hasOwnProperty.call(fromDb, 'budgetValue')
    const hasRemark = Object.prototype.hasOwnProperty.call(fromDb, 'remark')
    return {
      ...row,
      budgetValue:
        typeof fromDb.budgetValue === 'number'
          ? fromDb.budgetValue
          : hasBudgetValue
            ? null
            : row.budgetValue,
      remark: typeof fromDb.remark === 'string' ? fromDb.remark : hasRemark ? '' : row.remark
    }
  })
}

const columnDefs: ColDef<FactorRow>[] = [
  {
    headerName: t('xmFactorGrid.columns.standardFactor'),
    field: 'standardFactor',
    type: 'numericColumn',
    cellClass: 'ag-right-aligned-cell',
    minWidth: 86,
    maxWidth: 100,
    headerClass: 'ag-right-aligned-header',
    flex: 0.9,
    cellClassRules: {
      'ag-right-aligned-cell': () => true
    },
    valueFormatter: params => formatReadonlyFactor(params.value)
  },
  {
    headerName: t('xmFactorGrid.columns.budgetValue'),
    field: 'budgetValue',
    minWidth: 86,
    maxWidth: 100,
    headerClass: 'ag-right-aligned-header',
    cellClass: params => {
      const disabled = props.disableBudgetEditWhenStandardNull && params.data?.standardFactor == null
      return disabled ? '' : 'editable-cell-line'
    },
    flex: 0.9,
    cellClassRules: {
      'ag-right-aligned-cell': () => true,
      'editable-cell-empty': params => {
        const disabled = props.disableBudgetEditWhenStandardNull && params.data?.standardFactor == null
        return !disabled && (params.value == null || params.value === '')
      }
    },
    editable: params => {
      if (!props.disableBudgetEditWhenStandardNull) return true
      return params.data?.standardFactor != null
    },
    valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }),
    valueFormatter: params => {
      const disabled = props.disableBudgetEditWhenStandardNull && params.data?.standardFactor == null
      if (disabled && (params.value == null || params.value === '')) return ''
      return formatEditableFactor(params)
    }
  },
  {
    headerName: t('xmFactorGrid.columns.remark'),
    field: 'remark',
    minWidth: 170,
    flex: 2.4,
    cellEditor: 'agLargeTextCellEditor',
    wrapText: true,
    autoHeight: true,
    cellStyle: { whiteSpace: 'normal', lineHeight: '1.4' },
    editable: true,
    valueFormatter: params => params.value || t('xmFactorGrid.clickToInput'),
    cellClass: ' remark-wrap-cell',
    cellClassRules: {
      'editable-cell-empty': params => params.value == null || params.value === ''
    }
  }
]
const gridColumnDefs = computed(() => withReadonlyAutoHeight(columnDefs))

const autoGroupColumnDef: ColDef<FactorRow> = {
  headerName: t('xmFactorGrid.columns.groupName'),
  minWidth: 220,
  flex: 2.2,
  cellRendererParams: {
    suppressCount: true
  },
  valueFormatter: params => {
    if (params.data?.code && params.data?.name) return `${params.data.code} ${params.data.name}`
    const key = String(params.node?.key || '')
    const dictItem = (props.dict as DictSource)[key]
    return dictItem ? `${dictItem.code} ${dictItem.name}` : ''
  }
}

const saveToIndexedDB = async () => {
  try {
    const payload: GridState = {
      detailRows: JSON.parse(JSON.stringify(detailRows.value))
    }
    zxFwPricingStore.setKeyState(props.storageKey, payload)
  } catch (error) {
    console.error('saveToIndexedDB failed:', error)
  }
}

const normalizeFactorBudgetValue = (value: unknown) => {
  const parsed = parseNumberOrNull(value, { precision: 6 })
  return typeof parsed === 'number' && Number.isFinite(parsed) ? parsed : null
}

const isSameNullableFactorNumber = (left: unknown, right: unknown) => {
  const normalizedLeft = normalizeFactorBudgetValue(left)
  const normalizedRight = normalizeFactorBudgetValue(right)
  if (normalizedLeft == null && normalizedRight == null) return true
  if (normalizedLeft == null || normalizedRight == null) return false
  return normalizedLeft === normalizedRight
}

const parseContractFactorMeta = (storageKey: string) => {
  const consultMatch = /^ht-consult-category-factor-v1-(.+)$/.exec(storageKey)
  if (consultMatch) {
    return {
      factorType: 'consult' as const,
      contractId: String(consultMatch[1] || '').trim()
    }
  }
  const majorMatch = /^ht-major-factor-v1-(.+)$/.exec(storageKey)
  if (majorMatch) {
    return {
      factorType: 'major' as const,
      contractId: String(majorMatch[1] || '').trim()
    }
  }
  return null
}

const saveFactorChangeState = async (changedRowIds: string[]) => {
  if (changedRowIds.length === 0) return
  const payload: FactorChangeState = {
    changedRowIds: Array.from(new Set(changedRowIds.map(id => String(id || '').trim()).filter(Boolean))),
    updatedAt: Date.now()
  }
  if (payload.changedRowIds.length === 0) return
  zxFwPricingStore.setKeyState(CHANGE_STORAGE_KEY.value, payload, { force: true })

  const contractMeta = parseContractFactorMeta(props.storageKey)
  if (!contractMeta?.contractId) return
  if (contractMeta.factorType === 'consult') {
    await syncContractFactorsToPricing(contractMeta.contractId, {
      consultChangedServiceIds: payload.changedRowIds
    })
    return
  }
  await syncContractFactorsToPricing(contractMeta.contractId, {
    majorChangedRowIds: payload.changedRowIds
  })
}

const loadGridState = async (storageKey: string): Promise<GridState | null> => {
  if (!storageKey) return null
  const piniaData = await zxFwPricingStore.loadKeyState<GridState>(storageKey)
  if (piniaData?.detailRows && Array.isArray(piniaData.detailRows)) return piniaData

  // 兼容历史 kvStore 数据：命中后迁移到 pinia keyed state。
  const legacyData = await kvStore.getItem<GridState>(storageKey)
  if (!legacyData?.detailRows || !Array.isArray(legacyData.detailRows)) return null
  zxFwPricingStore.setKeyState(storageKey, legacyData, { force: true })
  return legacyData
}

const handleGridReady = (event: GridReadyEvent<FactorRow>) => {
  gridApi.value = event.api
}

const loadFromIndexedDB = async () => {
  try {
    const data = await loadGridState(props.storageKey)
    if (data && hasMeaningfulFactorValue(data.detailRows)) {
      detailRows.value = mergeWithDictRows(data.detailRows)
      return
    }

    const parentStorageKey = props.parentStorageKey?.trim()
    if (parentStorageKey) {
      const parentData = await loadGridState(parentStorageKey)
      if (parentData && hasMeaningfulFactorValue(parentData.detailRows)) {
        detailRows.value = mergeWithDictRows(parentData.detailRows)
        await saveToIndexedDB()
        return
      }
    }

    if (data) {
      detailRows.value = mergeWithDictRows(data.detailRows)
      return
    }

    detailRows.value = buildDefaultRows()
    await saveToIndexedDB()
  } catch (error) {
    console.error('loadFromIndexedDB failed:', error)
    detailRows.value = buildDefaultRows()
  }
}

let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
let isBulkClipboardMutation = false
let bulkBudgetSnapshot: Map<string, number | null> | null = null
const pendingChangedRowIds = new Set<string>()

const queueChangedRowId = (rowId: unknown) => {
  const normalizedId = String(rowId || '').trim()
  if (!normalizedId) return
  pendingChangedRowIds.add(normalizedId)
}

const flushGridPersist = async () => {
  await saveToIndexedDB()
  if (pendingChangedRowIds.size === 0) return
  const changedRowIds = Array.from(pendingChangedRowIds)
  pendingChangedRowIds.clear()
  await saveFactorChangeState(changedRowIds)
}

const scheduleGridPersist = () => {
  if (gridPersistTimer) clearTimeout(gridPersistTimer)
  gridPersistTimer = setTimeout(() => {
    void flushGridPersist()
  }, 500)
}

const handleCellValueChanged = (event?: any) => {
  if (isBulkClipboardMutation) return
  const field = String(event?.colDef?.field || '')
  if (field === 'budgetValue' && !isSameNullableFactorNumber(event?.oldValue, event?.newValue)) {
    queueChangedRowId(event?.data?.id)
  }
  scheduleGridPersist()
}

const handleBulkMutationStart = () => {
  isBulkClipboardMutation = true
  bulkBudgetSnapshot = new Map(
    detailRows.value.map(row => [String(row.id || '').trim(), normalizeFactorBudgetValue(row.budgetValue)] as const)
  )
}

const handleBulkMutationEnd = () => {
  isBulkClipboardMutation = false
  if (bulkBudgetSnapshot) {
    const nextBudgetMap = new Map(
      detailRows.value.map(row => [String(row.id || '').trim(), normalizeFactorBudgetValue(row.budgetValue)] as const)
    )
    const allRowIds = new Set<string>([...bulkBudgetSnapshot.keys(), ...nextBudgetMap.keys()])
    for (const rowId of allRowIds) {
      if (!isSameNullableFactorNumber(bulkBudgetSnapshot.get(rowId), nextBudgetMap.get(rowId))) {
        queueChangedRowId(rowId)
      }
    }
    bulkBudgetSnapshot = null
  }
  scheduleGridPersist()
}

const processCellForClipboard = (params: any) => {
  if (Array.isArray(params.value)) {
    return JSON.stringify(params.value)
  }
  return params.value
}

const processCellFromClipboard = (params: any) => {
  const field = String(params.column?.getColDef?.().field || '')
  if (field === 'budgetValue') {
    return parseNumberOrNull(params.value, { precision: 3 })
  }
  try {
    const parsed = JSON.parse(params.value)
    if (Array.isArray(parsed)) return parsed
  } catch (_error) {
    return params.value
  }
  return params.value
}


onMounted(async () => {
  await loadFromIndexedDB()
})

watch(
  () => props.dict,
  () => {
    void loadFromIndexedDB()
  },
  { deep: true }
)

onBeforeUnmount(() => {
  if (gridPersistTimer) clearTimeout(gridPersistTimer)
  gridApi.value?.stopEditing()
  gridApi.value = null
  void flushGridPersist()
})
</script>

<template>
  <div class="h-full min-h-0 flex flex-col">
    <div class="rounded-lg border bg-card xmMx flex min-h-0 flex-1 flex-col">
      <div class="flex items-center justify-between border-b px-4 py-3">
        <h3 class="text-sm font-semibold text-foreground">{{ title }}</h3>
        <div class="text-xs text-muted-foreground"></div>
      </div>

      <div :class="agGridWrapClass">
        <AgGridVue
          :style="agGridStyle"
          :rowData="detailRows"
          :columnDefs="gridColumnDefs"
          :autoGroupColumnDef="autoGroupColumnDef"
          :gridOptions="gridOptions"
          :theme="myTheme"
          :animateRows="true"
          :treeData="true"
          @cell-value-changed="handleCellValueChanged"
          @paste-start="handleBulkMutationStart"
          @paste-end="handleBulkMutationEnd"
          @fill-start="handleBulkMutationStart"
          @fill-end="handleBulkMutationEnd"
          :suppressColumnVirtualisation="true"
          :suppressRowVirtualisation="true"
          :cellSelection="{ handle: { mode: 'range' } }"
          :enableClipboard="true"
          :localeText="AG_GRID_LOCALE_CN"
          :tooltipShowDelay="500"
          :headerHeight="50"
          :suppressHorizontalScroll="true"
          :processCellForClipboard="processCellForClipboard"
          :processCellFromClipboard="processCellFromClipboard"
          :undoRedoCellEditing="true"
          :undoRedoCellEditingLimit="20"
          @grid-ready="handleGridReady"
       
        />
      </div>
    </div>
  </div>
</template>

