<script setup lang="ts">
import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, ColGroupDef } from 'ag-grid-community'
import localforage from 'localforage'
import { getMajorDictEntries, getServiceDictItemById, industryTypeList, isMajorIdInIndustryScope } from '@/sql'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import { addNumbers, decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
import { formatThousandsFlexible } from '@/lib/numberFormat'
import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync'
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
import { parseNumberOrNull } from '@/lib/number'
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
import { Button } from '@/components/ui/button'
import {
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogOverlay,
  AlertDialogPortal,
  AlertDialogRoot,
  NumberFieldDecrement,
  NumberFieldIncrement,
  NumberFieldInput,
  NumberFieldRoot,
  AlertDialogTitle,
  AlertDialogTrigger,
} from 'reka-ui'

import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
// 精简的边框配置（细线条+浅灰色，弱化分割线视觉）

interface DictLeaf {
  id: string
  code: string
  name: string
  hasCost: boolean
  hasArea: boolean
}

interface DictGroup {
  id: string
  code: string
  name: string
  children: DictLeaf[]
}

interface DetailRow {
  id: string
  projectIndex?: number
  majorDictId?: string
  groupCode: string
  groupName: string
  majorCode: string
  majorName: string
  hasCost: boolean
  hasArea: boolean
  amount: number | null
  landArea: number | null
  benchmarkBudget: number | null
  benchmarkBudgetBasic: number | null
  benchmarkBudgetOptional: number | null
  benchmarkBudgetBasicChecked: boolean
  benchmarkBudgetOptionalChecked: boolean
  basicFormula: string | null
  optionalFormula: string | null
  consultCategoryFactor: number | null
  majorFactor: number | null
  workStageFactor: number | null
  workRatio: number | null
  budgetFee: number | null
  budgetFeeBasic: number | null
  budgetFeeOptional: number | null
  remark: string
  path: string[]
}

interface XmInfoState {
  projectName: string
  detailRows: DetailRow[]
}
interface XmBaseInfoState {
  projectIndustry?: string
}

interface ServiceLite {
  mutiple?: boolean | null
}

const props = defineProps<{
  contractId: string,
  serviceId: string | number
}>()
const pricingPaneReloadStore = usePricingPaneReloadStore()
const DB_KEY = computed(() => `ydGMF-${props.contractId}-${props.serviceId}`)
const HT_DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
const HT_CONSULT_FACTOR_KEY = computed(() => `ht-consult-category-factor-v1-${props.contractId}`)
const HT_MAJOR_FACTOR_KEY = computed(() => `ht-major-factor-v1-${props.contractId}`)
const BASE_INFO_KEY = 'xm-base-info-v1'
const activeIndustryCode = ref('')
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
const consultCategoryFactorMap = ref<Map<string, number | null>>(new Map())
const majorFactorMap = ref<Map<string, number | null>>(new Map())
let factorDefaultsLoaded = false
const paneInstanceCreatedAt = Date.now()
const reloadSignal = ref(0)
const industryNameMap = new Map(
  industryTypeList.flatMap(item => [
    [String(item.id).trim(), item.name],
    [String(item.type).trim(), item.name]
  ])
)
const totalLabel = computed(() => {
  const industryName = industryNameMap.get(activeIndustryCode.value.trim()) || ''
  return industryName ? `${industryName}总投资` : '总投资'
})

const isMutipleService = computed(() => {
  const service = getServiceDictItemById(props.serviceId) as ServiceLite | undefined
  return service?.mutiple === true
})
const projectCount = ref<number>(1)
const PROJECT_PATH_PREFIX = 'project-'
const PROJECT_ROW_ID_SEPARATOR = '::'
const normalizeProjectCount = (value: unknown) => {
  const parsed = Number(value)
  if (!Number.isFinite(parsed)) return 1
  return Math.max(1, Math.floor(parsed))
}
const getTargetProjectCount = () => (isMutipleService.value ? normalizeProjectCount(projectCount.value) : 1)
const buildProjectGroupPathKey = (projectIndex: number) => `${PROJECT_PATH_PREFIX}${projectIndex}`
const parseProjectIndexFromPathKey = (value: string) => {
  const match = /^project-(\d+)$/.exec(value)
  if (!match) return null
  return normalizeProjectCount(Number(match[1]))
}
const buildScopedRowId = (projectIndex: number, majorId: string) =>
  isMutipleService.value ? `${projectIndex}${PROJECT_ROW_ID_SEPARATOR}${majorId}` : majorId
const parseScopedRowId = (id: unknown) => {
  const rawId = String(id || '')
  const match = /^(\d+)::(.+)$/.exec(rawId)
  if (!match) {
    return {
      projectIndex: 1,
      majorDictId: rawId
    }
  }
  return {
    projectIndex: normalizeProjectCount(Number(match[1])),
    majorDictId: String(match[2] || '').trim()
  }
}
const resolveRowProjectIndex = (row: Partial<DetailRow> | undefined) => {
  if (!row) return 1
  if (typeof row.projectIndex === 'number' && Number.isFinite(row.projectIndex)) {
    return normalizeProjectCount(row.projectIndex)
  }
  if (Array.isArray(row.path) && row.path.length > 0) {
    const projectIndexFromPath = parseProjectIndexFromPathKey(String(row.path[0] || ''))
    if (projectIndexFromPath != null) return projectIndexFromPath
  }
  return parseScopedRowId(row.id).projectIndex
}
const resolveRowMajorDictId = (row: Partial<DetailRow> | undefined) => {
  if (!row) return ''
  const direct = String(row.majorDictId || '').trim()
  if (direct) return majorIdAliasMap.get(direct) || direct
  const parsed = parseScopedRowId(row.id).majorDictId
  return majorIdAliasMap.get(parsed) || parsed
}
const makeProjectMajorKey = (projectIndex: number, majorDictId: string) =>
  `${normalizeProjectCount(projectIndex)}:${String(majorDictId || '').trim()}`
const inferProjectCountFromRows = (rows?: Array<Partial<DetailRow>>) => {
  if (!isMutipleService.value) return 1
  let maxProjectIndex = 1
  for (const row of rows || []) {
    maxProjectIndex = Math.max(maxProjectIndex, resolveRowProjectIndex(row))
  }
  return maxProjectIndex
}

const detailRows = ref<DetailRow[]>([])
const getDefaultConsultCategoryFactor = () =>
  consultCategoryFactorMap.value.get(String(props.serviceId)) ?? null

const getDefaultMajorFactorById = (id: string): number | null => majorFactorMap.value.get(id) ?? null

const loadFactorDefaults = async () => {
  const [consultMap, majorMap] = await Promise.all([
    loadConsultCategoryFactorMap(HT_CONSULT_FACTOR_KEY.value),
    loadMajorFactorMap(HT_MAJOR_FACTOR_KEY.value)
  ])
  consultCategoryFactorMap.value = consultMap
  majorFactorMap.value = majorMap
  factorDefaultsLoaded = true
}

const ensureFactorDefaultsLoaded = async () => {
  if (factorDefaultsLoaded) return
  await loadFactorDefaults()
}

const shouldForceDefaultLoad = () => {
  const storageKey = `${PRICING_FORCE_DEFAULT_PREFIX}${DB_KEY.value}`
  const raw = sessionStorage.getItem(storageKey)
  if (!raw) return false
  const forceUntil = Number(raw)
  sessionStorage.removeItem(storageKey)
  return Number.isFinite(forceUntil) && Date.now() <= forceUntil
}

const shouldSkipPersist = () => {
  const storageKey = `${PRICING_CLEAR_SKIP_PREFIX}${DB_KEY.value}`
  const raw = sessionStorage.getItem(storageKey)
  if (!raw) return false
  const now = Date.now()

  if (raw.includes(':')) {
    const [issuedRaw, untilRaw] = raw.split(':')
    const issuedAt = Number(issuedRaw)
    const skipUntil = Number(untilRaw)
    if (Number.isFinite(issuedAt) && Number.isFinite(skipUntil) && now <= skipUntil) {
      return paneInstanceCreatedAt <= issuedAt
    }
    sessionStorage.removeItem(storageKey)
    return false
  }

  const skipUntil = Number(raw)
  if (Number.isFinite(skipUntil) && now <= skipUntil) return true
  sessionStorage.removeItem(storageKey)
  return false
}

type majorLite = { code: string; name: string; defCoe: number | null; hasCost?: boolean; hasArea?: boolean }
const serviceEntries = getMajorDictEntries().map(({ id, item }) => [id, item] as [string, majorLite])
const majorIdAliasMap = new Map(getMajorDictEntries().map(({ rawId, id }) => [rawId, id]))

const detailDict: DictGroup[] = (() => {
  const groupMap = new Map<string, DictGroup>()
  const groupOrder: string[] = []
  const codeLookup = new Map(serviceEntries.map(([key, item]) => [item.code, { id: key, code: item.code, name: item.name }]))

  for (const [key, item] of serviceEntries) {
    const code = item.code
    const isGroup = !code.includes('-')
    if (isGroup) {
      if (!groupMap.has(code)) groupOrder.push(code)
      groupMap.set(code, {
        id: key,
        code,
        name: item.name,
        children: []
      })
      continue
    }

    const parentCode = code.split('-')[0]
    if (!groupMap.has(parentCode)) {
      const parent = codeLookup.get(parentCode)
      if (!groupOrder.includes(parentCode)) groupOrder.push(parentCode)
      groupMap.set(parentCode, {
        id: parent?.id || `group-${parentCode}`,
        code: parentCode,
        name: parent?.name || parentCode,
        children: []
      })
    }

    const hasArea = item.hasArea !== false
    if (!hasArea) continue

    groupMap.get(parentCode)!.children.push({
      id: key,
      code,
      name: item.name,
      hasCost: item.hasCost !== false,
      hasArea
    })
  }

  return groupOrder.map(code => groupMap.get(code)).filter((group): group is DictGroup => Boolean(group))
})()

const idLabelMap = new Map<string, string>()
for (const group of detailDict) {
  idLabelMap.set(group.id, `${group.code} ${group.name}`)
  for (const child of group.children) {
    idLabelMap.set(child.id, `${child.code} ${child.name}`)
  }
}

const buildDefaultRows = (projectCountValue = getTargetProjectCount()): DetailRow[] => {
  if (!activeIndustryCode.value) return []
  const rows: DetailRow[] = []
  for (let projectIndex = 1; projectIndex <= projectCountValue; projectIndex++) {
    for (const group of detailDict) {
      if (activeIndustryCode.value && !isMajorIdInIndustryScope(group.id, activeIndustryCode.value)) continue
      for (const child of group.children) {
        const rowId = buildScopedRowId(projectIndex, child.id)
        rows.push({
          id: rowId,
          projectIndex,
          majorDictId: child.id,
          groupCode: group.code,
          groupName: group.name,
          majorCode: child.code,
          majorName: child.name,
          hasCost: child.hasCost,
          hasArea: child.hasArea,
          amount: null,
          landArea: null,
          benchmarkBudget: null,
          benchmarkBudgetBasic: null,
          benchmarkBudgetOptional: null,
          benchmarkBudgetBasicChecked: true,
          benchmarkBudgetOptionalChecked: true,
          basicFormula: '',
          optionalFormula: '',
          consultCategoryFactor: null,
          majorFactor: null,
          workStageFactor: 1,
          workRatio: 100,
          budgetFee: null,
          budgetFeeBasic: null,
          budgetFeeOptional: null,
          remark: '',
          path: isMutipleService.value
            ? [buildProjectGroupPathKey(projectIndex), group.id, rowId]
            : [group.id, rowId]
        })
      }
    }
  }
  return rows
}

type SourceRow = Pick<DetailRow, 'id'> &
  Partial<
    Pick<
      DetailRow,
      | 'projectIndex'
      | 'majorDictId'
      | 'amount'
      | 'landArea'
      | 'benchmarkBudget'
      | 'benchmarkBudgetBasic'
      | 'benchmarkBudgetOptional'
      | 'benchmarkBudgetBasicChecked'
      | 'benchmarkBudgetOptionalChecked'
      | 'basicFormula'
      | 'optionalFormula'
      | 'consultCategoryFactor'
      | 'majorFactor'
      | 'workStageFactor'
      | 'workRatio'
      | 'budgetFee'
      | 'budgetFeeBasic'
      | 'budgetFeeOptional'
      | 'remark'
    >
  >
const mergeWithDictRows = (
  rowsFromDb: SourceRow[] | undefined,
  options?: {
    includeScaleValues?: boolean
    includeFactorValues?: boolean
    projectCount?: number
    cloneFromProjectOne?: boolean
  }
): DetailRow[] => {
  const includeScaleValues = options?.includeScaleValues ?? true
  const includeFactorValues = options?.includeFactorValues ?? true
  const targetProjectCount = normalizeProjectCount(options?.projectCount ?? getTargetProjectCount())
  const dbValueMap = new Map<string, SourceRow>()
  for (const row of rowsFromDb || []) {
    const projectIndex = resolveRowProjectIndex(row)
    const majorDictId = resolveRowMajorDictId(row)
    if (!majorDictId) continue
    dbValueMap.set(makeProjectMajorKey(projectIndex, majorDictId), row)
  }

  return buildDefaultRows(targetProjectCount).map(row => {
    const rowProjectIndex = resolveRowProjectIndex(row)
    const rowMajorDictId = resolveRowMajorDictId(row)
    const fromDb =
      dbValueMap.get(makeProjectMajorKey(rowProjectIndex, rowMajorDictId)) ||
      (options?.cloneFromProjectOne && rowProjectIndex > 1
        ? dbValueMap.get(makeProjectMajorKey(1, rowMajorDictId))
        : undefined)
    if (!fromDb) return row
    const hasConsultCategoryFactor = Object.prototype.hasOwnProperty.call(fromDb, 'consultCategoryFactor')
    const hasMajorFactor = Object.prototype.hasOwnProperty.call(fromDb, 'majorFactor')

    return {
      ...row,
      amount: includeScaleValues && row.hasCost && typeof fromDb.amount === 'number' ? fromDb.amount : null,
      landArea: includeScaleValues && row.hasArea && typeof fromDb.landArea === 'number' ? fromDb.landArea : null,
      benchmarkBudget: typeof fromDb.benchmarkBudget === 'number' ? fromDb.benchmarkBudget : null,
      benchmarkBudgetBasic: typeof fromDb.benchmarkBudgetBasic === 'number' ? fromDb.benchmarkBudgetBasic : null,
      benchmarkBudgetOptional: typeof fromDb.benchmarkBudgetOptional === 'number' ? fromDb.benchmarkBudgetOptional : null,
      benchmarkBudgetBasicChecked:
        typeof fromDb.benchmarkBudgetBasicChecked === 'boolean' ? fromDb.benchmarkBudgetBasicChecked : true,
      benchmarkBudgetOptionalChecked:
        typeof fromDb.benchmarkBudgetOptionalChecked === 'boolean' ? fromDb.benchmarkBudgetOptionalChecked : true,
      basicFormula: typeof fromDb.basicFormula === 'string' ? fromDb.basicFormula : '',
      optionalFormula: typeof fromDb.optionalFormula === 'string' ? fromDb.optionalFormula : '',
      consultCategoryFactor:
        !includeFactorValues
          ? null
          : typeof fromDb.consultCategoryFactor === 'number'
            ? fromDb.consultCategoryFactor
            : hasConsultCategoryFactor
              ? null
              : getDefaultConsultCategoryFactor(),
      majorFactor:
        !includeFactorValues
          ? null
          : typeof fromDb.majorFactor === 'number'
            ? fromDb.majorFactor
            : hasMajorFactor
              ? null
              : getDefaultMajorFactorById(rowMajorDictId),
      workStageFactor: typeof fromDb.workStageFactor === 'number' ? fromDb.workStageFactor : row.workStageFactor,
      workRatio: typeof fromDb.workRatio === 'number' ? fromDb.workRatio : row.workRatio,
      budgetFee: typeof fromDb.budgetFee === 'number' ? fromDb.budgetFee : null,
      budgetFeeBasic: typeof fromDb.budgetFeeBasic === 'number' ? fromDb.budgetFeeBasic : null,
      budgetFeeOptional: typeof fromDb.budgetFeeOptional === 'number' ? fromDb.budgetFeeOptional : null,
      remark: typeof fromDb.remark === 'string' ? fromDb.remark : ''
    }
  })
}

const formatEditableNumber = (params: any) => {
  if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
    return '请输入'
  }
  if (params.value == null) return ''
  return formatThousandsFlexible(params.value, 3)
}

const formatConsultCategoryFactor = (params: any) => {
  return formatEditableNumber(params)
}

const formatMajorFactor = (params: any) => {
  return formatEditableNumber(params)
}

const formatReadonlyMoney = (params: any) => {
  if (params.value == null || params.value === '') return ''
  return formatThousandsFlexible(roundTo(params.value, 3), 3)
}

type BudgetCheckField = 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked'

const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (params: any) => {
  const valueText = formatReadonlyMoney(params)
  const hasValue = params.value != null && params.value !== ''
  if (params.node?.group || params.node?.rowPinned || !params.data || !hasValue) {
    return valueText
  }

  const wrapper = document.createElement('div')
  wrapper.style.display = 'flex'
  wrapper.style.alignItems = 'center'
  wrapper.style.justifyContent = 'space-between'
  wrapper.style.gap = '6px'
  wrapper.style.width = '100%'

  const checkbox = document.createElement('input')
  checkbox.type = 'checkbox'
  checkbox.className = 'cursor-pointer'
  checkbox.checked = params.data[checkField] !== false
  checkbox.addEventListener('click', event => event.stopPropagation())
  checkbox.addEventListener('change', () => {
    params.data[checkField] = checkbox.checked
    if (!checkbox.checked) {
      const budgetField = checkField === 'benchmarkBudgetBasicChecked' ? 'benchmarkBudgetBasic' : 'benchmarkBudgetOptional'
      params.data[budgetField] = 0
    }
    handleCellValueChanged()
  })

  const valueSpan = document.createElement('span')
  valueSpan.textContent = valueText
  wrapper.append(checkbox, valueSpan)

  return wrapper
}

const getBenchmarkBudgetSplitByLandArea = (row?: Pick<DetailRow, 'landArea'>) =>
  getBenchmarkBudgetSplitByScale(row?.landArea, 'area')

const getCheckedBenchmarkBudgetSplitByLandArea = (
  row?: Pick<DetailRow, 'landArea' | 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked'>
) => {
  const split = getBenchmarkBudgetSplitByLandArea(row)
  if (!split) return null
  const basic = row?.benchmarkBudgetBasicChecked === false ? 0 : split.basic
  const optional = row?.benchmarkBudgetOptionalChecked === false ? 0 : split.optional
  return {
    ...split,
    basic,
    optional,
    total: roundTo(addNumbers(basic, optional), 2)
  }
}

const getBudgetFee = (
  row?: Pick<
    DetailRow,
    | 'landArea'
    | 'benchmarkBudgetBasicChecked'
    | 'benchmarkBudgetOptionalChecked'
    | 'majorFactor'
    | 'consultCategoryFactor'
    | 'workStageFactor'
    | 'workRatio'
  >
) => {
  const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByLandArea(row)
  if (!benchmarkBudgetSplit) return null

  const splitBudgetFee = getScaleBudgetFeeSplit({
    benchmarkBudgetBasic: benchmarkBudgetSplit.basic,
    benchmarkBudgetOptional: benchmarkBudgetSplit.optional,
    majorFactor: row?.majorFactor,
    consultCategoryFactor: row?.consultCategoryFactor,
    workStageFactor: row?.workStageFactor,
    workRatio: row?.workRatio
  })
  return splitBudgetFee ? splitBudgetFee.total : null
}

const getBudgetFeeSplit = (
  row?: Pick<
    DetailRow,
    | 'landArea'
    | 'benchmarkBudgetBasicChecked'
    | 'benchmarkBudgetOptionalChecked'
    | 'majorFactor'
    | 'consultCategoryFactor'
    | 'workStageFactor'
    | 'workRatio'
  >
) => {
  const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByLandArea(row)
  if (!benchmarkBudgetSplit) return null
  return getScaleBudgetFeeSplit({
    benchmarkBudgetBasic: benchmarkBudgetSplit.basic,
    benchmarkBudgetOptional: benchmarkBudgetSplit.optional,
    majorFactor: row?.majorFactor,
    consultCategoryFactor: row?.consultCategoryFactor,
    workStageFactor: row?.workStageFactor,
    workRatio: row?.workRatio
  })
}

const getMergeColSpanBeforeTotal = (params: any) => {
  if (!params.node?.group && !params.node?.rowPinned) return 1
  const displayedColumns = params.api?.getAllDisplayedColumns?.()
  if (!Array.isArray(displayedColumns) || !params.column) return 1
  const currentIndex = displayedColumns.findIndex((column: any) => column.getColId() === params.column.getColId())
  const totalIndex = displayedColumns.findIndex((column: any) => column.getColId() === 'budgetFeeTotal')
  if (currentIndex < 0 || totalIndex <= currentIndex) return 1
  return totalIndex - currentIndex
}

const formatEditableFlexibleNumber = (params: any) => {
  if (!params.node?.group && !params.node?.rowPinned && !params.data?.hasArea) {
    return ''
  }
  if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
    return '点击输入'
  }
  if (params.value == null) return ''
  return formatThousandsFlexible(params.value, 3)
}

const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
  {
    headerName: '用地面积(亩)',
    field: 'landArea',
    headerClass: 'ag-right-aligned-header',
    minWidth: 90,

    flex: 2,

    editable: params => !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasArea),
    cellClass: params =>
      !params.node?.group && !params.node?.rowPinned && params.data?.hasArea
        ? 'ag-right-aligned-cell editable-cell-line'
        : 'ag-right-aligned-cell',
    cellClassRules: {
      'editable-cell-empty': params =>
        !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasArea) && (params.value == null || params.value === '')
    },
    valueParser: params => {
      const value = parseNumberOrNull(params.newValue)
      return value == null ? null : roundTo(value, 3)
    },
    valueFormatter: formatEditableFlexibleNumber
  },
  {
    headerName: '基准预算(元)',
    marryChildren: true,
    children: [
      {
        headerName: '基本工作',
        field: 'benchmarkBudgetBasic',
        colId: 'benchmarkBudgetBasic',
        headerClass: 'ag-right-aligned-header',
        minWidth: 130,
        flex: 1,
        cellClass: 'ag-right-aligned-cell',
        valueGetter: params =>
          params.node?.rowPinned
            ? null
            : getCheckedBenchmarkBudgetSplitByLandArea(params.data)?.basic ?? null,
        cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'),
        valueFormatter: formatReadonlyMoney
      },
      {
        headerName: '可选工作',
        field: 'benchmarkBudgetOptional',
        colId: 'benchmarkBudgetOptional',
        headerClass: 'ag-right-aligned-header',
        minWidth: 130,
        flex: 1,
        cellClass: 'ag-right-aligned-cell',
        valueGetter: params =>
          params.node?.rowPinned
            ? null
            : getCheckedBenchmarkBudgetSplitByLandArea(params.data)?.optional ?? null,
        cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetOptionalChecked'),
        valueFormatter: formatReadonlyMoney
      },
      {
        headerName: '小计',
        field: 'benchmarkBudget',
        colId: 'benchmarkBudgetTotal',
        headerClass: 'ag-right-aligned-header',
        minWidth: 100,
        flex: 1,
        cellClass: 'ag-right-aligned-cell',
        valueGetter: params =>
          params.node?.rowPinned
            ? null
            : getCheckedBenchmarkBudgetSplitByLandArea(params.data)?.total ?? null,
        valueFormatter: formatReadonlyMoney
      }
    ]
  },
  {
    headerName: '预算费用',
    marryChildren: true,
    children: [
      {
        headerName: '咨询分类系数',
        field: 'consultCategoryFactor',
        colId: 'consultCategoryFactor',
        minWidth: 80,
        flex: 1,
        editable: params => !params.node?.group && !params.node?.rowPinned,
        cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
        cellClassRules: {
          'editable-cell-empty': params =>
            !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
        },
        valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }),
        valueFormatter: formatConsultCategoryFactor
      },
      {
        headerName: '专业系数',
        field: 'majorFactor',
        colId: 'majorFactor',
        minWidth: 80,
        flex: 1,
        editable: params => !params.node?.group && !params.node?.rowPinned,
        cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
        cellClassRules: {
          'editable-cell-empty': params =>
            !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
        },
        valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }),
        valueFormatter: formatMajorFactor
      },
      {
        headerName: '工作环节系数(编审系数)',
        field: 'workStageFactor',
        colId: 'workStageFactor',
        minWidth: 80,
        flex: 1,
        editable: params => !params.node?.group && !params.node?.rowPinned,
        cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
        cellClassRules: {
          'editable-cell-empty': params =>
            !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
        },
        valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }),
        valueFormatter: formatEditableNumber
      },
      {
        headerName: '工作占比(%)',
        field: 'workRatio',
        colId: 'workRatio',
        minWidth: 80,
        flex: 1,
        editable: params => !params.node?.group && !params.node?.rowPinned,
        cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
        cellClassRules: {
          'editable-cell-empty': params =>
            !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
        },
        valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }),
        valueFormatter: formatEditableNumber
      },
      {
        headerName: '合计',
        field: 'budgetFee',
        colId: 'budgetFeeTotal',
        headerClass: 'ag-right-aligned-header',
        minWidth: 120,
        flex: 1,
        cellClass: 'ag-right-aligned-cell',
        aggFunc: decimalAggSum,
        valueGetter: params => (params.node?.rowPinned ? params.data?.budgetFee ?? null : getBudgetFee(params.data)),
        valueFormatter: formatReadonlyMoney
      }
    ]
  },
  {
    headerName: '说明',
    field: 'remark',
    minWidth: 100,
    flex: 1.2,
    cellEditor: 'agLargeTextCellEditor',
    wrapText: true,
    autoHeight: true,
    cellStyle: { whiteSpace: 'normal', lineHeight: '1.4' },

    editable: params => !params.node?.group && !params.node?.rowPinned,
    valueFormatter: params => {
      if (!params.node?.group && !params.node?.rowPinned && !params.value) return '点击输入'
      return params.value || ''
    },
    cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line remark-wrap-cell' : ''),
    cellClassRules: {
      'editable-cell-empty': params =>
        !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
    }
  }
]

const autoGroupColumnDef: ColDef = {
  headerName: '专业编码以及工程专业名称',
  minWidth: 250,
  flex: 2,

  cellRendererParams: {
    suppressCount: true
  },
  colSpan: getMergeColSpanBeforeTotal,
  valueFormatter: params => {
    if (params.node?.rowPinned) {
      return totalLabel.value
    }
    const rowData = params.data as DetailRow | undefined
    if (!params.node?.group && rowData?.majorCode && rowData?.majorName) {
      return `${rowData.majorCode} ${rowData.majorName}`
    }
    const nodeId = String(params.value || '')
    const projectIndex = parseProjectIndexFromPathKey(nodeId)
    if (projectIndex != null) return `项目${projectIndex}`
    return idLabelMap.get(nodeId) || nodeId
  },
  tooltipValueGetter: params => {
    if (params.node?.rowPinned) return totalLabel.value
    const rowData = params.data as DetailRow | undefined
    if (!params.node?.group && rowData?.majorCode && rowData?.majorName) {
      return `${rowData.majorCode} ${rowData.majorName}`
    }
    const nodeId = String(params.value || '')
    const projectIndex = parseProjectIndexFromPathKey(nodeId)
    if (projectIndex != null) return `项目${projectIndex}`
    return idLabelMap.get(nodeId) || nodeId
  }
}



const totalAmount = computed(() => sumByNumber(detailRows.value, row => row.amount))

const totalBenchmarkBudgetBasic = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetSplitByLandArea(row)?.basic))
const totalBenchmarkBudgetOptional = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetSplitByLandArea(row)?.optional))
const totalBenchmarkBudget = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetSplitByLandArea(row)?.total))

const totalBudgetFeeBasic = computed(() => sumByNumber(detailRows.value, row => getBudgetFeeSplit(row)?.basic))
const totalBudgetFeeOptional = computed(() => sumByNumber(detailRows.value, row => getBudgetFeeSplit(row)?.optional))
const totalBudgetFee = computed(() => sumByNumber(detailRows.value, row => getBudgetFee(row)))
const pinnedTopRowData = computed(() => [
  {
    id: 'pinned-total-row',
    groupCode: '',
    groupName: '',
    majorCode: '',
    majorName: '',
    hasCost: false,
    hasArea: false,
    amount: null,
    landArea: null,
    benchmarkBudget: null,
    benchmarkBudgetBasic: null,
    benchmarkBudgetOptional: null,
    benchmarkBudgetBasicChecked: true,
    benchmarkBudgetOptionalChecked: true,
    basicFormula: '',
    optionalFormula: '',
    consultCategoryFactor: null,
    majorFactor: null,
    workStageFactor: null,
    workRatio: null,
    budgetFee: totalBudgetFee.value,
    budgetFeeBasic: totalBudgetFeeBasic.value,
    budgetFeeOptional: totalBudgetFeeOptional.value,
    remark: '',
    path: ['TOTAL']
  }
])

const syncComputedValuesToDetailRows = () => {
  for (const row of detailRows.value) {
    const benchmarkBudgetRawSplit = getBenchmarkBudgetSplitByLandArea(row)
    const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByLandArea(row)
    const budgetFeeSplit = benchmarkBudgetSplit
      ? getScaleBudgetFeeSplit({
        benchmarkBudgetBasic: benchmarkBudgetSplit.basic,
        benchmarkBudgetOptional: benchmarkBudgetSplit.optional,
        majorFactor: row.majorFactor,
        consultCategoryFactor: row.consultCategoryFactor,
        workStageFactor: row.workStageFactor,
        workRatio: row.workRatio
      })
      : null

    row.benchmarkBudget = benchmarkBudgetSplit?.total ?? null
    row.benchmarkBudgetBasic = benchmarkBudgetSplit?.basic ?? null
    row.benchmarkBudgetOptional = benchmarkBudgetSplit?.optional ?? null
    row.basicFormula =
      row.benchmarkBudgetBasicChecked === false ? null : (benchmarkBudgetRawSplit?.basicFormula ?? '')
    row.optionalFormula =
      row.benchmarkBudgetOptionalChecked === false ? null : (benchmarkBudgetRawSplit?.optionalFormula ?? '')
    row.budgetFee = budgetFeeSplit?.total ?? null
    row.budgetFeeBasic = budgetFeeSplit?.basic ?? null
    row.budgetFeeOptional = budgetFeeSplit?.optional ?? null
  }
}

const buildPersistDetailRows = () => {
  syncComputedValuesToDetailRows()
  return detailRows.value.map(row => ({ ...row }))
}

const saveToIndexedDB = async () => {
  if (shouldSkipPersist()) return
  try {
    const payload = {
      detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows()))
    }
    console.log('Saving to IndexedDB:', payload)
    await localforage.setItem(DB_KEY.value, payload)
    const synced = await syncPricingTotalToZxFw({
      contractId: props.contractId,
      serviceId: props.serviceId,
      field: 'landScale',
      value: totalBudgetFee.value
    })
    if (!synced) return
  } catch (error) {
    console.error('saveToIndexedDB failed:', error)
  }
}

const getProjectMajorKeyFromRow = (row: Partial<DetailRow> | undefined) => {
  if (!row) return ''
  const majorDictId = resolveRowMajorDictId(row)
  if (!majorDictId) return ''
  return makeProjectMajorKey(resolveRowProjectIndex(row), majorDictId)
}

const buildRowsFromImportDefaultSource = async (
  targetProjectCount: number
): Promise<DetailRow[]> => {
  // 与“使用默认数据”同源：先强制刷新系数，再按合同卡片默认带出。
  await loadFactorDefaults()
  const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
  const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
  return hasContractRows
    ? mergeWithDictRows(htData!.detailRows, {
        includeFactorValues: true,
        projectCount: targetProjectCount,
        cloneFromProjectOne: true
      })
    : buildDefaultRows(targetProjectCount).map(row => ({
        ...row,
        consultCategoryFactor: getDefaultConsultCategoryFactor(),
        majorFactor: getDefaultMajorFactorById(row.majorDictId || row.id)
      }))
}

const applyProjectCountChange = async (nextValue: unknown) => {
  const normalized = normalizeProjectCount(nextValue)
  projectCount.value = normalized
  if (!isMutipleService.value) return
  const previousRows = detailRows.value.map(row => ({ ...row }))
  const previousProjectCount = inferProjectCountFromRows(previousRows)
  if (normalized === previousProjectCount) return

  if (normalized < previousProjectCount) {
    detailRows.value = mergeWithDictRows(previousRows as any, { projectCount: normalized })
    syncComputedValuesToDetailRows()
    await saveToIndexedDB()
    return
  }

  const defaultRows = await buildRowsFromImportDefaultSource(normalized)
  const existingMap = new Map<string, DetailRow>()
  for (const row of previousRows) {
    const key = getProjectMajorKeyFromRow(row)
    if (!key) continue
    existingMap.set(key, row)
  }

  detailRows.value = defaultRows.map(defaultRow => {
    const key = getProjectMajorKeyFromRow(defaultRow)
    const existingRow = key ? existingMap.get(key) : undefined
    if (!existingRow) return defaultRow
    if (resolveRowProjectIndex(existingRow) > previousProjectCount) return defaultRow
    return {
      ...defaultRow,
      ...existingRow,
      id: defaultRow.id,
      projectIndex: defaultRow.projectIndex,
      majorDictId: defaultRow.majorDictId,
      groupCode: defaultRow.groupCode,
      groupName: defaultRow.groupName,
      majorCode: defaultRow.majorCode,
      majorName: defaultRow.majorName,
      hasCost: defaultRow.hasCost,
      hasArea: defaultRow.hasArea,
      path: defaultRow.path
    }
  })
  syncComputedValuesToDetailRows()
  await saveToIndexedDB()
}

const loadFromIndexedDB = async () => {
  try {
    const baseInfo = await localforage.getItem<XmBaseInfoState>(BASE_INFO_KEY)
    activeIndustryCode.value =
      typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
    projectCount.value = 1

    await ensureFactorDefaultsLoaded()
    const applyContractDefaultRows = async () => {
      const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
      const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
      const targetProjectCount = getTargetProjectCount()
      detailRows.value = hasContractRows
        ? mergeWithDictRows(htData!.detailRows, {
            includeFactorValues: true,
            projectCount: targetProjectCount,
            cloneFromProjectOne: true
          })
        : buildDefaultRows(targetProjectCount).map(row => ({
          ...row,
          consultCategoryFactor: getDefaultConsultCategoryFactor(),
          majorFactor: getDefaultMajorFactorById(row.majorDictId || row.id)
        }))
      syncComputedValuesToDetailRows()
    }
    if (shouldForceDefaultLoad()) {
      await applyContractDefaultRows()
      return
    }

    const data = await localforage.getItem<XmInfoState>(DB_KEY.value)
    if (data) {
      if (isMutipleService.value) {
        projectCount.value = inferProjectCountFromRows(data.detailRows as any)
      }
      detailRows.value = mergeWithDictRows(data.detailRows as any, {
        projectCount: getTargetProjectCount(),
        cloneFromProjectOne: true
      })
      syncComputedValuesToDetailRows()
      return
    }

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

const importContractData = async () => {
  try {
    const baseInfo = await localforage.getItem<XmBaseInfoState>(BASE_INFO_KEY)
    activeIndustryCode.value =
      typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''

    // 使用默认数据时，强制读取最新的项目系数（预算取值优先，空值回退标准系数）
    await loadFactorDefaults()
    const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
    const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
    const targetProjectCount = getTargetProjectCount()
    detailRows.value = hasContractRows
      ? mergeWithDictRows(htData!.detailRows, {
          includeFactorValues: true,
          projectCount: targetProjectCount,
          cloneFromProjectOne: true
        })
      : buildDefaultRows(targetProjectCount).map(row => ({
        ...row,
        consultCategoryFactor: getDefaultConsultCategoryFactor(),
        majorFactor: getDefaultMajorFactorById(row.majorDictId || row.id)
      }))
    await saveToIndexedDB()
  } catch (error) {
    console.error('importContractData failed:', error)
  }
}

const clearAllData = async () => {
  try {
    detailRows.value = buildDefaultRows(getTargetProjectCount())
    await saveToIndexedDB()
  } catch (error) {
    console.error('clearAllData failed:', error)
  }
}

watch(
  () => pricingPaneReloadStore.seq,
  (nextVersion, prevVersion) => {
    if (nextVersion === prevVersion || nextVersion === 0) return
    if (!matchPricingPaneReload(pricingPaneReloadStore.lastEvent, props.contractId, props.serviceId)) return
    reloadSignal.value += 1
  }
)

watch(
  () => reloadSignal.value,
  (nextVersion, prevVersion) => {
    if (nextVersion === prevVersion || nextVersion === 0) return
    void loadFromIndexedDB()
  }
)

let persistTimer: ReturnType<typeof setTimeout> | null = null




let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
const handleCellValueChanged = () => {
  syncComputedValuesToDetailRows()
  if (gridPersistTimer) clearTimeout(gridPersistTimer)
  gridPersistTimer = setTimeout(() => {
    void saveToIndexedDB()
  }, 300)
}

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

onActivated(() => {
  void loadFromIndexedDB()
})

onBeforeUnmount(() => {
  if (persistTimer) clearTimeout(persistTimer)
  if (gridPersistTimer) clearTimeout(gridPersistTimer)
  void saveToIndexedDB()
})
const processCellForClipboard = (params: any) => {
  if (Array.isArray(params.value)) {
    return JSON.stringify(params.value); // 数组转字符串复制
  }
  return params.value;
};

const processCellFromClipboard = (params: any) => {
  try {
    const parsed = JSON.parse(params.value);
    if (Array.isArray(parsed)) return parsed;
  } catch (e) {
    // 解析失败时返回原始值，无需额外处理
  }
  return params.value;
};


</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">
        <div class="flex items-center gap-3">
          <h3 class="text-sm font-semibold text-foreground">用地规模明细</h3>
          <div v-if="isMutipleService" class="flex items-center gap-2">
            <span class="text-xs text-muted-foreground">项目数量</span>
            <NumberFieldRoot
              v-model="projectCount"
              :min="1"
              :step="1"
              class="inline-flex items-center rounded-md border bg-background"
              @update:model-value="value => void applyProjectCountChange(value)"
            >
              <NumberFieldDecrement class="cursor-pointer px-2 py-1 text-xs text-muted-foreground hover:bg-muted">-</NumberFieldDecrement>
              <NumberFieldInput class="h-7 w-14 border-x bg-transparent px-2 text-center text-xs outline-none" />
              <NumberFieldIncrement class="cursor-pointer px-2 py-1 text-xs text-muted-foreground hover:bg-muted">+</NumberFieldIncrement>
            </NumberFieldRoot>
          </div>
        </div>
        <div class="flex items-center gap-2">
          <AlertDialogRoot>
            <AlertDialogTrigger as-child>
              <Button type="button" variant="outline" size="sm">清空</Button>
            </AlertDialogTrigger>
            <AlertDialogPortal>
              <AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
              <AlertDialogContent
                class="fixed left-1/2 top-1/2 z-50 w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
                <AlertDialogTitle class="text-base font-semibold">确认清空当前明细</AlertDialogTitle>
                <AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
                  将清空当前用地规模明细，是否继续？
                </AlertDialogDescription>
                <div class="mt-4 flex items-center justify-end gap-2">
                  <AlertDialogCancel as-child>
                    <Button variant="outline">取消</Button>
                  </AlertDialogCancel>
                  <AlertDialogAction as-child>
                    <Button variant="destructive" @click="clearAllData">确认清空</Button>
                  </AlertDialogAction>
                </div>
              </AlertDialogContent>
            </AlertDialogPortal>
          </AlertDialogRoot>
          <AlertDialogRoot>
            <AlertDialogTrigger as-child>
              <Button type="button" variant="outline" size="sm">使用默认数据</Button>
            </AlertDialogTrigger>
            <AlertDialogPortal>
              <AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
              <AlertDialogContent
                class="fixed left-1/2 top-1/2 z-50 w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
                <AlertDialogTitle class="text-base font-semibold">确认覆盖当前明细</AlertDialogTitle>
                <AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
                  将使用合同默认数据覆盖当前用地规模明细，是否继续？
                </AlertDialogDescription>
                <div class="mt-4 flex items-center justify-end gap-2">
                  <AlertDialogCancel as-child>
                    <Button variant="outline">取消</Button>
                  </AlertDialogCancel>
                  <AlertDialogAction as-child>
                    <Button @click="importContractData">确认覆盖</Button>
                  </AlertDialogAction>
                </div>
              </AlertDialogContent>
            </AlertDialogPortal>
          </AlertDialogRoot>
        </div>
      </div>

      <div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
        <AgGridVue :style="{ height: '100%' }" :rowData="detailRows" :pinnedTopRowData="pinnedTopRowData"
          :columnDefs="columnDefs" :autoGroupColumnDef="autoGroupColumnDef" :gridOptions="gridOptions" :theme="myTheme"
          @cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="true"
          :suppressRowVirtualisation="true" :cellSelection="{ handle: { mode: 'range' } }" :enableClipboard="true"
          :localeText="AG_GRID_LOCALE_CN" :tooltipShowDelay="500" :headerHeight="50"
          :processCellForClipboard="processCellForClipboard" :processCellFromClipboard="processCellFromClipboard"
          :undoRedoCellEditing="true" :undoRedoCellEditingLimit="20" />
      </div>
    </div>
  </div>
</template>
