<script setup lang="ts">
import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import draggable from 'vuedraggable'
import { Card, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area'
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip'
import { useTabStore } from '@/pinia/tab'
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
import { useZxFwPricingKeysStore } from '@/pinia/zxFwPricingKeys'
import { useZxFwPricingHtFeeStore } from '@/pinia/zxFwPricingHtFee'
import { useKvStore } from '@/pinia/kv'
import { ArrowUp, Edit3, GripVertical, MoreHorizontal, Plus, Trash2, X } from 'lucide-vue-next'
import { decodeZwArchive, encodeZwArchive } from '@/lib/zwArchive'
import {
  formatDateTime,
  isEntryRelatedToAnyContract,
  normalizeContractsFromPayload,
  normalizeOrder,
  type ContractItem
} from '@/features/ht/contracts'
import {
  applyImportedContractPiniaPayload,
  buildContractPiniaPayload,
  readContractRelatedForageEntries,
  readContractRelatedKeyedEntries
} from '@/features/ht/importExport'
import type {
  HourlyMethodStateLike,
  HtFeeMainRowLike,
  QuantityMethodStateLike,
  RateMethodStateLike,
  XmBaseInfoState,
  XmScaleState
} from '@/features/ht/types'
import {
  cloneJson,
  CONTRACT_CONSULT_FACTOR_KEY_PREFIX,
  CONTRACT_KEY_PREFIX,
  CONTRACT_MAJOR_FACTOR_KEY_PREFIX,
  CONTRACT_SEGMENT_FILE_EXTENSION,
  CONTRACT_SEGMENT_VERSION,
  formatExportTimestamp,
  generateContractId,
  isContractRelatedForageKey,
  isContractRelatedKeyedStateKey,
  isContractSegmentPackage,
  normalizeContractSegmentPackage,
  PROJECT_INFO_KEY,
  PROJECT_SCALE_KEY,
  PRICING_KEY_PREFIXES,
  rewriteKeyWithContractId,
  SERVICE_KEY_PREFIX,
  type ContractSegmentPackage
} from '@/lib/contractSegment'
import { buildDefaultProjectScaleState } from '@/lib/projectWorkspace'
import { getIndustryDisplayName, industryTypeList } from '@/sql'
import { roundTo, sumNullableNumbers, toFiniteNumber } from '@/lib/decimal'
import { formatThousands } from '@/lib/numberFormat'
import {
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogOverlay,
  AlertDialogPortal,
  AlertDialogRoot,
  AlertDialogTitle,
  ToastAction,
  ToastDescription,
  ToastProvider,
  ToastRoot,
  ToastTitle,
  ToastViewport
} from 'reka-ui'
import {useDataStore} from '@/pinia/zx'

const STORAGE_KEY = 'ht-card-v1'
const tabStore = useTabStore()
const zxFwPricingStore = useZxFwPricingStore()
const zxFwPricingKeysStore = useZxFwPricingKeysStore()
const zxFwPricingHtFeeStore = useZxFwPricingHtFeeStore()
const kvStore = useKvStore()
const { t, locale } = useI18n()



const contracts = ref<ContractItem[]>([])
const contractSearchKeyword = ref('')
const isListLayout = ref(false)
const contractDataMenuOpen = ref(false)
const contractDataMenuRef = ref<HTMLElement | null>(null)
const contractImportFileRef = ref<HTMLInputElement | null>(null)
const selectionMode = ref<'none' | 'export' | 'delete'>('none')
const selectedContractIds = ref<string[]>([])

const showCreateModal = ref(false)
const contractNameInput = ref('')
const editingContractId = ref<string | null>(null)
const toastOpen = ref(false)
const toastTitle = ref(t('ht.toastSuccessTitle'))
const toastText = ref('')
const deleteConfirmOpen = ref(false)
const pendingDeleteContractId = ref<string | null>(null)
const messageDialogOpen = ref(false)
const messageDialogTitle = ref('')
const messageDialogDesc = ref('')
const batchDeleteConfirmOpen = ref(false)
const modalOffset = ref({ x: 0, y: 0 })
let dragStartX = 0
let dragStartY = 0
let baseOffsetX = 0
let baseOffsetY = 0
const contractListScrollWrapRef = ref<HTMLElement | null>(null)
const contractListViewportRef = ref<HTMLElement | null>(null)
const showScrollTopFab = ref(false)
const isDraggingContracts = ref(false)
const cardMotionState = ref<'enter' | 'ready'>('ready')
const canManageContracts = ref(true)
const contractBudgetById = ref<Record<string, number | null>>({})
const contractBudgetLoading = ref(false)
let contractBudgetRefreshSeq = 0
let contractAutoScrollRaf = 0
let dragPointerClientY: number | null = null
let cardEnterTimer: ReturnType<typeof setTimeout> | null = null
let contractListScrollBoundEl: HTMLElement | null = null
let budgetRefreshTimer: ReturnType<typeof setTimeout> | null = null

const CARD_ENTER_STEP_MS = 58
const CARD_ENTER_DURATION_MS = 560
const CARD_ENTER_MAX_INDEX = 24
const CARD_ENTER_TOTAL_MS = CARD_ENTER_DURATION_MS + CARD_ENTER_STEP_MS * CARD_ENTER_MAX_INDEX + 80
const SCROLL_TOP_FAB_THRESHOLD = 220

const buildDefaultContracts = (): ContractItem[] => [
  
]

const normalizedSearchKeyword = computed(() => contractSearchKeyword.value.trim().toLowerCase())
const isSelectingContracts = computed(() => selectionMode.value !== 'none')
const selectedContractCount = computed(() => selectedContractIds.value.length)
const hasContracts = computed(() => contracts.value.length > 0)
const filteredContracts = computed(() => {
  if (!normalizedSearchKeyword.value) return contracts.value
  return contracts.value.filter(item => {
    const name = item.name.toLowerCase()
    const id = item.id.toLowerCase()
    return name.includes(normalizedSearchKeyword.value) || id.includes(normalizedSearchKeyword.value)
  })
})
const isSearchingContracts = computed(() => Boolean(normalizedSearchKeyword.value))
const projectTotalBudget = computed(() => {
  let hasValid = false
  let total = 0
  for (const contract of contracts.value) {
    const fee = contractBudgetById.value[contract.id]
    if (typeof fee !== 'number' || !Number.isFinite(fee)) continue
    total += fee
    hasValid = true
  }
  return hasValid ? roundTo(total, 2) : null
})
const budgetRefreshSignature = computed(() => {
  const ids = contracts.value.map(item => String(item.id || '').trim()).filter(Boolean)
  if (ids.length === 0) return ''
  return ids
    .map(id => {
      const additionalMainKey = `htExtraFee-${id}-additional-work`
      const reserveMainKey = `htExtraFee-${id}-reserve`
      const contractState = zxFwPricingStore.contracts[id] || null
      const addMain = zxFwPricingStore.htFeeMainStates[additionalMainKey] || null
      const reserveMain = zxFwPricingStore.htFeeMainStates[reserveMainKey] || null
      const addMethods = zxFwPricingStore.htFeeMethodStates[additionalMainKey] || null
      const reserveMethods = zxFwPricingStore.htFeeMethodStates[reserveMainKey] || null
      return JSON.stringify({
        id,
        contractState,
        addMain,
        reserveMain,
        addMethods,
        reserveMethods
      })
    })
    .join('|')
})

const notify = (text: string) => {
  toastTitle.value = t('ht.toastSuccessTitle')
  toastText.value = text
  toastOpen.value = false
  requestAnimationFrame(() => {
    toastOpen.value = true
  })
}

const showMessageDialog = (title: string, description: string) => {
  messageDialogTitle.value = title
  messageDialogDesc.value = description
  messageDialogOpen.value = true
}

const pendingDeleteContractName = computed(() => {
  if (!pendingDeleteContractId.value) return ''
  const target = contracts.value.find(item => item.id === pendingDeleteContractId.value)
  return target?.name || pendingDeleteContractId.value
})

const batchDeleteCount = computed(() => {
  const selectedSet = new Set(selectedContractIds.value)
  return contracts.value.filter(item => selectedSet.has(item.id)).length
})

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

const requestDeleteContract = (id: string) => {
  pendingDeleteContractId.value = id
  deleteConfirmOpen.value = true
}

const confirmDeleteContract = async () => {
  const id = pendingDeleteContractId.value
  if (!id) return
  await deleteContract(id)
  deleteConfirmOpen.value = false
  pendingDeleteContractId.value = null
}

const closeContractDataMenu = () => {
  contractDataMenuOpen.value = false
}

const loadProjectBaseState = async () => {
  try {
    const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
    canManageContracts.value = Boolean(typeof data?.projectIndustry === 'string' && data.projectIndustry.trim())
  } catch (error) {
    console.error('load project base state failed:', error)
    canManageContracts.value = false
  }
}

const getCurrentProjectIndustry = async (): Promise<string> => {
  const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
  return typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''
}

const formatBudgetAmount = (value: number | null | undefined) =>
  typeof value === 'number' && Number.isFinite(value)
    ? `${formatThousands(value, 2)} ${t('htCard.currencySuffix')}`
    : '--'

const sumHourlyMethodFee = (state: HourlyMethodStateLike | null): number | null => {
  const rows = Array.isArray(state?.detailRows) ? state.detailRows : []
  if (rows.length === 0) return null
  let hasValid = false
  let total = 0
  for (const row of rows) {
    const serviceBudget = toFiniteNumber(row?.serviceBudget)
    if (serviceBudget != null) {
      total += serviceBudget
      hasValid = true
      continue
    }
    const adopted = toFiniteNumber(row?.adoptedBudgetUnitPrice)
    const personnel = toFiniteNumber(row?.personnelCount)
    const workday = toFiniteNumber(row?.workdayCount)
    if (adopted == null || personnel == null || workday == null) continue
    total += adopted * personnel * workday
    hasValid = true
  }
  return hasValid ? roundTo(total, 2) : null
}

const sumQuantityMethodFee = (state: QuantityMethodStateLike | null): number | null => {
  const rows = Array.isArray(state?.detailRows) ? state.detailRows : []
  if (rows.length === 0) return null
  const subtotalRow = rows.find(row => String(row?.id || '') === 'fee-subtotal-fixed')
  const subtotal = toFiniteNumber(subtotalRow?.budgetFee)
  if (subtotal != null) return roundTo(subtotal, 2)
  let hasValid = false
  let total = 0
  for (const row of rows) {
    if (String(row?.id || '') === 'fee-subtotal-fixed') continue
    const budget = toFiniteNumber(row?.budgetFee)
    if (budget != null) {
      total += budget
      hasValid = true
      continue
    }
    const quantity = toFiniteNumber(row?.quantity)
    const unitPrice = toFiniteNumber(row?.unitPrice)
    if (quantity == null || unitPrice == null) continue
    total += quantity * unitPrice
    hasValid = true
  }
  return hasValid ? roundTo(total, 2) : null
}

const loadHtMethodTotalByRow = async (mainStorageKey: string, rowId: string) => {
  const [rateState, hourlyState, quantityState] = await Promise.all([
    zxFwPricingStore.loadHtFeeMethodState<RateMethodStateLike>(mainStorageKey, rowId, 'rate-fee'),
    zxFwPricingStore.loadHtFeeMethodState<HourlyMethodStateLike>(mainStorageKey, rowId, 'hourly-fee'),
    zxFwPricingStore.loadHtFeeMethodState<QuantityMethodStateLike>(mainStorageKey, rowId, 'quantity-unit-price-fee')
  ])

  const parts = [
    toFiniteNumber(rateState?.budgetFee),
    sumHourlyMethodFee(hourlyState),
    sumQuantityMethodFee(quantityState)
  ]
  const total = sumNullableNumbers(parts)
  return total == null ? null : roundTo(total, 2)
}

const loadHtMainTotalFee = async (mainStorageKey: string) => {
  const mainState = await zxFwPricingStore.loadHtFeeMainState<HtFeeMainRowLike>(mainStorageKey)
  const rows = Array.isArray(mainState?.detailRows) ? mainState.detailRows : []
  const rowIds = rows
    .map(row => String(row?.id || '').trim())
    .filter(Boolean)
  if (rowIds.length === 0) return null
  const rowTotals = await Promise.all(rowIds.map(rowId => loadHtMethodTotalByRow(mainStorageKey, rowId)))
  const total = sumNullableNumbers(rowTotals)
  return total == null ? null : roundTo(total, 2)
}

const loadContractBudgetFee = async (contractId: string) => {
  await zxFwPricingStore.loadContract(contractId)
  const serviceFee = zxFwPricingStore.getBaseSubtotal(contractId)
  const [additionalFee, reserveFee] = await Promise.all([
    loadHtMainTotalFee(`htExtraFee-${contractId}-additional-work`),
    loadHtMainTotalFee(`htExtraFee-${contractId}-reserve`)
  ])

  const zxRows = await useDataStore().query([
    { field: 'type', value: `${contractId}-zxFw`, operator: 'eq' }
  ])
  const totalFinalFee = zxRows.reduce((sum, row) => {
    return sum + (Number(row.finalFee) || 0)
  }, 0)

  const total = totalFinalFee;

  /*const parts = [serviceFee, additionalFee, reserveFee]
  const total = sumNullableNumbers(parts)*/
  return total == null ? null : roundTo(total, 2)
}

const refreshContractBudgets = async () => {
  const seq = ++contractBudgetRefreshSeq
  const ids = contracts.value.map(item => String(item.id || '').trim()).filter(Boolean)
  if (ids.length === 0) {
    contractBudgetById.value = {}
    contractBudgetLoading.value = false
    return
  }
  contractBudgetLoading.value = true
  try {
    const nextPairs = await Promise.all(
      ids.map(async id => {
        const fee = await loadContractBudgetFee(id)
        return [id, fee] as const
      })
    )
    if (seq !== contractBudgetRefreshSeq) return
    contractBudgetById.value = Object.fromEntries(nextPairs)
  } catch (error) {
    console.error('refresh contract budgets failed:', error)
  } finally {
    if (seq === contractBudgetRefreshSeq) {
      contractBudgetLoading.value = false
    }
  }
}

const scheduleRefreshContractBudgets = () => {
  if (budgetRefreshTimer) clearTimeout(budgetRefreshTimer)
  budgetRefreshTimer = setTimeout(() => {
    void refreshContractBudgets()
  }, 80)
}

const formatIndustryLabel = (code: string) => {
  const trimmed = code.trim()
  const target = industryTypeList.find(item => String(item.id || '').trim() === trimmed)
  const name = target ? getIndustryDisplayName(target.id, locale.value) : ''
  return name ? `${trimmed} ${name}` : trimmed
}

const isContractSelected = (contractId: string) =>
  selectedContractIds.value.includes(contractId)

const toggleContractSelection = (contractId: string) => {
  if (!isSelectingContracts.value) return
  if (isContractSelected(contractId)) {
    selectedContractIds.value = selectedContractIds.value.filter(id => id !== contractId)
    return
  }
  selectedContractIds.value = [...selectedContractIds.value, contractId]
}

const exitContractSelectionMode = () => {
  selectionMode.value = 'none'
  selectedContractIds.value = []
}

const enterContractExportMode = () => {
  if (!hasContracts.value) return
  closeContractDataMenu()
  selectionMode.value = 'export'
  selectedContractIds.value = []
}

const enterContractDeleteMode = () => {
  if (!hasContracts.value) return
  closeContractDataMenu()
  selectionMode.value = 'delete'
  selectedContractIds.value = []
}

const triggerContractImport = () => {
  if (!canManageContracts.value) return
  closeContractDataMenu()
  contractImportFileRef.value?.click()
}

const getContractListViewport = () => {
  const viewport = contractListScrollWrapRef.value?.querySelector<HTMLElement>(
    '[data-slot="scroll-area-viewport"]'
  ) || null
  contractListViewportRef.value = viewport
  return viewport
}

const scrollContractsToBottom = (behavior: ScrollBehavior = 'smooth') => {
  const viewport = getContractListViewport()
  if (!viewport) return
  viewport.scrollTo({
    top: viewport.scrollHeight,
    behavior
  })
}

const scrollContractsToTop = (behavior: ScrollBehavior = 'smooth') => {
  const viewport = getContractListViewport()
  if (!viewport) return
  viewport.scrollTo({
    top: 0,
    behavior
  })
}

const updateScrollTopFabVisible = () => {
  const viewport = contractListViewportRef.value
  showScrollTopFab.value = Boolean(viewport && viewport.scrollTop > SCROLL_TOP_FAB_THRESHOLD)
}

const handleContractListScroll = () => {
  updateScrollTopFabVisible()
}

const bindContractListScroll = () => {
  const viewport = getContractListViewport()
  if (contractListScrollBoundEl === viewport) {
    updateScrollTopFabVisible()
    return
  }
  if (contractListScrollBoundEl) {
    contractListScrollBoundEl.removeEventListener('scroll', handleContractListScroll)
    contractListScrollBoundEl = null
  }
  if (!viewport) {
    showScrollTopFab.value = false
    return
  }
  contractListScrollBoundEl = viewport
  contractListScrollBoundEl.addEventListener('scroll', handleContractListScroll, { passive: true })
  updateScrollTopFabVisible()
}

const unbindContractListScroll = () => {
  if (contractListScrollBoundEl) {
    contractListScrollBoundEl.removeEventListener('scroll', handleContractListScroll)
    contractListScrollBoundEl = null
  }
  showScrollTopFab.value = false
}

const triggerCardEnterAnimation = () => {
  if (cardEnterTimer) {
    clearTimeout(cardEnterTimer)
    cardEnterTimer = null
  }
  cardMotionState.value = 'enter'
  cardEnterTimer = setTimeout(() => {
    cardMotionState.value = 'ready'
    cardEnterTimer = null
  }, CARD_ENTER_TOTAL_MS)
}

const getCardEnterStyle = (index: number) => ({
  '--ht-card-enter-delay': `${Math.min(index, CARD_ENTER_MAX_INDEX) * CARD_ENTER_STEP_MS}ms`
})

const getCardSelectStyle = (index: number) => ({
  '--ht-card-select-delay': `${(index % 10) * 70}ms`
})

const getContractCardStyle = (index: number) => {
  if (isSelectingContracts.value) {
    return getCardSelectStyle(index)
  }
  if (cardMotionState.value === 'enter') {
    return getCardEnterStyle(index)
  }
  return undefined
}

const saveContracts = async () => {
  try {
    contracts.value = normalizeOrder(contracts.value)
    await kvStore.setItem(STORAGE_KEY, JSON.parse(JSON.stringify(contracts.value)))
  } catch (error) {
    console.error('save contracts failed:', error)
  }
}

const initializeContractScaleData = async (contractId: string) => {
  const [source, projectInfo] = await Promise.all([
    kvStore.getItem<XmScaleState>(PROJECT_SCALE_KEY),
    kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
  ])
  const industry = typeof projectInfo?.projectIndustry === 'string' ? projectInfo.projectIndustry.trim() : ''
  const payload: XmScaleState = industry
    ? buildDefaultProjectScaleState(industry, source)
    : {
        detailRows: Array.isArray(source?.detailRows) ? cloneJson(source.detailRows) : [],
        roughCalcEnabled: Boolean(source?.roughCalcEnabled),
        totalAmount:
          typeof source?.totalAmount === 'number' && Number.isFinite(source.totalAmount)
            ? source.totalAmount
            : null
      }
  await kvStore.setItem(`${CONTRACT_KEY_PREFIX}${contractId}`, payload)
}

const exportSelectedContracts = async () => {
  if (selectedContractIds.value.length === 0) {
    showMessageDialog(t('ht.tipTitle'), t('ht.selectAtLeastOne'))
    return
  }

  try {
    const selectedSet = new Set(selectedContractIds.value)
    const selectedContracts = contracts.value
      .filter(item => selectedSet.has(item.id))
      .map((item, index) => ({
        ...item,
        order: index
      }))

    const localforageEntries = await readContractRelatedForageEntries(
      kvStore,
      selectedContracts.map(item => item.id)
    )
    const keyedEntries = readContractRelatedKeyedEntries(
      zxFwPricingStore,
      selectedContracts.map(item => item.id)
    )
    const piniaPayload = await buildContractPiniaPayload(zxFwPricingStore, selectedContracts.map(item => item.id))

    const projectIndustry = await getCurrentProjectIndustry()
    if (!projectIndustry) {
      showMessageDialog(t('ht.exportFailedTitle'), t('ht.industryMissingForExport'))
      return
    }

    const now = new Date()
    const payload: ContractSegmentPackage = {
      version: CONTRACT_SEGMENT_VERSION,
      packageType: 'contract-segments',
      exportedAt: now.toISOString(),
      project: {
        industry: projectIndustry
      },
      contracts: selectedContracts,
      storage: {
        localforageEntries,
        keyedEntries
      },
      pinia: {
        zxFwPricing: piniaPayload
      }
    }

    const content = await encodeZwArchive(payload)
    const binary = new Uint8Array(content.length)
    binary.set(content)
    const blob = new Blob([binary], { type: 'application/octet-stream' })
    const url = URL.createObjectURL(blob)
    const link = document.createElement('a')
    link.href = url
    link.download = `contract-segments-${formatExportTimestamp(now)}${CONTRACT_SEGMENT_FILE_EXTENSION}`
    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
    URL.revokeObjectURL(url)

    notify(t('ht.exportSuccess', { count: selectedContracts.length }))
    exitContractSelectionMode()
  } catch (error) {
    console.error('export selected contracts failed:', error)
    showMessageDialog(t('ht.exportFailedTitle'), t('ht.retry'))
  }
}

const importContractSegments = async (event: Event) => {
  const input = event.target as HTMLInputElement
  const file = input.files?.[0]
  if (!file) return

  try {
    const name = file.name.toLowerCase()
    if (!name.endsWith(CONTRACT_SEGMENT_FILE_EXTENSION) && !name.endsWith('.zw')) {
      throw new Error('INVALID_FILE_EXTENSION')
    }

    const buffer = await file.arrayBuffer()
    const payload = await decodeZwArchive<ContractSegmentPackage>(buffer)
    if (!isContractSegmentPackage(payload)) {
      throw new Error('INVALID_CONTRACT_SEGMENT_PAYLOAD')
    }
    const normalizedPackage = normalizeContractSegmentPackage(payload)

    const currentProjectIndustry = await getCurrentProjectIndustry()
    if (!currentProjectIndustry) {
      throw new Error('CURRENT_PROJECT_INDUSTRY_MISSING')
    }
    if (!normalizedPackage.projectIndustry) {
      throw new Error('IMPORT_PACKAGE_INDUSTRY_MISSING')
    }
    if (normalizedPackage.projectIndustry !== currentProjectIndustry) {
      throw new Error(`PROJECT_INDUSTRY_MISMATCH:${normalizedPackage.projectIndustry}:${currentProjectIndustry}`)
    }

    const importedContracts = normalizeContractsFromPayload(payload.contracts)
    if (importedContracts.length === 0) {
      throw new Error('EMPTY_CONTRACTS')
    }

    const importedEntries = normalizedPackage.localforageEntries
    const importedKeyedEntries = normalizedPackage.keyedEntries
    const importedContractIdSet = new Set(importedContracts.map(item => String(item.id || '').trim()).filter(Boolean))
    const filteredImportedEntries = importedEntries.filter(entry =>
      isEntryRelatedToAnyContract(entry.key, importedContractIdSet, isContractRelatedForageKey)
    )
    const filteredImportedKeyedEntries = importedKeyedEntries.filter(entry =>
      isEntryRelatedToAnyContract(entry.key, importedContractIdSet, isContractRelatedKeyedStateKey)
    )
    const usedIds = new Set(contracts.value.map(item => item.id))
    const oldToNewIdMap = new Map<string, string>()
    const nextContracts: ContractItem[] = importedContracts.map((item, index) => {
      const nextId = generateContractId(usedIds)
      oldToNewIdMap.set(item.id, nextId)
      return {
        ...item,
        id: nextId,
        order: contracts.value.length + index,
        createdAt: item.createdAt || new Date().toISOString()
      }
    })

    const rewrittenEntries = filteredImportedEntries.map(entry => {
      let nextKey = entry.key
      for (const [oldId, newId] of oldToNewIdMap.entries()) {
        if (!nextKey.includes(oldId)) continue
        nextKey = rewriteKeyWithContractId(nextKey, oldId, newId)
      }
      return {
        key: nextKey,
        value: entry.value
      }
    })

    const rewrittenKeyedEntries = filteredImportedKeyedEntries.map(entry => {
      let nextKey = entry.key
      for (const [oldId, newId] of oldToNewIdMap.entries()) {
        if (!nextKey.includes(oldId)) continue
        nextKey = rewriteKeyWithContractId(nextKey, oldId, newId)
      }
      return {
        key: nextKey,
        value: entry.value
      }
    })

    await Promise.all(rewrittenEntries.map(entry => kvStore.setItem(entry.key, entry.value)))
    for (const entry of rewrittenKeyedEntries) {
      zxFwPricingStore.setKeyState(entry.key, cloneJson(entry.value), { force: true })
    }
    await applyImportedContractPiniaPayload(zxFwPricingStore, normalizedPackage.piniaState, oldToNewIdMap)

    contracts.value = [...contracts.value, ...nextContracts]
    await saveContracts()
    await Promise.all([
      zxFwPricingStore.$persistNow?.(),
      zxFwPricingKeysStore.$persistNow?.(),
      zxFwPricingHtFeeStore.$persistNow?.()
    ])
    await refreshContractBudgets()
    notify(t('ht.importSuccess', { count: nextContracts.length }))
    await nextTick()
    scrollContractsToBottom()
  } catch (error) {
    console.error('import contract segments failed:', error)
    const message = error instanceof Error ? error.message : ''
    if (message.startsWith('PROJECT_INDUSTRY_MISMATCH:')) {
      const [, importIndustry = '', currentIndustry = ''] = message.split(':')
      showMessageDialog(
        t('ht.importFailedTitle'),
        t('ht.importIndustryMismatch', {
          importIndustry: formatIndustryLabel(importIndustry),
          currentIndustry: formatIndustryLabel(currentIndustry)
        })
      )
    } else if (message === 'CURRENT_PROJECT_INDUSTRY_MISSING') {
      showMessageDialog(t('ht.importFailedTitle'), t('ht.importCurrentIndustryMissing'))
    } else if (message === 'IMPORT_PACKAGE_INDUSTRY_MISSING') {
      showMessageDialog(t('ht.importFailedTitle'), t('ht.importPackageIndustryMissing'))
    } else if (message === 'ZW_CRYPTO_UNAVAILABLE') {
      showMessageDialog(t('ht.importFailedTitle'), t('ht.importCryptoUnavailable'))
    } else {
      showMessageDialog(t('ht.importFailedTitle'), t('ht.importFileInvalid'))
    }
  } finally {
    input.value = ''
  }
}

const removeForageKeysByContractId = async (store: typeof kvStore, contractId: string) => {
  try {
    const keys = await store.keys()
    const targetKeys = keys.filter(key => key.includes(contractId))
    await Promise.all(targetKeys.map(key => store.removeItem(key)))
  } catch (error) {
    console.error('remove forage keys by contract id failed:', contractId, error)
  }
}

const removeRelatedTabsByContractId = (contractId: string) => {
  const relatedTabIds = tabStore.tabs
    .filter(tab => {
      const propsContractId = tab?.props?.contractId
      return (
        tab.id === `contract-${contractId}` ||
        tab.id.startsWith(`zxfw-edit-${contractId}-`) ||
        propsContractId === contractId
      )
    })
    .map(tab => tab.id)

  for (const tabId of relatedTabIds) {
    tabStore.removeTab(tabId)
  }
}

const cleanupContractRelatedData = async (contractId: string) => {
  zxFwPricingStore.removeContractData(contractId)
  await Promise.all([
    removeForageKeysByContractId(kvStore, contractId),
  ])
  await Promise.all([
    zxFwPricingStore.$persistNow?.(),
    zxFwPricingKeysStore.$persistNow?.(),
    zxFwPricingHtFeeStore.$persistNow?.()
  ])
}

const loadContracts = async () => {
  try {
    const saved = await kvStore.getItem<ContractItem[]>(STORAGE_KEY)
    if (!saved || saved.length === 0) {
      contracts.value = buildDefaultContracts()
      await saveContracts()
      return
    }

    const sorted = [...saved].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
    contracts.value = normalizeOrder(sorted)
  } catch (error) {
    console.error('load contracts failed:', error)
    contracts.value = buildDefaultContracts()
  }
}

const openCreateModal = () => {
  if (!canManageContracts.value) return
  closeContractDataMenu()
  editingContractId.value = null
  contractNameInput.value = ''
  modalOffset.value = { x: 0, y: 0 }
  showCreateModal.value = true
}

const openEditModal = (item: ContractItem) => {
  closeContractDataMenu()
  editingContractId.value = item.id
  contractNameInput.value = item.name
  modalOffset.value = { x: 0, y: 0 }
  showCreateModal.value = true
}

const closeCreateModal = () => {
  showCreateModal.value = false
  editingContractId.value = null
  contractNameInput.value = ''
  modalOffset.value = { x: 0, y: 0 }
}

const createContract = async () => {
  const name = contractNameInput.value.trim()
  if (!name) return

  if (editingContractId.value) {
    const current = contracts.value.find(item => item.id === editingContractId.value)
    if (!current) return
    if (current.name === name) {
      closeCreateModal()
      return
    }

    contracts.value = contracts.value.map(item =>
      item.id === editingContractId.value ? { ...item, name } : item
    )
    await saveContracts()
    notify(t('ht.editSuccess'))
    closeCreateModal()
    return
  }

  const newContract: ContractItem = {
    id: `ct-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`,
    name,
    order: contracts.value.length,
    createdAt: new Date().toISOString()
  }
  contracts.value = [...contracts.value, newContract]

  await saveContracts()
  try {
    await initializeContractScaleData(newContract.id)
  } catch (error) {
    console.error('initialize contract scale failed:', error)
  }
  await refreshContractBudgets()
  notify(t('ht.createSuccess'))
  closeCreateModal()
  await nextTick()
  scrollContractsToBottom()
}

const deleteContract = async (id: string) => {
  // 先关闭合同段及其咨询服务相关 tab，避免页面仍在运行时继续写回缓存
  removeRelatedTabsByContractId(id)
  await nextTick()
  await cleanupContractRelatedData(id)
  // 组件卸载时会异步保存一次，延迟再清一遍避免数据回写
  await new Promise(resolve => setTimeout(resolve, 80))
  await cleanupContractRelatedData(id)

  contracts.value = contracts.value.filter(item => item.id !== id)
  selectedContractIds.value = selectedContractIds.value.filter(item => item !== id)
  await saveContracts()
  await refreshContractBudgets()
  notify(t('ht.deleteSuccess'))
}

const deleteSelectedContracts = async () => {
  if (selectedContractIds.value.length === 0) {
    showMessageDialog(t('ht.tipTitle'), t('ht.selectAtLeastOne'))
    return
  }

  const selectedSet = new Set(selectedContractIds.value)
  const targets = contracts.value.filter(item => selectedSet.has(item.id))
  if (targets.length === 0) {
    showMessageDialog(t('ht.tipTitle'), t('ht.noContractsToDelete'))
    return
  }
  batchDeleteConfirmOpen.value = true
}

const confirmDeleteSelectedContracts = async () => {
  const selectedSet = new Set(selectedContractIds.value)
  const targets = contracts.value.filter(item => selectedSet.has(item.id))
  if (targets.length === 0) {
    batchDeleteConfirmOpen.value = false
    showMessageDialog(t('ht.tipTitle'), t('ht.noContractsToDelete'))
    return
  }

  try {
    const targetIds = targets.map(item => item.id)
    for (const id of targetIds) {
      removeRelatedTabsByContractId(id)
    }
    await nextTick()
    for (const id of targetIds) {
      await cleanupContractRelatedData(id)
    }
    await new Promise(resolve => setTimeout(resolve, 80))
    for (const id of targetIds) {
      await cleanupContractRelatedData(id)
    }

    contracts.value = contracts.value.filter(item => !selectedSet.has(item.id))
    selectedContractIds.value = selectedContractIds.value.filter(item => !selectedSet.has(item))
    await saveContracts()
    await refreshContractBudgets()
    notify(t('ht.deleteBatchSuccess', { count: targetIds.length }))
    exitContractSelectionMode()
  } catch (error) {
    console.error('delete selected contracts failed:', error)
    showMessageDialog(t('ht.batchDeleteFailedTitle'), t('ht.retry'))
  } finally {
    batchDeleteConfirmOpen.value = false
  }
}

const handleDragEnd = async (event: { oldIndex?: number; newIndex?: number }) => {
  stopContractAutoScroll()
  if (
    event.oldIndex == null ||
    event.newIndex == null ||
    event.oldIndex === event.newIndex
  ) {
    return
  }

  await saveContracts()
  notify(t('ht.sortDone'))
}

const updateDragPointerPosition = (event: MouseEvent | DragEvent) => {
  if (!isDraggingContracts.value) return
  dragPointerClientY = event.clientY
}

const contractAutoScrollTick = () => {
  if (!isDraggingContracts.value) {
    contractAutoScrollRaf = 0
    return
  }

  const viewport = contractListViewportRef.value || getContractListViewport()
  const clientY = dragPointerClientY
  if (viewport && clientY != null) {
    const rect = viewport.getBoundingClientRect()
    const edge = 88
    const maxStep = 22
    let delta = 0

    if (clientY < rect.top + edge) {
      const ratio = Math.max(0, Math.min(1, (rect.top + edge - clientY) / edge))
      delta = -Math.ceil(maxStep * ratio)
    } else if (clientY > rect.bottom - edge) {
      const ratio = Math.max(0, Math.min(1, (clientY - (rect.bottom - edge)) / edge))
      delta = Math.ceil(maxStep * ratio)
    }

    if (delta !== 0) {
      viewport.scrollTop = Math.max(
        0,
        Math.min(viewport.scrollTop + delta, viewport.scrollHeight - viewport.clientHeight)
      )
    }
  }

  contractAutoScrollRaf = window.requestAnimationFrame(contractAutoScrollTick)
}

const startContractAutoScroll = () => {
  if (cardEnterTimer) {
    clearTimeout(cardEnterTimer)
    cardEnterTimer = null
  }
  cardMotionState.value = 'ready'
  getContractListViewport()
  isDraggingContracts.value = true
  dragPointerClientY = null
  window.addEventListener('pointermove', updateDragPointerPosition as EventListener, { passive: true })
  window.addEventListener('dragover', updateDragPointerPosition as EventListener, { passive: true })
  if (contractAutoScrollRaf) cancelAnimationFrame(contractAutoScrollRaf)
  contractAutoScrollRaf = window.requestAnimationFrame(contractAutoScrollTick)
}

const stopContractAutoScroll = () => {
  isDraggingContracts.value = false
  dragPointerClientY = null
  window.removeEventListener('pointermove', updateDragPointerPosition as EventListener)
  window.removeEventListener('dragover', updateDragPointerPosition as EventListener)
  if (contractAutoScrollRaf) {
    cancelAnimationFrame(contractAutoScrollRaf)
    contractAutoScrollRaf = 0
  }
}

const handleCardClick = (item: ContractItem) => {
  if (isSelectingContracts.value) {
    toggleContractSelection(item.id)
    return
  }
  tabStore.openTab({
    id: `contract-${item.id}`,
    title: t('ht.contractTabTitle', { name: item.name }),
    componentName: 'QuickCalcView',
    props: { contractId: item.id, contractName: item.name }
  })
}

const onDragMove = (event: MouseEvent) => {
  modalOffset.value = {
    x: baseOffsetX + (event.clientX - dragStartX),
    y: baseOffsetY + (event.clientY - dragStartY)
  }
}

const stopDrag = () => {
  window.removeEventListener('mousemove', onDragMove)
  window.removeEventListener('mouseup', stopDrag)
}

const startDrag = (event: MouseEvent) => {
  dragStartX = event.clientX
  dragStartY = event.clientY
  baseOffsetX = modalOffset.value.x
  baseOffsetY = modalOffset.value.y
  window.addEventListener('mousemove', onDragMove)
  window.addEventListener('mouseup', stopDrag)
}

const handleGlobalMouseDown = (event: MouseEvent) => {
  if (!contractDataMenuOpen.value || !contractDataMenuRef.value) return
  const target = event.target as Node
  if (!contractDataMenuRef.value.contains(target)) {
    contractDataMenuOpen.value = false
  }
}

onMounted(async () => {
  await loadProjectBaseState()
  await loadContracts()
  await refreshContractBudgets()
  triggerCardEnterAnimation()
  await nextTick()
  bindContractListScroll()
  window.addEventListener('mousedown', handleGlobalMouseDown)
})

onActivated(() => {
  void loadProjectBaseState()
  void refreshContractBudgets()
  triggerCardEnterAnimation()
  void nextTick(() => {
    bindContractListScroll()
  })
})

onBeforeUnmount(() => {
  stopDrag()
  stopContractAutoScroll()
  if (budgetRefreshTimer) clearTimeout(budgetRefreshTimer)
  unbindContractListScroll()
  window.removeEventListener('mousedown', handleGlobalMouseDown)
  if (cardEnterTimer) clearTimeout(cardEnterTimer)
  void saveContracts()
})

watch(budgetRefreshSignature, (next, prev) => {
  if (next === prev) return
  scheduleRefreshContractBudgets()
})
</script>

<template>
  <ToastProvider>
  <TooltipProvider>
  <div class="flex h-full min-h-0 flex-col overflow-hidden">
    <div class="relative z-30 shrink-0 overflow-visible border-b bg-background/95 px-1 pb-4 backdrop-blur supports-[backdrop-filter]:bg-background/80">
      <div class="mb-6 flex items-center justify-between pt-1">
        <div class="space-y-1">
          <h3 class="text-lg font-bold">{{ t('ht.title') }}</h3>
          <div class="text-xs text-muted-foreground">
            {{ t('ht.projectTotalBudget', { amount: contractBudgetLoading ? t('ht.budgetLoading') : formatBudgetAmount(projectTotalBudget) }) }}
          </div>
        </div>
        <div class="flex items-center gap-2">
          <template v-if="isSelectingContracts">
            <div class="text-xs text-muted-foreground">{{ t('ht.selectedCount', { count: selectedContractCount }) }}</div>
            <Button
              variant="outline"
              :disabled="selectedContractCount === 0"
              @click="selectionMode === 'export' ? exportSelectedContracts() : deleteSelectedContracts()"
            >
              {{ selectionMode === 'export' ? t('ht.exportSelected') : t('ht.deleteSelected') }}
            </Button>
            <Button variant="ghost" @click="exitContractSelectionMode">
              {{ t('ht.cancelSelect') }}
            </Button>
          </template>
          <template v-else>
            <Button class="whitespace-nowrap" :disabled="!canManageContracts" @click="openCreateModal">
              <Plus class="mr-2 h-4 w-4" />
              {{ t('ht.addContract') }}
            </Button>
            <div ref="contractDataMenuRef" class="relative">
              <Button
                variant="outline"
                size="icon"
                class="h-10 w-10"
                @click="contractDataMenuOpen = !contractDataMenuOpen"
              >
                <MoreHorizontal class="h-4 w-4" />
              </Button>
              <div
                v-if="contractDataMenuOpen"
                class="absolute right-0 top-full z-[80] mt-1 w-max rounded-md border bg-background p-1 shadow-md"
              >
                <button
                  class="block whitespace-nowrap rounded px-3 py-1.5 text-left text-sm"
                  :class="hasContracts ? 'cursor-pointer hover:bg-muted' : 'cursor-not-allowed text-muted-foreground'"
                  :disabled="!hasContracts"
                  @click="enterContractDeleteMode"
                >
                  {{ t('ht.batchDelete') }}
                </button>
                <button
                  class="block whitespace-nowrap rounded px-3 py-1.5 text-left text-sm"
                  :class="hasContracts ? 'cursor-pointer hover:bg-muted' : 'cursor-not-allowed text-muted-foreground'"
                  :disabled="!hasContracts"
                  @click="enterContractExportMode"
                >
                  {{ t('ht.exportContracts') }}
                </button>
              
                <button
                  class="block whitespace-nowrap rounded px-3 py-1.5 text-left text-sm"
                  :class="canManageContracts ? 'cursor-pointer hover:bg-muted' : 'cursor-not-allowed text-muted-foreground'"
                  :disabled="!canManageContracts"
                  @click="triggerContractImport"
                >
                  {{ t('ht.importContracts') }}
                </button>
              </div>
              <input
                ref="contractImportFileRef"
                type="file"
                class="hidden"
                accept=".htzw,.zw"
                @change="importContractSegments"
              />
            </div>
          </template>
        </div>
      </div>

      <div class="flex flex-col gap-2 md:flex-row md:items-start">
        <div class="w-full md:max-w-md">
          <div class="flex items-center gap-2">
            <input
              v-model="contractSearchKeyword"
              type="text"
              :placeholder="t('ht.searchPlaceholder')"
              class="h-10 w-full rounded-md border bg-background px-3 text-sm outline-none transition focus-visible:ring-2 focus-visible:ring-ring"
            />
            <Button
              v-if="contractSearchKeyword"
              variant="outline"
              size="sm"
              class="h-10 shrink-0 px-3"
              @click="contractSearchKeyword = ''"
            >
              {{ t('ht.clearFilter') }}
            </Button>
          </div>
          <div v-if="isSearchingContracts" class="mt-1 text-xs text-muted-foreground">
            {{ t('ht.searchingHint', { filtered: filteredContracts.length, total: contracts.length }) }}
          </div>
          <div v-if="isSelectingContracts" class="mt-1 text-xs text-muted-foreground">
            {{ selectionMode === 'export' ? t('ht.selectModeExportHint') : t('ht.selectModeDeleteHint') }}
          </div>
          <div v-if="!canManageContracts" class="mt-1 text-xs text-muted-foreground">
            {{ t('ht.setupRequiredHint') }}
          </div>
        </div>
        <div class="flex flex-wrap items-center gap-2 md:ml-auto">
          <label class="inline-flex cursor-pointer items-center gap-2 text-xs text-muted-foreground select-none">
            <span>{{ isListLayout ? t('ht.listLayout') : t('ht.gridLayout') }}</span>
            <button
              type="button"
              role="switch"
              :aria-checked="isListLayout"
              class="relative h-6 w-11 cursor-pointer rounded-full border transition-colors active:scale-95"
              :class="isListLayout ? 'bg-primary border-primary' : 'bg-muted border-border'"
              @click="isListLayout = !isListLayout"
            >
              <span
                class="pointer-events-none absolute top-1/2 h-4 w-4 -translate-y-1/2 rounded-full bg-background shadow-sm transition-all"
                :class="isListLayout ? 'left-6' : 'left-1'"
              />
            </button>
          </label>
        </div>
      </div>
    </div>

    <div ref="contractListScrollWrapRef" class="mt-4 flex-1 min-h-0">
      <!-- 合同卡片列表滚动容器：由 ScrollArea 承载滚动行为与滚动条样式。 -->
      <ScrollArea :class="['ht-contract-scroll-area h-full', isDraggingContracts && 'is-dragging']">
      <draggable
        v-if="!isSearchingContracts && filteredContracts.length > 0"
        :key="`contracts-${isListLayout ? 'list' : 'grid'}`"
        v-model="contracts"
        item-key="id"
        :disabled="isSelectingContracts"
        handle=".contract-drag-handle"
        ghost-class="ht-sortable-ghost"
        chosen-class="ht-sortable-chosen"
        drag-class="ht-sortable-drag"
        :class="[
          'grid grid-cols-1 pb-4 pr-4 pt-3',
          isListLayout ? 'gap-2' : 'gap-4',
          !isListLayout && 'md:grid-cols-2 lg:grid-cols-3'
        ]"
        animation="200"
        @start="startContractAutoScroll"
        @end="handleDragEnd"
      >
        <template #item="{ element, index }">
          <Card
            :class="[
              'group relative cursor-pointer snap-start snap-always transition-colors hover:border-primary ht-contract-card',
              isSelectingContracts
                ? 'ht-contract-card--selecting'
                : cardMotionState === 'enter' && !isDraggingContracts
                  ? 'ht-contract-card--enter'
                  : 'ht-contract-card--ready',
              isContractSelected(element.id) && 'ht-contract-card--selected',
              isListLayout && 'gap-0 py-0'
            ]"
            :style="getContractCardStyle(index)"
            @click="handleCardClick(element)"
          >
            <label
              v-if="isSelectingContracts"
              class="absolute left-2 top-2 z-10 inline-flex cursor-pointer items-center rounded bg-background/90 p-0.5 shadow-sm"
              @click.stop
            >
              <input
                type="checkbox"
                class="h-4 w-4 cursor-pointer"
                :checked="isContractSelected(element.id)"
                @change.stop="toggleContractSelection(element.id)"
              />
            </label>
            <CardHeader
              :class="[
                'flex flex-row items-center justify-between gap-0 space-y-0',
                isListLayout ? 'px-3 py-2' : 'pb-4'
              ]"
            >
              <CardTitle
                :class="[
                'text-sm font-medium',
                isListLayout && 'mr-1.5 flex min-w-0 flex-1 items-center gap-1.5'
              ]"
            >
                <span :class="isListLayout ? 'min-w-0 truncate' : ''">{{ element.name }}</span>
                <template v-if="isListLayout">
                  <span class="min-w-0 shrink text-[11px] leading-none font-normal text-muted-foreground truncate">
                    ID: {{ element.id }}
                  </span>
                  <span class="shrink-0 text-[11px] leading-none font-normal text-muted-foreground">
                    {{ t('ht.contractBudget', { amount: formatBudgetAmount(contractBudgetById[element.id]) }) }}
                  </span>
                  <span class="shrink-0 text-[11px] leading-none font-normal text-muted-foreground">
                    {{ t('ht.createdAt', { time: formatDateTime(element.createdAt) }) }}
                  </span>
                </template>
              </CardTitle>
              <div
                v-if="!isSelectingContracts"
                :class="['flex shrink-0 opacity-0 transition-opacity group-hover:opacity-100', isListLayout ? 'gap-0.5' : 'gap-1']"
              >
                <TooltipRoot>
                  <TooltipTrigger as-child>
                    <button
                      type="button"
                      :class="[
                        'contract-drag-handle inline-flex cursor-grab items-center justify-center rounded-md text-muted-foreground hover:bg-muted active:cursor-grabbing',
                        isListLayout ? 'h-6 w-6' : 'h-7 w-7'
                      ]"
                      @click.stop
                    >
                      <GripVertical :class="isListLayout ? 'h-3.5 w-3.5' : 'h-4 w-4'" />
                    </button>
                  </TooltipTrigger>
                  <TooltipContent side="top">{{ t('ht.dragSort') }}</TooltipContent>
                </TooltipRoot>
                <TooltipRoot>
                  <TooltipTrigger as-child>
                    <Button
                      variant="ghost"
                      size="icon"
                      :class="isListLayout ? 'h-6 w-6' : 'h-7 w-7'"
                      @click.stop="openEditModal(element)"
                    >
                      <Edit3 :class="isListLayout ? 'h-3.5 w-3.5' : 'h-4 w-4'" />
                    </Button>
                  </TooltipTrigger>
                  <TooltipContent side="top">{{ t('ht.edit') }}</TooltipContent>
                </TooltipRoot>
                <TooltipRoot>
                  <TooltipTrigger as-child>
                    <Button
                      variant="ghost"
                      size="icon"
                      :class="[isListLayout ? 'h-6 w-6' : 'h-7 w-7', 'text-destructive']"
                      @click.stop="requestDeleteContract(element.id)"
                    >
                      <Trash2 :class="isListLayout ? 'h-3.5 w-3.5' : 'h-4 w-4'" />
                    </Button>
                  </TooltipTrigger>
                  <TooltipContent side="top">{{ t('ht.remove') }}</TooltipContent>
                </TooltipRoot>
              </div>
            </CardHeader>
            <div
              v-if="!isListLayout"
              :class="[
                'px-6 text-xs text-muted-foreground',
                'space-y-1 pb-1'
              ]"
            >
              <div class="break-all">{{ t('ht.idLabel', { id: element.id }) }}</div>
              <div>{{ t('ht.contractBudgetLine', { amount: formatBudgetAmount(contractBudgetById[element.id]) }) }}</div>
              <div>{{ t('ht.createdAt', { time: formatDateTime(element.createdAt) }) }}</div>
            </div>
          </Card>
        </template>
      </draggable>
      <div
        v-else-if="!isSearchingContracts && filteredContracts.length === 0"
        class="mx-2 mb-4 rounded-2xl border border-dashed border-primary/30 bg-gradient-to-br from-primary/5 via-background to-muted/30 p-10 text-center shadow-sm"
      >
        <div class="text-lg font-semibold tracking-wide text-foreground">{{ t('ht.emptyTitle') }}</div>
        <div class="mt-2 text-sm text-muted-foreground">{{ t('ht.emptyDesc') }}</div>
      </div>
      <div
        v-else
        :key="`contracts-search-${isListLayout ? 'list' : 'grid'}`"
        :class="[
          'grid grid-cols-1 pb-4 pr-4 pt-3',
          isListLayout ? 'gap-2' : 'gap-4',
          !isListLayout && 'md:grid-cols-2 lg:grid-cols-3'
        ]"
      >
        <Card
          v-for="(element, index) in filteredContracts"
          :key="element.id"
          :class="[
            'group relative cursor-pointer snap-start snap-always transition-colors hover:border-primary ht-contract-card',
            isSelectingContracts
              ? 'ht-contract-card--selecting'
              : cardMotionState === 'enter' && !isDraggingContracts
                ? 'ht-contract-card--enter'
                : 'ht-contract-card--ready',
            isContractSelected(element.id) && 'ht-contract-card--selected',
            isListLayout && 'gap-0 py-0'
          ]"
          :style="getContractCardStyle(index)"
          @click="handleCardClick(element)"
        >
          <label
            v-if="isSelectingContracts"
            class="absolute left-2 top-2 z-10 inline-flex cursor-pointer items-center rounded bg-background/90 p-0.5 shadow-sm"
            @click.stop
          >
            <input
              type="checkbox"
              class="h-4 w-4 cursor-pointer"
              :checked="isContractSelected(element.id)"
              @change.stop="toggleContractSelection(element.id)"
            />
          </label>
          <CardHeader
            :class="[
              'flex flex-row items-center justify-between gap-0 space-y-0',
              isListLayout ? 'px-3 py-2' : 'pb-2'
            ]"
          >
            <CardTitle
              :class="[
                'text-sm font-medium',
                isListLayout && 'mr-1.5 flex min-w-0 flex-1 items-center gap-1.5'
              ]"
            >
              <span :class="isListLayout ? 'min-w-0 truncate' : ''">{{ element.name }}</span>
              <template v-if="isListLayout">
                <span class="min-w-0 shrink text-[11px] leading-none font-normal text-muted-foreground truncate">
                  ID: {{ element.id }}
                </span>
                <span class="shrink-0 text-[11px] leading-none font-normal text-muted-foreground">
                  {{ t('ht.contractBudget', { amount: formatBudgetAmount(contractBudgetById[element.id]) }) }}
                </span>
                <span class="shrink-0 text-[11px] leading-none font-normal text-muted-foreground">
                  {{ t('ht.createdAt', { time: formatDateTime(element.createdAt) }) }}
                </span>
              </template>
            </CardTitle>
            <div
              v-if="!isSelectingContracts"
              :class="['flex shrink-0 opacity-0 transition-opacity group-hover:opacity-100', isListLayout ? 'gap-0.5' : 'gap-1']"
            >
              <TooltipRoot>
                <TooltipTrigger as-child>
                  <span
                    :class="[
                      'inline-flex items-center justify-center rounded-md text-muted-foreground',
                      isListLayout ? 'h-6 w-6' : 'h-7 w-7'
                    ]"
                  >
                    <GripVertical :class="isListLayout ? 'h-3.5 w-3.5' : 'h-4 w-4'" />
                  </span>
                </TooltipTrigger>
                <TooltipContent side="top">{{ t('ht.dragSortSearchOff') }}</TooltipContent>
              </TooltipRoot>
              <TooltipRoot>
                <TooltipTrigger as-child>
                  <Button
                    variant="ghost"
                    size="icon"
                    :class="isListLayout ? 'h-6 w-6' : 'h-7 w-7'"
                    @click.stop="openEditModal(element)"
                  >
                    <Edit3 :class="isListLayout ? 'h-3.5 w-3.5' : 'h-4 w-4'" />
                  </Button>
                </TooltipTrigger>
                <TooltipContent side="top">{{ t('ht.edit') }}</TooltipContent>
              </TooltipRoot>
              <TooltipRoot>
                <TooltipTrigger as-child>
                  <Button
                    variant="ghost"
                    size="icon"
                    :class="[isListLayout ? 'h-6 w-6' : 'h-7 w-7', 'text-destructive']"
                    @click.stop="requestDeleteContract(element.id)"
                  >
                    <Trash2 :class="isListLayout ? 'h-3.5 w-3.5' : 'h-4 w-4'" />
                  </Button>
                </TooltipTrigger>
                <TooltipContent side="top">{{ t('ht.remove') }}</TooltipContent>
              </TooltipRoot>
            </div>
          </CardHeader>
          <div
            v-if="!isListLayout"
            :class="[
              'px-6 text-xs text-muted-foreground',
              'space-y-1 pb-4'
            ]"
          >
            <div class="break-all">{{ t('ht.idLabel', { id: element.id }) }}</div>
            <div>{{ t('ht.contractBudgetLine', { amount: formatBudgetAmount(contractBudgetById[element.id]) }) }}</div>
            <div>{{ t('ht.createdAt', { time: formatDateTime(element.createdAt) }) }}</div>
          </div>
        </Card>
        <div
          v-if="filteredContracts.length === 0"
          class="col-span-full rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground"
        >
          {{ t('ht.notFound') }}
        </div>
      </div>
      </ScrollArea>
      
    </div>

    <TooltipRoot>
      <TooltipTrigger as-child>
        <button
          type="button"
          :aria-label="t('ht.backToTop')"
          :class="[
            'fixed bottom-8 right-8 z-40 inline-flex h-11 w-11 cursor-pointer items-center justify-center rounded-full border border-black/15 bg-white text-black shadow-[0_10px_24px_rgba(0,0,0,0.16)] transition-all duration-300 hover:scale-105 hover:border-black/30 hover:bg-black hover:text-white',
            showScrollTopFab ? 'translate-y-0 opacity-100' : 'pointer-events-none translate-y-3 opacity-0'
          ]"
          @click="scrollContractsToTop()"
        >
          <ArrowUp class="h-5 w-5" />
        </button>
      </TooltipTrigger>
      <TooltipContent side="left">{{ t('ht.backToTop') }}</TooltipContent>
    </TooltipRoot>

    <div
      v-if="showCreateModal"
      class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
    >
      <div
        class="w-full max-w-md rounded-lg border bg-background shadow-xl"
        :style="{ transform: `translate(${modalOffset.x}px, ${modalOffset.y}px)` }"
      >
        <div
          class="flex items-center justify-between border-b px-5 py-4 cursor-move select-none"
          @mousedown.prevent="startDrag"
        >
          <h4 class="text-base font-semibold">
            {{ editingContractId ? t('ht.editContract') : t('ht.createContract') }}
          </h4>
          <Button variant="ghost" size="icon" class="h-8 w-8" @click="closeCreateModal">
            <X class="h-4 w-4" />
          </Button>
        </div>

        <div class="space-y-2 px-5 py-4">
          <label class="block text-sm font-medium text-foreground">{{ t('ht.contractName') }}</label>
          <input
            v-model="contractNameInput"
            type="text"
            :placeholder="t('ht.contractNamePlaceholder')"
            class="h-10 w-full rounded-md border bg-background px-3 text-sm outline-none transition focus-visible:ring-2 focus-visible:ring-ring"
            @keydown.enter="createContract"
          />
        </div>

        <div class="flex items-center justify-end gap-2  px-5 py-3">
          <Button variant="outline" @click="closeCreateModal">{{ t('common.cancel') }}</Button>
          <Button :disabled="!contractNameInput.trim()" @click="createContract">
            {{ editingContractId ? t('ht.save') : t('ht.ok') }}
          </Button>
        </div>
      </div>
    </div>
  </div>
  <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">{{ t('ht.deleteSingleTitle') }}</AlertDialogTitle>
        <AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
          {{ t('ht.deleteSingleDesc', { name: pendingDeleteContractName }) }}
        </AlertDialogDescription>
        <div class="mt-4 flex items-center justify-end gap-2">
          <AlertDialogCancel as-child>
            <Button variant="outline" @click="pendingDeleteContractId = null">{{ t('common.cancel') }}</Button>
          </AlertDialogCancel>
          <AlertDialogAction as-child>
            <Button variant="destructive" @click="confirmDeleteContract">{{ t('common.confirm') }}</Button>
          </AlertDialogAction>
        </div>
      </AlertDialogContent>
    </AlertDialogPortal>
  </AlertDialogRoot>
  <AlertDialogRoot :open="batchDeleteConfirmOpen" @update:open="batchDeleteConfirmOpen = $event">
    <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">{{ t('ht.deleteBatchTitle') }}</AlertDialogTitle>
        <AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
          {{ t('ht.deleteBatchDesc', { count: batchDeleteCount }) }}
        </AlertDialogDescription>
        <div class="mt-4 flex items-center justify-end gap-2">
          <AlertDialogCancel as-child>
            <Button variant="outline">{{ t('common.cancel') }}</Button>
          </AlertDialogCancel>
          <AlertDialogAction as-child>
            <Button variant="destructive" @click="confirmDeleteSelectedContracts">{{ t('common.confirm') }}</Button>
          </AlertDialogAction>
        </div>
      </AlertDialogContent>
    </AlertDialogPortal>
  </AlertDialogRoot>
  <AlertDialogRoot :open="messageDialogOpen" @update:open="messageDialogOpen = $event">
    <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">{{ messageDialogTitle }}</AlertDialogTitle>
        <AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
          {{ messageDialogDesc }}
        </AlertDialogDescription>
        <div class="mt-4 flex items-center justify-end gap-2">
          <AlertDialogAction as-child>
            <Button @click="messageDialogOpen = false">{{ t('tab.dialog.iKnow') }}</Button>
          </AlertDialogAction>
        </div>
      </AlertDialogContent>
    </AlertDialogPortal>
  </AlertDialogRoot>
  <ToastRoot
    v-model:open="toastOpen"
    :duration="1800"
    class="group pointer-events-auto flex items-center gap-3 rounded-xl border border-border bg-card px-4 py-3 text-foreground shadow-lg"
  >
    <div class="grid gap-1">
      <ToastTitle class="text-sm font-semibold text-foreground">{{ toastTitle }}</ToastTitle>
      <ToastDescription class="text-xs text-muted-foreground">{{ toastText }}</ToastDescription>
    </div>
    <ToastAction
      :alt-text="t('tab.dialog.iKnow')"
      class="ml-auto cursor-pointer inline-flex h-7 items-center rounded-md border border-border bg-muted px-2 text-xs text-foreground hover:bg-muted/80"
      @click="toastOpen = false"
    >
      {{ t('tab.dialog.iKnow') }}
    </ToastAction>
  </ToastRoot>
  <ToastViewport class="fixed bottom-5 right-5 z-[85] flex w-[380px] max-w-[92vw] flex-col gap-2 outline-none" />
  </TooltipProvider>
  </ToastProvider>
</template>

<style scoped src="@/features/ht/ht.css"></style>

