<script setup lang="ts">
import { computed, defineComponent, h, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import type { ComponentPublicInstance, PropType } from 'vue'
import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, GridOptions, ICellRendererParams } from 'ag-grid-community'
import { myTheme, gridOptions, agGridWrapClass, agGridStyle } from '@/lib/diyAgGridOptions'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
import { addNumbers } from '@/lib/decimal'
import { parseNumberOrNull } from '@/lib/number'
import { formatThousandsFlexible } from '@/lib/numberFormat'
import {
  ensurePricingMethodDetailRowsForServices,
  getPricingMethodTotalsForService,
  getPricingMethodTotalsForServices,
  type PricingMethodTotals
} from '@/lib/pricingMethodTotals'
import { Pencil, Eraser, Trash2 } from 'lucide-vue-next'
import {
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogOverlay,
  AlertDialogPortal,
  AlertDialogRoot,
  AlertDialogTitle,

} from 'reka-ui'
import { Button } from '@/components/ui/button'
import { TooltipProvider } from '@/components/ui/tooltip'
import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue } from '@/sql'
import { useTabStore } from '@/pinia/tab'
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
import { useKvStore } from '@/pinia/kv'
import ServiceCheckboxSelector from '@/components/shared/ServiceCheckboxSelector.vue'

interface ServiceItem {
  id: string
  code: string
  name: string
  type: ServiceMethodType
}

interface DetailRow {
  id: string
  code: string
  name: string
  process: number | null
  investScale: number | null
  landScale: number | null
  workload: number | null
  hourly: number | null
  subtotal?: number | null
  actions?: unknown
}

interface ZxFwViewState {
  selectedIds?: string[]
  selectedCodes?: string[]
  detailRows: DetailRow[]
}

interface XmBaseInfoState {
  projectIndustry?: string
}

interface ServiceMethodType {
  scale?: boolean | null
  onlyCostScale?: boolean | null
  amount?: boolean | null
  workDay?: boolean | null
}

const props = defineProps<{
  contractId: string
  contractName?: string
  projectInfoKey?: string
}>()
const tabStore = useTabStore()
const zxFwPricingStore = useZxFwPricingStore()
const kvStore = useKvStore()
const PROJECT_INFO_KEY = computed(() => props.projectInfoKey || 'xm-base-info-v1')
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
const PRICING_CLEAR_SKIP_TTL_MS = 5000
const PRICING_TOTALS_OPTIONS = { excludeInvestmentCostAndAreaRows: true } as const
const projectIndustry = ref('')

type ServiceListItem = {
  code?: string
  ref?: string
  name: string
  defCoe: number | null
  isRoad?: boolean
  isRailway?: boolean
  isWaterway?: boolean
  scale?: boolean | null
  onlyCostScale?: boolean | null
  amount?: boolean | null
  workDay?: boolean | null
}

const toNullableBoolean = (value: unknown): boolean | null =>
  typeof value === 'boolean' ? value : null

const resolveMethodEnabled = (value: boolean | null | undefined, fallback = true) =>
  typeof value === 'boolean' ? value : fallback

const defaultServiceMethodType = {
  scale: true,
  onlyCostScale: false,
  amount: true,
  workDay: true
}

const serviceDict = computed<ServiceItem[]>(() => {
  const industry = projectIndustry.value
  if (!industry) return []
  const filteredEntries = getServiceDictEntries()
    .map(({ id, item }) => ({ id, item: item as ServiceListItem }))
    .filter(({ item }) => {
      const itemCode = item?.code || item?.ref
      return Boolean(itemCode && item?.name) && item.defCoe !== null && isIndustryEnabledByType(item, getIndustryTypeValue(industry))

    })
  return filteredEntries.map(({ id, item }) => ({
    id,
    code: item.code || item.ref || '',
    name: item.name,
    type: {
      scale: toNullableBoolean(item.scale),
      onlyCostScale: toNullableBoolean(item.onlyCostScale),
      amount: toNullableBoolean(item.amount),
      workDay: toNullableBoolean(item.workDay)
    }
  }))
})

const serviceById = computed(() => new Map(serviceDict.value.map(item => [item.id, item])))
const serviceIdByCode = computed(() => new Map(serviceDict.value.map(item => [item.code, item.id])))
const serviceIdSignature = computed(() => serviceDict.value.map(item => item.id).join('|'))
const fixedBudgetRow: Pick<DetailRow, 'id' | 'code' | 'name'> = { id: 'fixed-budget-c', code: '', name: '小计' }
const isFixedRow = (row?: DetailRow | null) => row?.id === fixedBudgetRow.id

const selectedIds = computed<string[]>(() => zxFwPricingStore.contracts[props.contractId]?.selectedIds || [])
const detailRows = computed<DetailRow[]>(() =>
  (zxFwPricingStore.contracts[props.contractId]?.detailRows || []).map(row => ({
    id: String(row.id || ''),
    code: row.code || '',
    name: row.name || '',
    process: String(row.id || '') === fixedBudgetRow.id ? null : (Number(row.process) === 1 ? 1 : 0),
    investScale: typeof row.investScale === 'number' ? row.investScale : null,
    landScale: typeof row.landScale === 'number' ? row.landScale : null,
    workload: typeof row.workload === 'number' ? row.workload : null,
    hourly: typeof row.hourly === 'number' ? row.hourly : null,
    subtotal: typeof row.subtotal === 'number' ? row.subtotal : null,
    actions: row.actions
  }))
)

const getCurrentContractState = (): ZxFwViewState => {
  const current = zxFwPricingStore.getContractState(props.contractId)
  if (current) {
    return {
      selectedIds: Array.isArray(current.selectedIds) ? [...current.selectedIds] : [],
      selectedCodes: Array.isArray(current.selectedCodes) ? [...current.selectedCodes] : [],
      detailRows: (current.detailRows || []).map(row => ({
        id: String(row.id || ''),
        code: row.code || '',
        name: row.name || '',
        process: String(row.id || '') === fixedBudgetRow.id ? null : (Number(row.process) === 1 ? 1 : 0),
        investScale: typeof row.investScale === 'number' ? row.investScale : null,
        landScale: typeof row.landScale === 'number' ? row.landScale : null,
        workload: typeof row.workload === 'number' ? row.workload : null,
        hourly: typeof row.hourly === 'number' ? row.hourly : null,
        subtotal: typeof row.subtotal === 'number' ? row.subtotal : null,
        actions: row.actions
      }))
    }
  }
  return {
    selectedIds: [],
    detailRows: []
  }
}

const setCurrentContractState = async (nextState: ZxFwViewState) => {
  await zxFwPricingStore.setContractState(props.contractId, nextState)
}

const pickerOpen = ref(false)
const pickerTempIds = ref<string[]>([])
const pickerSearch = ref('')
const pickerListScrollRef = ref<HTMLElement | null>(null)
const clearConfirmOpen = ref(false)
const pendingClearServiceId = ref<string | null>(null)
const deleteConfirmOpen = ref(false)
const pendingDeleteServiceId = ref<string | null>(null)
const dragSelecting = ref(false)
const dragMoved = ref(false)
let dragSelectChecked = false
const dragBaseIds = ref<string[]>([])
const dragStartPoint = ref({ x: 0, y: 0 })
const dragCurrentPoint = ref({ x: 0, y: 0 })
const pickerItemElMap = new Map<string, HTMLElement>()
let dragTouchedIds = new Set<string>()
let dragAutoScrollRafId: number | null = null

const DRAG_AUTO_SCROLL_EDGE = 36
const DRAG_AUTO_SCROLL_MAX_STEP = 16

const stopDragAutoScroll = () => {
  if (dragAutoScrollRafId !== null) {
    cancelAnimationFrame(dragAutoScrollRafId)
    dragAutoScrollRafId = null
  }
}

const resolveDragAutoScrollStep = () => {
  const scroller = pickerListScrollRef.value
  if (!scroller) return 0
  const rect = scroller.getBoundingClientRect()
  const pointerY = dragCurrentPoint.value.y
  let step = 0

  if (pointerY > rect.bottom - DRAG_AUTO_SCROLL_EDGE) {
    const ratio = Math.min(2, (pointerY - (rect.bottom - DRAG_AUTO_SCROLL_EDGE)) / DRAG_AUTO_SCROLL_EDGE)
    step = Math.max(2, Math.round(ratio * DRAG_AUTO_SCROLL_MAX_STEP))
  } else if (pointerY < rect.top + DRAG_AUTO_SCROLL_EDGE) {
    const ratio = Math.min(2, ((rect.top + DRAG_AUTO_SCROLL_EDGE) - pointerY) / DRAG_AUTO_SCROLL_EDGE)
    step = -Math.max(2, Math.round(ratio * DRAG_AUTO_SCROLL_MAX_STEP))
  }
  return step
}

const runDragAutoScroll = () => {
  if (!dragSelecting.value) {
    stopDragAutoScroll()
    return
  }

  const scroller = pickerListScrollRef.value
  if (scroller) {
    const step = resolveDragAutoScrollStep()
    if (step !== 0) {
      const prev = scroller.scrollTop
      scroller.scrollTop += step
      if (scroller.scrollTop !== prev) {
        applyDragSelectionByRect()
      }
    }
  }

  dragAutoScrollRafId = requestAnimationFrame(runDragAutoScroll)
}

const startDragAutoScroll = () => {
  if (dragAutoScrollRafId !== null) return
  dragAutoScrollRafId = requestAnimationFrame(runDragAutoScroll)
}

const selectedServiceText = computed(() => {
  if (selectedIds.value.length === 0) return ''
  const names = selectedIds.value
    .map(id => serviceById.value.get(id)?.name || '')
    .filter(Boolean)
  if (names.length <= 2) return names.join('、')
  return `${names.slice(0, 2).join('、')} 等 ${names.length} 项`
})

const pendingClearServiceName = computed(() => {
  if (!pendingClearServiceId.value) return ''
  const row = detailRows.value.find(item => item.id === pendingClearServiceId.value)
  if (row) return `${row.code}${row.name}`
  const dict = serviceById.value.get(pendingClearServiceId.value)
  if (dict) return `${dict.code}${dict.name}`
  return pendingClearServiceId.value
})

const pendingDeleteServiceName = computed(() => {
  if (!pendingDeleteServiceId.value) return ''
  const row = detailRows.value.find(item => item.id === pendingDeleteServiceId.value)
  if (row) return `${row.code}${row.name}`
  const dict = serviceById.value.get(pendingDeleteServiceId.value)
  if (dict) return `${dict.code}${dict.name}`
  return pendingDeleteServiceId.value
})

const handleClearConfirmOpenChange = (open: boolean) => {
  clearConfirmOpen.value = open
}

const handleDeleteConfirmOpenChange = (open: boolean) => {
  deleteConfirmOpen.value = open
}

const requestClearRow = (row: DetailRow) => {
  if (isFixedRow(row)) return
  pendingClearServiceId.value = row.id
  clearConfirmOpen.value = true
}

const confirmClearRow = async () => {
  const id = pendingClearServiceId.value
  if (!id) return
  const row = detailRows.value.find(item => item.id === id)
  if (!row || isFixedRow(row)) {
    clearConfirmOpen.value = false
    pendingClearServiceId.value = null
    return
  }
  await clearRowValues(row)
  clearConfirmOpen.value = false
  pendingClearServiceId.value = null
}

const requestDeleteRow = (row: DetailRow) => {
  if (isFixedRow(row)) return
  pendingDeleteServiceId.value = row.id
  deleteConfirmOpen.value = true
}

const confirmDeleteRow = async () => {
  const id = pendingDeleteServiceId.value
  if (!id) return
  const row = detailRows.value.find(item => item.id === id)
  if (!row || isFixedRow(row)) {
    deleteConfirmOpen.value = false
    pendingDeleteServiceId.value = null
    return
  }
  await removeRow(row)
  deleteConfirmOpen.value = false
  pendingDeleteServiceId.value = null
}

const filteredServiceDict = computed(() => {
  const keyword = pickerSearch.value.trim()
  if (!keyword) return serviceDict.value
  return serviceDict.value.filter(item => item.code.includes(keyword) || item.name.includes(keyword))
})

const dragRectStyle = computed(() => {
  if (!dragSelecting.value) return {}
  const left = Math.min(dragStartPoint.value.x, dragCurrentPoint.value.x)
  const top = Math.min(dragStartPoint.value.y, dragCurrentPoint.value.y)
  const width = Math.abs(dragCurrentPoint.value.x - dragStartPoint.value.x)
  const height = Math.abs(dragCurrentPoint.value.y - dragStartPoint.value.y)
  return {
    left: `${left}px`,
    top: `${top}px`,
    width: `${width}px`,
    height: `${height}px`
  }
})

const numericParser = (newValue: any): number | null => {
  return parseNumberOrNull(newValue, { precision: 3 })
}

const isFiniteNumberValue = (value: unknown): value is number =>
  typeof value === 'number' && Number.isFinite(value)

const sumNullableNumbers = (values: Array<number | null | undefined>): number | null => {
  const validValues = values.filter(isFiniteNumberValue)
  if (validValues.length === 0) return null
  return addNumbers(...validValues)
}

const getServiceMethodTypeById = (serviceId: string) => {
  const type = serviceById.value.get(serviceId)?.type
  const scale = resolveMethodEnabled(type?.scale, defaultServiceMethodType.scale)
  const onlyCostScale = resolveMethodEnabled(type?.onlyCostScale, defaultServiceMethodType.onlyCostScale)
  const amount = resolveMethodEnabled(type?.amount, defaultServiceMethodType.amount)
  const workDay = resolveMethodEnabled(type?.workDay, defaultServiceMethodType.workDay)
  return { scale, onlyCostScale, amount, workDay }
}

const sanitizePricingTotalsByService = (serviceId: string, totals: PricingMethodTotals): PricingMethodTotals => {
  const methodType = getServiceMethodTypeById(serviceId)
  const isScaleEnabled = methodType.scale
  const isLandScaleEnabled = isScaleEnabled && !methodType.onlyCostScale
  return {
    investScale: isScaleEnabled ? totals.investScale : null,
    landScale: isLandScaleEnabled ? totals.landScale : null,
    workload: methodType.amount ? totals.workload : null,
    hourly: methodType.workDay ? totals.hourly : null
  }
}

const sanitizePricingFieldsByService = (
  serviceId: string,
  values: Pick<DetailRow, 'investScale' | 'landScale' | 'workload' | 'hourly'>
) => {
  const sanitized = sanitizePricingTotalsByService(serviceId, values)
  return {
    investScale: sanitized.investScale,
    landScale: sanitized.landScale,
    workload: sanitized.workload,
    hourly: sanitized.hourly
  }
}

const getMethodTotalFromRows = (
  rows: DetailRow[],
  field: 'investScale' | 'landScale' | 'workload' | 'hourly'
) => sumNullableNumbers(
  rows
    .filter(row => !isFixedRow(row))
    .map(row => row[field])
)

const getMethodTotal = (field: 'investScale' | 'landScale' | 'workload' | 'hourly') =>
  getMethodTotalFromRows(detailRows.value, field)

const getFixedRowSubtotal = () =>
  sumNullableNumbers([
    getMethodTotal('investScale'),
    getMethodTotal('landScale'),
    getMethodTotal('workload'),
    getMethodTotal('hourly')
  ])

const getPricingPaneStorageKeys = (serviceId: string) =>
  zxFwPricingStore.getServicePricingStorageKeys(props.contractId, serviceId)

const clearPricingPaneValues = async (serviceId: string) => {
  const keys = getPricingPaneStorageKeys(serviceId)
  const clearIssuedAt = Date.now()
  const skipUntil = clearIssuedAt + PRICING_CLEAR_SKIP_TTL_MS
  const skipToken = `${clearIssuedAt}:${skipUntil}`
  for (const key of keys) {
    sessionStorage.setItem(`${PRICING_CLEAR_SKIP_PREFIX}${key}`, skipToken)
    sessionStorage.setItem(`${PRICING_FORCE_DEFAULT_PREFIX}${key}`, String(skipUntil))
  }
  zxFwPricingStore.removeAllServicePricingMethodStates(props.contractId, serviceId)
  // Reset后会立刻有逻辑读取IndexedDB计算默认值，这里强制同步删除持久层，避免读到旧数据。
  await Promise.all(keys.map(key => kvStore.removeItem(key)))
}

const clearRowValues = async (row: DetailRow) => {
  if (isFixedRow(row)) return

  // 若该服务编辑页已打开，先关闭，避免子页面卸载时把旧数据写回。
  tabStore.removeTab(`zxfw-edit-${props.contractId}-${row.id}`)
  await nextTick()
  await clearPricingPaneValues(row.id)
  await ensurePricingMethodDetailRowsForServices({
    contractId: props.contractId,
    serviceIds: [row.id],
    options: PRICING_TOTALS_OPTIONS
  })
  const totals = await getPricingMethodTotalsForService({
    contractId: props.contractId,
    serviceId: row.id,
    options: PRICING_TOTALS_OPTIONS
  })
  const sanitizedTotals = sanitizePricingTotalsByService(row.id, totals)
  const currentState = getCurrentContractState()
  const clearedRows = currentState.detailRows.map(item =>
    item.id !== row.id
      ? item
      : {
        ...item,
        investScale: sanitizedTotals.investScale,
        landScale: sanitizedTotals.landScale,
        workload: sanitizedTotals.workload,
        hourly: sanitizedTotals.hourly
      }
  )
  const nextInvestScale = getMethodTotalFromRows(clearedRows, 'investScale')
  const nextLandScale = getMethodTotalFromRows(clearedRows, 'landScale')
  const nextWorkload = getMethodTotalFromRows(clearedRows, 'workload')
  const nextHourly = getMethodTotalFromRows(clearedRows, 'hourly')
  const nextRows = clearedRows.map(item =>
    isFixedRow(item)
      ? {
        ...item,
        investScale: nextInvestScale,
        landScale: nextLandScale,
        workload: nextWorkload,
        hourly: nextHourly,
        subtotal: sumNullableNumbers([nextInvestScale, nextLandScale, nextWorkload, nextHourly])
      }
      : item
  )
  await setCurrentContractState({
    ...currentState,
    detailRows: nextRows
  })
}

const openEditTab = (row: DetailRow) => {
  const serviceType = serviceById.value.get(row.id)?.type
  tabStore.openTab({
    id: `zxfw-edit-${props.contractId}-${row.id}`,
    title: `服务编辑-${row.code}${row.name}`,
    componentName: 'ZxFwView',
    props: {
      contractId: props.contractId,
      contractName: props.contractName || '',
      serviceId: row.id,
      fwName: row.code + row.name,
      type: serviceType ? { ...serviceType } : undefined,
      projectInfoKey: props.projectInfoKey
    }
  })
}

const removeRow = async (row: DetailRow) => {
  if (isFixedRow(row)) return
  const nextIds = selectedIds.value.filter(id => id !== row.id)
  await handleServiceSelectionChange(nextIds)
}

const ActionCellRenderer = defineComponent({
  name: 'ActionCellRenderer',
  props: {
    params: {
      type: Object as PropType<ICellRendererParams<DetailRow>>,
      required: true
    }
  },
  setup(props) {
    return () => {
      if (isFixedRow(props.params.data)) return null
      return h('div', { class: 'zxfw-action-wrap' }, [
        h('div', { class: 'zxfw-action-group' }, [
          h('button', { class: 'zxfw-action-btn', 'data-action': 'edit', type: 'button' }, [
            h(Pencil, { size: 13, 'aria-hidden': 'true' }),
            h('span', '编辑')
          ]),
          h('button', { class: 'zxfw-action-btn', 'data-action': 'clear', type: 'button' }, [
            h(Eraser, { size: 13, 'aria-hidden': 'true' }),
            h('span', '恢复默认')
          ]),
          h('button', { class: 'zxfw-action-btn zxfw-action-btn--danger', 'data-action': 'delete', type: 'button' }, [
            h(Trash2, { size: 13, 'aria-hidden': 'true' }),
            h('span', '删除')
          ])
        ])
      ])
    }
  }
})

const ProcessCellRenderer = defineComponent({
  name: 'ProcessCellRenderer',
  props: {
    params: {
      type: Object as PropType<ICellRendererParams<DetailRow>>,
      required: true
    }
  },
  setup(props) {
    return () => {
      const row = props.params.data
      if (!row || isFixedRow(row)) return null
      const processValue = row.process === 1 ? 1 : 0
      const onSelect = (event: Event, value: 0 | 1) => {
        event.stopPropagation()
        void props.params.context?.onSetProcess?.(row.id, value)
      }
      const radioName = `process-${row.id}`
      return h('div', { class: 'flex items-center justify-center gap-4 w-full text-sm' }, [
        h('label', { class: 'inline-flex items-center gap-1.5 cursor-pointer' }, [
          h('input', {
            type: 'radio',
            name: radioName,
            checked: processValue === 0,
            class: 'cursor-pointer h-4 w-4',
            onClick: (event: Event) => event.stopPropagation(),
            onChange: (event: Event) => onSelect(event, 0)
          }),
          h('span', '编制')
        ]),
        h('label', { class: 'inline-flex items-center gap-1.5 cursor-pointer' }, [
          h('input', {
            type: 'radio',
            name: radioName,
            checked: processValue === 1,
            class: 'cursor-pointer h-4 w-4',
            onClick: (event: Event) => event.stopPropagation(),
            onChange: (event: Event) => onSelect(event, 1)
          }),
          h('span', '审核')
        ])
      ])
    }
  }
})

const columnDefs: ColDef<DetailRow>[] = [
  {
    headerName: '编码',
    field: 'code',
    minWidth: 50,
    maxWidth: 100,
    valueGetter: params => {
      if (!params.data) return ''
      if (isFixedRow(params.data)) return '小计'
      return params.data.code
    },
    colSpan: params => (params.data && isFixedRow(params.data) ? 2 : 1)
  },
  {
    headerName: '名称',
    field: 'name',
    minWidth: 250,
    flex: 3,
    tooltipField: 'name',
    valueGetter: params => {
      if (!params.data) return ''
      if (isFixedRow(params.data)) return ''
      return params.data.name
    }
  },
  {
    headerName: '工作环节',
    field: 'process',
    headerClass: 'ag-center-header zxfw-process-header',
    minWidth: 170,
    maxWidth: 220,
    flex: 1.2,
    editable: false,
    sortable: false,
    filter: false,
    cellStyle: {
      textAlign: 'center',
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center'
    },
    valueGetter: params => {
      if (!params.data || isFixedRow(params.data)) return null
      return params.data.process === 1 ? 1 : 0
    },
    cellRenderer: ProcessCellRenderer
  },
  {
    headerName: '投资规模法',
    field: 'investScale',
    headerClass: 'ag-right-aligned-header',
    minWidth: 140,
    flex: 2,
    cellClass: 'ag-right-aligned-cell',
    editable: false,

    valueGetter: params => {
      if (!params.data) return null
      if (isFixedRow(params.data)) return getMethodTotal('investScale')
      return params.data.investScale
    },
    valueParser: params => numericParser(params.newValue),
    valueFormatter: params => (params.value == null ? '' : formatThousandsFlexible(params.value, 3))
  },
  {
    headerName: '用地规模法',
    field: 'landScale',
    headerClass: 'ag-right-aligned-header',
    minWidth: 140,
    flex: 2,
    cellClass: 'ag-right-aligned-cell',
    editable: false,

    valueGetter: params => {
      if (!params.data) return null
      if (isFixedRow(params.data)) return getMethodTotal('landScale')
      return params.data.landScale
    },
    valueParser: params => numericParser(params.newValue),
    valueFormatter: params => (params.value == null ? '' : formatThousandsFlexible(params.value, 3))
  },
  {
    headerName: '工作量法',
    field: 'workload',
    headerClass: 'ag-right-aligned-header',
    minWidth: 120,
    flex: 2,
    cellClass: 'ag-right-aligned-cell',
    editable: false,

    valueGetter: params => {
      if (!params.data) return null
      if (isFixedRow(params.data)) return getMethodTotal('workload')
      return params.data.workload
    },
    // editable: params => !params.node?.rowPinned && !isFixedRow(params.data),
    valueParser: params => numericParser(params.newValue),
    valueFormatter: params => (params.value == null ? '' : formatThousandsFlexible(params.value, 3))
  },
  {
    headerName: '工时法',
    field: 'hourly',
    headerClass: 'ag-right-aligned-header',
    minWidth: 120,
    flex: 2,
    cellClass: 'ag-right-aligned-cell',
    editable: false,

    valueGetter: params => {
      if (!params.data) return null
      if (isFixedRow(params.data)) return getMethodTotal('hourly')
      return params.data.hourly
    },
    // editable: params => !params.node?.rowPinned && !isFixedRow(params.data),
    valueParser: params => numericParser(params.newValue),
    valueFormatter: params => (params.value == null ? '' : formatThousandsFlexible(params.value, 3))
  },
  {
    headerName: '小计',
    field: 'subtotal',
    headerClass: 'ag-right-aligned-header',
    flex: 3,

    minWidth: 120,
    cellClass: 'ag-right-aligned-cell',
    editable: false,
    valueGetter: params => {
      if (!params.data) return null
      if (isFixedRow(params.data)) return getFixedRowSubtotal()
      return sumNullableNumbers([
        params.data.investScale,
        params.data.landScale,
        params.data.workload,
        params.data.hourly
      ])
    },
    valueFormatter: params => (params.value == null ? '' : formatThousandsFlexible(params.value, 3))
  },
  {
    headerName: '操作',
    field: 'actions',
    minWidth: 220,
    flex: 2,
    maxWidth: 260,
    editable: false,
    sortable: false,
    filter: false,
    suppressMovable: true,
    cellRenderer: ActionCellRenderer
  }
]

const detailGridOptions: GridOptions<DetailRow> = {
  ...gridOptions,
  treeData: false,
  getDataPath: undefined,
  context: {
    onSetProcess: async (rowId: string, value: 0 | 1) => {
      const currentState = getCurrentContractState()
      let changed = false
      const nextRows = currentState.detailRows.map(row => {
        if (isFixedRow(row) || String(row.id) !== String(rowId)) return row
        const nextValue = value === 1 ? 1 : 0
        if ((row.process === 1 ? 1 : 0) === nextValue) return row
        changed = true
        return {
          ...row,
          process: nextValue
        }
      })
      if (!changed) return
      await setCurrentContractState({
        ...currentState,
        detailRows: nextRows
      })
    }
  },
  onCellClicked: async params => {
    if (params.colDef.field !== 'actions' || !params.data || isFixedRow(params.data)) return
    const target = params.event?.target as HTMLElement | null
    const btn = target?.closest('button[data-action]') as HTMLButtonElement | null
    const action = btn?.dataset.action
    if (action === 'clear') {
      requestClearRow(params.data)
      return
    }
    if (action === 'edit') {
      openEditTab(params.data)
      return
    }
    if (action === 'delete') {
      requestDeleteRow(params.data)
    }
  }
}

const applyFixedRowTotals = (rows: DetailRow[]) => {
  const nextInvestScale = getMethodTotalFromRows(rows, 'investScale')
  const nextLandScale = getMethodTotalFromRows(rows, 'landScale')
  const nextWorkload = getMethodTotalFromRows(rows, 'workload')
  const nextHourly = getMethodTotalFromRows(rows, 'hourly')
  return rows.map(row =>
    isFixedRow(row)
      ? {
        ...row,
        investScale: nextInvestScale,
        landScale: nextLandScale,
        workload: nextWorkload,
        hourly: nextHourly,
        subtotal: sumNullableNumbers([nextInvestScale, nextLandScale, nextWorkload, nextHourly])
      }
      : row
  )
}

const getSelectedServiceIdsWithoutFixed = () =>
  detailRows.value
    .filter(row => !isFixedRow(row))
    .map(row => String(row.id))

const ensurePricingDetailRowsForCurrentSelection = async () => {
  const serviceIds = getSelectedServiceIdsWithoutFixed()
  if (serviceIds.length === 0) return
  await ensurePricingMethodDetailRowsForServices({
    contractId: props.contractId,
    serviceIds,
    options: PRICING_TOTALS_OPTIONS
  })
}

const fillPricingTotalsForServiceIds = async (serviceIds: string[]) => {
  const currentState = getCurrentContractState()
  const targetIds = Array.from(
    new Set(
      serviceIds.filter(id =>
        currentState.detailRows.some(row => !isFixedRow(row) && String(row.id) === String(id))
      )
    )
  )

  if (targetIds.length === 0) {
    await setCurrentContractState({
      ...currentState,
      detailRows: applyFixedRowTotals(currentState.detailRows)
    })
    return
  }

  await ensurePricingMethodDetailRowsForServices({
    contractId: props.contractId,
    serviceIds: targetIds,
    options: PRICING_TOTALS_OPTIONS
  })

  const totalsByServiceId = await getPricingMethodTotalsForServices({
    contractId: props.contractId,
    serviceIds: targetIds,
    options: PRICING_TOTALS_OPTIONS
  })

  const targetSet = new Set(targetIds.map(id => String(id)))
  const nextRows = currentState.detailRows.map(row => {
    if (isFixedRow(row) || !targetSet.has(String(row.id))) return row
    const totalsRaw = totalsByServiceId.get(String(row.id))
    const totals = totalsRaw ? sanitizePricingTotalsByService(String(row.id), totalsRaw) : null
    if (!totals) return row
    return {
      ...row,
      investScale: totals.investScale,
      landScale: totals.landScale,
      workload: totals.workload,
      hourly: totals.hourly
    }
  })

  await setCurrentContractState({
    ...currentState,
    detailRows: applyFixedRowTotals(nextRows)
  })
}

const applySelection = async (codes: string[]) => {
  const currentState = getCurrentContractState()
  const prevSelectedSet = new Set(currentState.selectedIds || [])
  const uniqueIds = Array.from(new Set(codes)).filter(
    id => serviceById.value.has(id) && id !== fixedBudgetRow.id
  )
  const existingMap = new Map(currentState.detailRows.map(row => [row.id, row]))

  const baseRows: DetailRow[] = uniqueIds
    .map<DetailRow | null>(id => {
      const dictItem = serviceById.value.get(id)
      if (!dictItem) return null

      const old = existingMap.get(id)
      const nextValues = sanitizePricingFieldsByService(id, {
        investScale: old?.investScale ?? null,
        landScale: old?.landScale ?? null,
        workload: old?.workload ?? null,
        hourly: old?.hourly ?? null
      })
      return {
        id: old?.id || id,
        code: dictItem.code,
        name: dictItem.name,
        process: old?.process === 1 ? 1 : 0,
        investScale: nextValues.investScale,
        landScale: nextValues.landScale,
        workload: nextValues.workload,
        hourly: nextValues.hourly
      }
    })
    .filter((row): row is DetailRow => row !== null)

  const orderMap = new Map(serviceDict.value.map((item, index) => [item.id, index]))
  baseRows.sort((a, b) => (orderMap.get(a.id) || 0) - (orderMap.get(b.id) || 0))

  const fixedOld = existingMap.get(fixedBudgetRow.id)
  const fixedRow: DetailRow = {
    id: fixedOld?.id || fixedBudgetRow.id,
    code: fixedBudgetRow.code,
    name: fixedBudgetRow.name,
    process: null,
    investScale: typeof fixedOld?.investScale === 'number' ? fixedOld.investScale : null,
    landScale: typeof fixedOld?.landScale === 'number' ? fixedOld.landScale : null,
    workload: typeof fixedOld?.workload === 'number' ? fixedOld.workload : null,
    hourly: typeof fixedOld?.hourly === 'number' ? fixedOld.hourly : null,
    subtotal: null,
    actions: null
  }

  const removedIds = Array.from(prevSelectedSet).filter(id => !uniqueIds.includes(id))
  for (const id of removedIds) {
    tabStore.removeTab(`zxfw-edit-${props.contractId}-${id}`)
  }

  await setCurrentContractState({
    ...currentState,
    selectedIds: uniqueIds,
    detailRows: applyFixedRowTotals([...baseRows, fixedRow])
  })
}

const handleServiceSelectionChange = async (ids: string[]) => {
  const prevIds = [...selectedIds.value]
  await applySelection(ids)
  const nextSelectedIds = getCurrentContractState().selectedIds || []
  const nextSelectedSet = new Set(nextSelectedIds)
  const addedIds = nextSelectedIds.filter(id => !prevIds.includes(id) && nextSelectedSet.has(id))
  await fillPricingTotalsForServiceIds(addedIds)
  await ensurePricingDetailRowsForCurrentSelection()
}

const preparePickerOpen = () => {
  pickerTempIds.value = [...selectedIds.value]
  pickerSearch.value = ''
}

const closePicker = () => {
  stopDragSelect()
  pickerOpen.value = false
}

const handlePickerOpenChange = (open: boolean) => {
  if (open) {
    preparePickerOpen()
  } else {
    stopDragSelect()
  }
  pickerOpen.value = open
}

const confirmPicker = async () => {
  await applySelection(pickerTempIds.value)
}

const clearPickerSelection = () => {
  pickerTempIds.value = []
}



const applyTempChecked = (code: string, checked: boolean) => {
  const exists = pickerTempIds.value.includes(code)
  if (checked && !exists) {
    pickerTempIds.value = [...pickerTempIds.value, code]
    return
  }
  if (!checked && exists) {
    pickerTempIds.value = pickerTempIds.value.filter(item => item !== code)
  }
}

const setPickerItemRef = (
  code: string,
  el: Element | ComponentPublicInstance | null
) => {
  if (el instanceof HTMLElement) {
    pickerItemElMap.set(code, el)
    return
  }
  pickerItemElMap.delete(code)
}

const isRectIntersect = (
  a: { left: number; right: number; top: number; bottom: number },
  b: { left: number; right: number; top: number; bottom: number }
) => !(a.right < b.left || a.left > b.right || a.bottom < b.top || a.top > b.bottom)

const applyDragSelectionByRect = () => {
  const rect = {
    left: Math.min(dragStartPoint.value.x, dragCurrentPoint.value.x),
    right: Math.max(dragStartPoint.value.x, dragCurrentPoint.value.x),
    top: Math.min(dragStartPoint.value.y, dragCurrentPoint.value.y),
    bottom: Math.max(dragStartPoint.value.y, dragCurrentPoint.value.y)
  }

  const nextSelectedSet = new Set(dragBaseIds.value)
  for (const [code, el] of pickerItemElMap.entries()) {
    const itemRect = el.getBoundingClientRect()
    const hit = isRectIntersect(rect, itemRect)
    if (hit) {
      dragTouchedIds.add(code)
    }
  }

  for (const code of dragTouchedIds) {
    if (dragSelectChecked) {
      nextSelectedSet.add(code)
    } else {
      nextSelectedSet.delete(code)
    }
  }

  pickerTempIds.value = serviceDict
    .value
    .map(item => item.id)
    .filter(id => nextSelectedSet.has(id))
}

const stopDragSelect = () => {
  dragSelecting.value = false
  dragMoved.value = false
  dragBaseIds.value = []
  dragTouchedIds.clear()
  stopDragAutoScroll()
  window.removeEventListener('mousemove', onDragSelectingMove)
  window.removeEventListener('mouseup', stopDragSelect)
}

const onDragSelectingMove = (event: MouseEvent) => {
  dragCurrentPoint.value = { x: event.clientX, y: event.clientY }
  if (!dragMoved.value) {
    const dx = Math.abs(event.clientX - dragStartPoint.value.x)
    const dy = Math.abs(event.clientY - dragStartPoint.value.y)
    if (dx >= 3 || dy >= 3) {
      dragMoved.value = true
    }
  }
  applyDragSelectionByRect()
}

const startDragSelect = (event: MouseEvent, code: string) => {
  dragSelecting.value = true
  dragMoved.value = false
  dragBaseIds.value = [...pickerTempIds.value]
  dragTouchedIds = new Set([code])
  dragSelectChecked = !pickerTempIds.value.includes(code)
  dragStartPoint.value = { x: event.clientX, y: event.clientY }
  dragCurrentPoint.value = { x: event.clientX, y: event.clientY }
  applyTempChecked(code, dragSelectChecked)
  startDragAutoScroll()
  window.addEventListener('mousemove', onDragSelectingMove)
  window.addEventListener('mouseup', stopDragSelect)
}
const handleDragHover = (_code: string) => {
  if (!dragSelecting.value || !dragMoved.value) return
  applyDragSelectionByRect()
}

const initializeContractState = async () => {
  try {
    await zxFwPricingStore.loadContract(props.contractId)
    const data = zxFwPricingStore.getContractState(props.contractId)
    const idsFromStorage = data?.selectedIds
      || (data?.selectedCodes || []).map(code => serviceIdByCode.value.get(code)).filter((id): id is string => Boolean(id))
    await applySelection(idsFromStorage || [])
    await ensurePricingDetailRowsForCurrentSelection()
  } catch (error) {
    console.error('initializeContractState failed:', error)
    await setCurrentContractState({
      selectedIds: [],
      detailRows: applyFixedRowTotals([{
        id: fixedBudgetRow.id,
        code: fixedBudgetRow.code,
        name: fixedBudgetRow.name,
        process: null,
        investScale: null,
        landScale: null,
        workload: null,
        hourly: null,
        subtotal: null,
        actions: null
      }])
    })
  }
}

const loadProjectIndustry = async () => {
  try {
    const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY.value)
    projectIndustry.value =
      typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''
  } catch (error) {
    console.error('loadProjectIndustry failed:', error)
    projectIndustry.value = ''
  }
}

watch(serviceIdSignature, () => {
  const availableIds = new Set(serviceDict.value.map(item => item.id))
  const nextSelectedIds = selectedIds.value.filter(id => availableIds.has(id))
  if (nextSelectedIds.length !== selectedIds.value.length) {
    void applySelection(nextSelectedIds)
  }
})

const handleCellValueChanged = () => {}

onMounted(async () => {
  await loadProjectIndustry()
  await initializeContractState()
})

onActivated(async () => {
  await loadProjectIndustry()
  await initializeContractState()
})

onBeforeUnmount(() => {
  stopDragSelect()
})
</script>

<template>
  <TooltipProvider>
    <div class="h-full min-h-0 flex flex-col gap-2">
      <!-- 原“浏览框选择服务”实现已抽离并停用，改为直接复选框勾选 -->
      <!-- <DialogRoot v-model:open="pickerOpen" @update:open="handlePickerOpenChange" /> -->
      <ServiceCheckboxSelector :services="serviceDict" :model-value="selectedIds"
        @update:model-value="handleServiceSelectionChange" />

      <div class="rounded-lg border bg-card xmMx flex min-h-0 flex-1 flex-col overflow-hidden">
        <div class="flex items-center justify-between border-b px-3 py-2">
          <h3 class="text-xs font-semibold text-foreground leading-none">
            咨询服务明细
          </h3>
          <div class="text-[11px] text-muted-foreground leading-none">按服务词典生成</div>
        </div>

        <div :class="agGridWrapClass">
          <AgGridVue :style="agGridStyle" :rowData="detailRows" :columnDefs="columnDefs"
            :gridOptions="detailGridOptions" :theme="myTheme" @cell-value-changed="handleCellValueChanged"
            :enableClipboard="true" :localeText="AG_GRID_LOCALE_CN" :tooltipShowDelay="500" :headerHeight="30"
            :undoRedoCellEditing="true" :undoRedoCellEditingLimit="20" />
        </div>
      </div>

      <AlertDialogRoot :open="clearConfirmOpen" @update:open="handleClearConfirmOpenChange">
        <AlertDialogPortal>
          <AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
          <AlertDialogContent
            class="fixed left-1/2 top-1/2 z-[70] 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">
              会使用合同卡片里面最新填写的规模信息以及系数，自动计算默认数据，覆盖“{{ pendingClearServiceName }}”当前数据，是否继续？
            </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="confirmClearRow">确认恢复</Button>
              </AlertDialogAction>
            </div>
          </AlertDialogContent>
        </AlertDialogPortal>
      </AlertDialogRoot>

      <AlertDialogRoot :open="deleteConfirmOpen" @update:open="handleDeleteConfirmOpenChange">
        <AlertDialogPortal>
          <AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
          <AlertDialogContent
            class="fixed left-1/2 top-1/2 z-[70] 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">
              将逻辑删除“{{ pendingDeleteServiceName }}”，已填写的数据不会清空，重新勾选后会恢复，是否继续？
            </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="confirmDeleteRow">确认删除</Button>
              </AlertDialogAction>
            </div>
          </AlertDialogContent>
        </AlertDialogPortal>
      </AlertDialogRoot>

    </div>
  </TooltipProvider>
</template>

<style scoped>
:deep(.zxfw-process-header .ag-header-cell-label) {
  justify-content: center;
}

:deep(.zxfw-process-header .ag-header-cell-text) {
  text-align: center;
}
</style>
