<script setup lang="ts">
import { computed, defineAsyncComponent, markRaw, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
import type { ComponentPublicInstance } from 'vue'
import { useI18n } from 'vue-i18n'
import draggable from 'vuedraggable'
import { useDark, useToggle } from '@vueuse/core'
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 { UI_PREFS_STORAGE_KEY, useUiPrefsStore } from '@/pinia/uiPrefs'
import { Button } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area'
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip'
import { Check, ChevronDown, CircleHelp, Loader2, Moon, Sun, X } from 'lucide-vue-next'
import localforage from 'localforage'
import {
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogOverlay,
  AlertDialogPortal,
  AlertDialogRoot,
  AlertDialogTitle,
  ToastDescription,
  ToastProvider,
  ToastRoot,
  ToastTitle,
  ToastViewport,
  SelectContent,
  SelectIcon,
  SelectItem,
  SelectItemIndicator,
  SelectItemText,
  SelectPortal,
  SelectRoot,
  SelectTrigger,
  SelectViewport
} from 'reka-ui'
import { decodeZwArchive, encodeZwArchive, ZW_FILE_EXTENSION } from '@/lib/zwArchive'
import { formatExportTimestamp } from '@/lib/contractSegment'
import {
  getExportProjectName,
  isDataPackageLike,
  normalizeEntries,
  normalizeForageStoreSnapshots,
  readForage,
  sanitizeFileNamePart,
  writeForage,
  type DataPackage,
  type ForageInstance,
  type ForageStore
} from '@/features/tab/importExport'
import type {
  ContractCardItem,
  DetailRowsStorageLike,
  ExportAdditional,
  ExportAdditionalDetail,
  ExportContract,
  ExportMajorCoe,
  ExportMethod0,
  ExportMethod1,
  ExportMethod1Detail,
  ExportMethod2,
  ExportMethod2Detail,
  ExportMethod3,
  ExportMethod3Detail,
  ExportMethod4,
  ExportMethod4Detail,
  ExportMethod5,
  ExportMethod5Detail,
  ExportReportPayload,
  ExportReserve,
  ExportScaleRow,
  ExportService,
  ExportServiceCoe,
  ExportTaskGroup,
  FactorRowLike,
  HourlyMethodRowLike,
  HtBaseInfoLike,
  HtFeeMainRowLike,
  QuantityMethodRowLike,
  RateMethodRowLike,
  ScaleMethodRowLike,
  ScaleRowLike,
  UserGuideStep,
  WorkContentStateLike,
  WorkloadMethodRowLike,
  XmInfoLike,
  XmInfoStorageLike,
  XmScaleStorageLike,
  ZxFwRowLike,
  ZxFwStorageLike
} from '@/features/tab/types'
import {
  buildProjectUrl,
  getProjectDbName,
  readCurrentProjectId,
  PROJECT_TAB_ID,
  QUICK_TAB_ID,
  consumePendingHomeImportFile,
  consumePendingHomeImportSkipConfirm,
  readWorkspaceMode,
  writeProjectIdToUrl,
  writeWorkspaceMode
} from '@/lib/workspace'
import {
  createProject,
  deleteProject,
  listProjects,
  upsertProject,
  type ProjectMeta
} from '@/lib/projectRegistry'
import { emitProjectDeleted, emitResetAll } from '@/lib/projectEvents'
import { collectActiveProjectSessionLocks } from '@/lib/projectSessionLock'
import { addNumbers } from '@/lib/decimal'
import {
  buildMethod0,
  buildMethod1,
  buildMethod2,
  buildMethod3,
  buildMethod4,
  buildMethod5,
  buildProjectMajorCoes,
  buildProjectServiceCoes,
  buildServiceFee,
  buildServiceFinalFee,
  getExpertIdFromRowId,
  getTaskIdFromRowId,
  groupWorkContentTasks,
  hasServiceId,
  isNonEmptyString,
  mapIndustryCodeToExportIndustry,
  sortServiceIdsByDict,
  sumNumbers,
  toExportScaleRows,
  toFiniteNumber,
  toFiniteNumberOrZero,
  toSafeInteger,
  toMoney
} from '@/lib/reportExportBuilders'
import { exportFile } from '@/sql'
import {
  getAdditionalWorkListEntries,
  getIndustryDisplayName,
  getReserveListEntries,
  getServiceDictItemById,
  industryTypeList
} from '@/sql'
import { initializeProjectFactorStates } from '@/lib/projectWorkspace'
import { createProjectKvAdapter } from '@/lib/projectKvStore'
import { I18N_LOCALE_KEY, type AppLocale } from '@/i18n'
const { t, locale } = useI18n()

const USER_GUIDE_COMPLETED_KEY = 'jgjs-user-guide-completed-v1'
const THEME_PREFERENCE_KEY = 'jgjs-theme-dark-v1'
const PROJECT_INFO_DB_KEY = 'xm-base-info-v1'
const LEGACY_PROJECT_DB_KEY = 'xm-info-v3'
const CONSULT_CATEGORY_FACTOR_DB_KEY = 'xm-consult-category-factor-v1'
const MAJOR_FACTOR_DB_KEY = 'xm-major-factor-v1'
const DEFAULT_PROJECT_NAME = t('xmInfo.defaultProjectName')
const MAX_PROJECT_COUNT = 10
const PINIA_PERSIST_DB_NAME = getProjectDbName(readCurrentProjectId())
const PINIA_PERSIST_BASE_STORE_NAME = 'pinia'
const PINIA_PERSIST_STORE_IDS = ['tabs', 'zxFwPricing', 'zxFwPricingKeys', 'zxFwPricingHtFee', 'kv'] as const
const RESET_MIN_LOADING_MS = 1000
const userGuideSteps = computed<UserGuideStep[]>(() => {
  const stepKeys = ['step1', 'step2', 'step3', 'step4', 'step5', 'step6', 'step7', 'step8']
  return stepKeys.map(step => ({
    title: t(`tab.guide.steps.${step}.title`),
    description: t(`tab.guide.steps.${step}.description`),
    points: [
      t(`tab.guide.steps.${step}.point1`),
      t(`tab.guide.steps.${step}.point2`),
      t(`tab.guide.steps.${step}.point3`)
    ]
  }))
})

const componentMap: Record<string, any> = {
  ProjectCalcView: markRaw(defineAsyncComponent(() => import('@/features/xm/components/xmCard.vue'))),
  QuickCalcView: markRaw(defineAsyncComponent(() => import('@/features/ht/components/htCard.vue'))),
  QuickCalcWorkbenchView: markRaw(defineAsyncComponent(() => import('@/features/workbench/components/QuickCalcWorkbenchView.vue'))),
  ZxFwView: markRaw(defineAsyncComponent(() => import('@/features/workbench/components/ZxFwView.vue'))),
  HtFeeMethodTypeLineView: markRaw(defineAsyncComponent(() => import('@/features/workbench/components/HtFeeMethodTypeLineView.vue'))),
}

const tabStore = useTabStore()
const zxFwPricingStore = useZxFwPricingStore()
const zxFwPricingKeysStore = useZxFwPricingKeysStore()
const zxFwPricingHtFeeStore = useZxFwPricingHtFeeStore()
const kvStore = useKvStore()
const uiPrefsStore = useUiPrefsStore()
const isDark = useDark({
  selector: 'html',
  attribute: 'class',
  valueDark: 'dark',
  valueLight: '',
  storageKey: 'jgjs-theme-dark-v1'
})
const toggleDark = useToggle(isDark)



const tabContextOpen = ref(false)
const tabContextX = ref(0)
const tabContextY = ref(0)
const contextTabId = ref<string>('ProjectCalcView')
const tabContextRef = ref<HTMLElement | null>(null)

const dataMenuOpen = ref(false)
const dataMenuRef = ref<HTMLElement | null>(null)
const projectMenuOpen = ref(false)
const projectMenuRef = ref<HTMLElement | null>(null)
const newProjectDialogOpen = ref(false)
const newProjectIndustry = ref(String(industryTypeList[0]?.id || ''))
const newProjectSubmitting = ref(false)
const projectLimitDialogOpen = ref(false)
const projectDeleteConfirmOpen = ref(false)
const pendingDeleteProject = shallowRef<ProjectMeta | null>(null)
const messageDialogOpen = ref(false)
const messageDialogTitle = ref('')
const messageDialogDesc = ref('')
const projectList = ref<ProjectMeta[]>([])
const openedProjectIds = ref<string[]>([])
const currentProjectId = ref(readCurrentProjectId())
const resetConfirmOpen = ref(false)
const isResetting = ref(false)
const importFileRef = ref<HTMLInputElement | null>(null)
const importConfirmOpen = ref(false)
const pendingImportPayload = shallowRef<DataPackage | null>(null)
const pendingImportFileName = ref('')
const userGuideOpen = ref(false)
const userGuideStepIndex = ref(0)
const tabItemElMap = new Map<string, HTMLElement>()
const tabTitleElMap = new Map<string, HTMLElement>()
const tabPanelElMap = new Map<string, HTMLElement>()
const tabScrollAreaRef = ref<HTMLElement | null>(null)
const showTabScrollLeft = ref(false)
const showTabScrollRight = ref(false)
const isTabStripHover = ref(false)
const isTabDragging = ref(false)
const tabTitleOverflowMap = ref<Record<string, boolean>>({})
let tabStripViewportEl: HTMLElement | null = null
let tabTitleOverflowRafId: number | null = null

const reportExportToastOpen = ref(false)
const reportExportProgress = ref(0)
const reportExportStatus = ref<'running' | 'success' | 'error'>('running')
const reportExportText = ref('')
const reportExportBlobUrl = ref<string | null>(null)
let reportExportToastTimer: ReturnType<typeof setTimeout> | null = null

const clearReportExportToastTimer = () => {
  if (!reportExportToastTimer) return
  clearTimeout(reportExportToastTimer)
  reportExportToastTimer = null
}

const showReportExportProgress = (progress: number, text: string) => {
  clearReportExportToastTimer()
  reportExportStatus.value = 'running'
  reportExportProgress.value = Math.max(0, Math.min(100, progress))
  reportExportText.value = text
  reportExportToastOpen.value = true
}

const finishReportExportProgress = (success: boolean, text: string, blobUrl?: string | null) => {
  clearReportExportToastTimer()
  reportExportStatus.value = success ? 'success' : 'error'
  reportExportProgress.value = 100
  reportExportText.value = text
  reportExportBlobUrl.value = success && blobUrl ? blobUrl : null
  reportExportToastOpen.value = true
  reportExportToastTimer = setTimeout(() => {
    reportExportToastOpen.value = false
  }, success ? 2000 : 1800)
}

const openExportedReport = () => {
  if (!reportExportBlobUrl.value) return
  window.open(reportExportBlobUrl.value, '_blank')
  reportExportToastOpen.value = false
}

const dismissReportToast = () => {
  if (reportExportBlobUrl.value) {
    URL.revokeObjectURL(reportExportBlobUrl.value)
    reportExportBlobUrl.value = null
  }
  reportExportToastOpen.value = false
}

const tabsModel = computed({
  get: () => tabStore.tabs,
  set: (value) => {
    tabStore.tabs = value
  }
})
const activeTab = computed(() => tabStore.tabs.find((t: any) => t.id === tabStore.activeTabId) || null)
const activeTabId = computed(() => (activeTab.value ? String(activeTab.value.id || '') : ''))
const resetUiWorkspaceMode = ref<string>('')
const quickModeThemePreferenceSnapshot = ref<string | null | undefined>(undefined)
const workspaceModeForUi = computed(() =>
  isResetting.value && resetUiWorkspaceMode.value
    ? resetUiWorkspaceMode.value
    : readWorkspaceMode()
)

const contextTabIndex = computed(() => tabStore.tabs.findIndex((t:any) => t.id === contextTabId.value))

const hasClosableTabs = computed(() => {
  const fixedId = workspaceModeForUi.value === 'quick' ? QUICK_TAB_ID : PROJECT_TAB_ID
  
  return tabStore.tabs.some((t: any) => t.componentName !== fixedId)
})
const activeGuideStep = computed(
  () => userGuideSteps.value[userGuideStepIndex.value] || userGuideSteps.value[0]
)
const isFirstGuideStep = computed(() => userGuideStepIndex.value === 0)
const isLastGuideStep = computed(() => userGuideStepIndex.value >= userGuideSteps.value.length - 1)
const guideProgressText = computed(() => `${userGuideStepIndex.value + 1} / ${userGuideSteps.value.length}`)
const canCloseLeft = computed(() => {
  if (contextTabIndex.value <= 0) return false
  return tabStore.tabs.slice(1, contextTabIndex.value).length > 0
})
const canCloseRight = computed(() => {
  if (contextTabIndex.value < 0) return false
  return tabStore.tabs.slice(contextTabIndex.value + 1).length > 0
})
const canCloseOther = computed(() =>
 tabStore.tabs.slice(1, contextTabIndex.value).length  > 1 && contextTabIndex.value !== 0
)
const localeLabel = computed(() => (locale.value === 'en-US' ? 'EN' : '中'))
const projectCountText = computed(() => t('tab.toolbar.projectCount', { count: `${projectList.value.length}/${MAX_PROJECT_COUNT}` }))
const newProjectIndustryLabel = computed(() => {
  const target = String(newProjectIndustry.value || '').trim()
  if (!target) return ''
  return getIndustryDisplayName(target, locale.value) || ''
})

const toggleLocale = () => {
  const nextLocale: AppLocale = locale.value === 'en-US' ? 'zh-CN' : 'en-US'
  uiPrefsStore.setLocale(nextLocale)
}

const resolveHtFeeMethodRowNameByDict = (
  storageKeyRaw: string,
  rowIdRaw: string,
  fallbackRaw: string
) => {
  const storageKey = String(storageKeyRaw || '').trim()
  const rowId = String(rowIdRaw || '').trim()
  const fallback = String(fallbackRaw || '').trim()
  if (!storageKey || !rowId) return fallback
  if (storageKey.includes('-additional-work')) {
    const item = getAdditionalWorkListEntries(locale.value).find(entry => String(entry?.id || '').trim() === rowId)
    return String(item?.name || '').trim() || fallback
  }
  if (storageKey.includes('-reserve')) {
    const item = getReserveListEntries(locale.value).find(entry => String(entry?.id || '').trim() === rowId)
    return String(item?.name || '').trim() || fallback
  }
  return fallback
}

const syncTabLabelsByLocale = () => {
  const nextProjectTitle = t('home.projectCalcTab')
  const nextQuickTitle = t('home.quickCalcTab')
  let changed = false
  tabStore.tabs = tabStore.tabs.map((tab: any) => {
    const currentProps = tab?.props && typeof tab.props === 'object' ? { ...tab.props } : undefined
    if (tab.id === PROJECT_TAB_ID) {
      if (tab.title === nextProjectTitle) return tab
      changed = true
      return { ...tab, title: nextProjectTitle }
    }
    if (tab.id === QUICK_TAB_ID) {
      if (tab.title === nextQuickTitle) return tab
      changed = true
      return { ...tab, title: nextQuickTitle }
    }
    if (tab.componentName === 'QuickCalcView') {
      const contractName = String(currentProps?.contractName || '').trim()
      const nextTitle = t('ht.contractTabTitle', { name: contractName || '-' })
      if (tab.title === nextTitle) return tab
      changed = true
      return { ...tab, title: nextTitle }
    }
    if (tab.componentName === 'ZxFwView') {
      const serviceId = String(currentProps?.serviceId || '').trim()
      const dict = serviceId ? (getServiceDictItemById(serviceId) as { code?: string; name?: string } | undefined) : undefined
      const serviceName = dict
        ? `${String(dict.code || '').trim()}${String(dict.name || '').trim()}`
        : String(currentProps?.fwName || '').trim()
      const nextTitle = t('htZxFw.editTabTitle', { name: serviceName || '-' })
      const nextProps = {
        ...(currentProps || {}),
        fwName: serviceName || String(currentProps?.fwName || '')
      }
      if (tab.title === nextTitle && String(currentProps?.fwName || '') === String(nextProps.fwName || '')) return tab
      changed = true
      return { ...tab, title: nextTitle, props: nextProps }
    }
    if (tab.componentName === 'HtFeeMethodTypeLineView') {
      const storageKey = String(currentProps?.storageKey || '').trim()
      const rowId = String(currentProps?.rowId || '').trim()
      const nextSourceTitle = storageKey.includes('-reserve')
        ? t('htSummary.reservePrefix')
        : storageKey.includes('-additional-work')
          ? t('htSummary.additionalPrefix')
          : String(currentProps?.sourceTitle || '')
      const rows = storageKey
        ? (zxFwPricingStore.getHtFeeMainState<any>(storageKey)?.detailRows || [])
        : []
      const fromStoreName = Array.isArray(rows)
        ? String(rows.find((row: any) => String(row?.id || '') === rowId)?.name || '').trim()
        : ''
      const rowName = resolveHtFeeMethodRowNameByDict(
        storageKey,
        rowId,
        fromStoreName || String(currentProps?.rowName || '').trim()
      )
      const nextTitle = t('htFeeGrid.editTabTitle', { name: rowName || t('htFeeGrid.unnamed') })
      const nextProps = {
        ...(currentProps || {}),
        sourceTitle: nextSourceTitle,
        rowName
      }
      if (
        tab.title === nextTitle
        && String(currentProps?.rowName || '') === String(nextProps.rowName || '')
        && String(currentProps?.sourceTitle || '') === String(nextProps.sourceTitle || '')
      ) return tab
      changed = true
      return { ...tab, title: nextTitle, props: nextProps }
    }
    return tab
  })
  if (!changed) return
  scheduleUpdateTabTitleOverflow()
}

const closeMenus = () => {
  tabContextOpen.value = false
  dataMenuOpen.value = false
  projectMenuOpen.value = false
}

const refreshProjectList = async () => {
  const baseProjects = listProjects()
  openedProjectIds.value = Array.from(collectActiveProjectSessionLocks(baseProjects.map(item => item.id)))
  const enriched = await Promise.all(
    baseProjects.map(async (project) => {
      try {
        const kvStoreInstance = localforage.createInstance({
          name: getProjectDbName(project.id),
          storeName: 'pinia-kv'
        })
        const kvState = await kvStoreInstance.getItem<any>('pinia-kv')
        const entries = kvState?.entries && typeof kvState.entries === 'object' ? kvState.entries : null
        const projectInfo = entries?.[PROJECT_INFO_DB_KEY]
        const projectName =
          projectInfo && typeof projectInfo === 'object' && typeof projectInfo.projectName === 'string'
            ? projectInfo.projectName.trim()
            : ''
        return {
          ...project,
          name: projectName || project.name
        }
      } catch {
        return project
      }
    })
  )
  projectList.value = enriched
}

const isProjectOpen = (projectId: string) => openedProjectIds.value.includes(projectId)

const openProjectInNewTab = (projectId: string) => {
  if (isProjectOpen(projectId)) return
  const href = buildProjectUrl(projectId)
  window.open(href, '_blank', 'noopener')
  projectMenuOpen.value = false
}

const getTodayDateString = () => {
  const now = new Date()
  const year = String(now.getFullYear())
  const month = String(now.getMonth() + 1).padStart(2, '0')
  const day = String(now.getDate()).padStart(2, '0')
  return `${year}-${month}-${day}`
}

const openCreateProjectDialog = () => {
  if (listProjects().length >= MAX_PROJECT_COUNT) {
    projectMenuOpen.value = false
    projectLimitDialogOpen.value = true
    return
  }
  projectMenuOpen.value = false
  newProjectIndustry.value = String(industryTypeList[0]?.id || '')
  newProjectDialogOpen.value = true
}

const closeCreateProjectDialog = () => {
  if (newProjectSubmitting.value) return
  newProjectDialogOpen.value = false
}

const createProjectAndOpen = async () => {
  if (newProjectSubmitting.value) return
  if (listProjects().length >= MAX_PROJECT_COUNT) {
    projectMenuOpen.value = false
    newProjectDialogOpen.value = false
    projectLimitDialogOpen.value = true
    return
  }
  const industry = String(newProjectIndustry.value || '').trim()
  if (!industry) return
  newProjectSubmitting.value = true
  try {
    const project = createProject(DEFAULT_PROJECT_NAME)
    const kvAdapter = createProjectKvAdapter(project.id)
    await kvAdapter.setItem(PROJECT_INFO_DB_KEY, {
      projectIndustry: industry,
      projectName: DEFAULT_PROJECT_NAME,
      preparedBy: '',
      reviewedBy: '',
      preparedCompany: '',
      preparedDate: getTodayDateString()
    })
    await initializeProjectFactorStates(
      kvAdapter,
      industry,
      CONSULT_CATEGORY_FACTOR_DB_KEY,
      MAJOR_FACTOR_DB_KEY
    )
    void refreshProjectList()
    const href = buildProjectUrl(project.id)
    window.open(href, '_blank', 'noopener')
    newProjectDialogOpen.value = false
  } finally {
    newProjectSubmitting.value = false
  }
}

const handleCreateProjectDialogOpenChange = (open: boolean) => {
  if (newProjectSubmitting.value) return
  newProjectDialogOpen.value = open
}

const handleCreateProjectConfirm = () => {
  void createProjectAndOpen()
  projectMenuOpen.value = false
}

const returnToHome = () => {
  projectMenuOpen.value = false
  const href = buildProjectUrl(currentProjectId.value || readCurrentProjectId(), {
    forceHome: true
  })
  window.location.href = href
}

const formatProjectEditedTime = (value: string) => {
  const date = new Date(value)
  if (Number.isNaN(date.getTime())) return '-'
  const pad = (num: number) => String(num).padStart(2, '0')
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
}

const removeProjectItem = async (project: ProjectMeta) => {
  const isCurrentProject = project.id === currentProjectId.value
  const deleteIndexedDBByName = (dbName: string) =>
    new Promise<void>((resolve) => {
      try {
        const request = window.indexedDB?.deleteDatabase(dbName)
        if (!request) {
          resolve()
          return
        }
        request.onsuccess = () => resolve()
        request.onerror = () => resolve()
        request.onblocked = () => resolve()
      } catch (_error) {
        resolve()
      }
    })
  const clearProjectPersistence = async () => {
    await projectDefaultForage.clear()
    await Promise.all(
      getPiniaPersistStores().map(async ({ store }) => {
        await store.clear()
      })
    )
    const clearTasks: Promise<void>[] = []
    if (tabStore.$clearPersisted) clearTasks.push(tabStore.$clearPersisted())
    if (zxFwPricingStore.$clearPersisted) clearTasks.push(zxFwPricingStore.$clearPersisted())
    if (zxFwPricingKeysStore.$clearPersisted) clearTasks.push(zxFwPricingKeysStore.$clearPersisted())
    if (zxFwPricingHtFeeStore.$clearPersisted) clearTasks.push(zxFwPricingHtFeeStore.$clearPersisted())
    if (kvStore.$clearPersisted) clearTasks.push(kvStore.$clearPersisted())
    await Promise.all(clearTasks)
  }

  if (isCurrentProject) {
    window.dispatchEvent(new CustomEvent('jgjs-release-project-lock'))
    await clearProjectPersistence()
    await deleteIndexedDBByName(getProjectDbName(project.id))
    const removed = deleteProject(project.id)
    if (!removed) return
    emitProjectDeleted(project.id)
    writeWorkspaceMode('project')
    tabStore.resetTabs()
    tabStore.hasCompletedSetup = false
    window.location.href = buildProjectUrl('default', { forceHome: true })
    return
  }

  const removed = deleteProject(project.id)
  if (!removed) return
  emitProjectDeleted(project.id)
  try {
    await deleteIndexedDBByName(getProjectDbName(project.id))
  } catch (error) {
    console.error('delete project database failed:', error)
  }
  await refreshProjectList()
}

const requestRemoveProjectItem = (project: ProjectMeta) => {
  pendingDeleteProject.value = project
  projectDeleteConfirmOpen.value = true
}

const handleProjectDeleteDialogOpenChange = (open: boolean) => {
  projectDeleteConfirmOpen.value = open
}

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

const handleToggleTheme = () => {
  toggleDark()
}

const cancelRemoveProjectItem = () => {
  projectDeleteConfirmOpen.value = false
  pendingDeleteProject.value = null
}

const confirmRemoveProjectItem = async () => {
  const project = pendingDeleteProject.value
  if (!project) return
  projectDeleteConfirmOpen.value = false
  pendingDeleteProject.value = null
  await removeProjectItem(project)
}

const markGuideCompleted = () => {
  try {
    localStorage.setItem(USER_GUIDE_COMPLETED_KEY, '1')
  } catch (error) {
    console.error('mark guide completed failed:', error)
  }
}

const hasGuideCompleted = () => {
  try {
    return localStorage.getItem(USER_GUIDE_COMPLETED_KEY) === '1'
  } catch (error) {
    console.error('read guide completion failed:', error)
    return false
  }
}

const hasNonDefaultTabState = () => {
  try {
    const raw = localStorage.getItem('tabs')
    if (!raw) return false
    const parsed = JSON.parse(raw) as { tabs?: Array<{ id?: string }>; activeTabId?: string }
    const tabs = Array.isArray(parsed?.tabs) ? parsed.tabs : []
    const hasCustomTabs = tabs.some(item => item?.id && item.id !== 'ProjectCalcView')
    const activeTabId = typeof parsed?.activeTabId === 'string' ? parsed.activeTabId : ''
    return hasCustomTabs || (activeTabId !== '' && activeTabId !== 'ProjectCalcView')
  } catch (error) {
    console.error('parse tabs cache failed:', error)
    return false
  }
}

const shouldAutoOpenGuide = async () => {
  if (hasGuideCompleted()) return false
  if (hasNonDefaultTabState()) return false
  try {
    const keys = await kvStore.keys()
    return keys.length === 0
  } catch (error) {
    console.error('read kv keys failed:', error)
    return false
  }
}

const openUserGuide = (startAt = 0) => {
  closeMenus()
  userGuideStepIndex.value = Math.min(Math.max(startAt, 0), userGuideSteps.value.length - 1)
  userGuideOpen.value = true
}

const closeUserGuide = (completed = false) => {
  userGuideOpen.value = false
  if (completed) markGuideCompleted()
}

const prevUserGuideStep = () => {
  if (isFirstGuideStep.value) return
  userGuideStepIndex.value -= 1
}

const nextUserGuideStep = () => {
  if (isLastGuideStep.value) {
    closeUserGuide(true)
    return
  }
  userGuideStepIndex.value += 1
}

const jumpToGuideStep = (stepIndex: number) => {
  userGuideStepIndex.value = Math.min(Math.max(stepIndex, 0), userGuideSteps.value.length - 1)
}

const openTabContextMenu = (event: MouseEvent, tabId: string) => {
  contextTabId.value = tabId
  tabContextX.value = event.clientX
  tabContextY.value = event.clientY
  tabContextOpen.value = true
  void nextTick(() => {
    if (!tabContextRef.value) return
    const gap = 8
    const rect = tabContextRef.value.getBoundingClientRect()
    if (tabContextX.value + rect.width + gap > window.innerWidth) {
      tabContextX.value = Math.max(gap, window.innerWidth - rect.width - gap)
    }
    if (tabContextY.value + rect.height + gap > window.innerHeight) {
      tabContextY.value = Math.max(gap, window.innerHeight - rect.height - gap)
    }
    if (tabContextX.value < gap) tabContextX.value = gap
    if (tabContextY.value < gap) tabContextY.value = gap
  })
}

const handleGlobalMouseDown = (event: MouseEvent) => {
  const target = event.target as Node
  if (tabContextOpen.value && tabContextRef.value && !tabContextRef.value.contains(target)) {
    tabContextOpen.value = false
  }
  if (dataMenuOpen.value && dataMenuRef.value && !dataMenuRef.value.contains(target)) {
    dataMenuOpen.value = false
  }
  if (projectMenuOpen.value && projectMenuRef.value && !projectMenuRef.value.contains(target)) {
    projectMenuOpen.value = false
  }
}

const handleGlobalKeyDown = (event: KeyboardEvent) => {
  if (!userGuideOpen.value) return
  if (event.key === 'Escape') {
    event.preventDefault()
    closeUserGuide(false)
    return
  }
  if (event.key === 'ArrowLeft') {
    event.preventDefault()
    prevUserGuideStep()
    return
  }
  if (event.key === 'ArrowRight') {
    event.preventDefault()
    nextUserGuideStep()
  }
}

const runTabMenuAction = (action: 'all' | 'left' | 'right' | 'other') => {
  if (action === 'all') tabStore.closeAllTabs()
  if (action === 'left') tabStore.closeLeftTabs(contextTabId.value)
  if (action === 'right') tabStore.closeRightTabs(contextTabId.value)
  if (action === 'other') tabStore.closeOtherTabs(contextTabId.value)
  tabContextOpen.value = false
}

const canMoveTab = (event: any) => {
  const draggedId = event?.draggedContext?.element?.id
  const targetIndex = event?.relatedContext?.index
  if (draggedId === tabStore.tabs[0]?.id) return false
  if (typeof targetIndex === 'number' && targetIndex === 0) return false
  return true
}

const handleTabDragStart = () => {
  isTabDragging.value = true
}

const handleTabDragEnd = () => {
  isTabDragging.value = false
}

const setTabItemRef = (id: string, el: Element | ComponentPublicInstance | null) => {
  if (el instanceof HTMLElement) {
    tabItemElMap.set(id, el)
    return
  }
  tabItemElMap.delete(id)
  tabTitleElMap.delete(id)
  delete tabTitleOverflowMap.value[id]
  scheduleUpdateTabTitleOverflow()
}

const setTabTitleRef = (id: string, el: Element | ComponentPublicInstance | null) => {
  if (el instanceof HTMLElement) {
    tabTitleElMap.set(id, el)
    scheduleUpdateTabTitleOverflow()
    return
  }
  tabTitleElMap.delete(id)
  delete tabTitleOverflowMap.value[id]
  scheduleUpdateTabTitleOverflow()
}

const updateTabTitleOverflow = () => {
  const nextMap: Record<string, boolean> = {}
  for (const [id, el] of tabTitleElMap.entries()) {
    nextMap[id] = el.scrollWidth > el.clientWidth + 1
  }
  tabTitleOverflowMap.value = nextMap
}

const scheduleUpdateTabTitleOverflow = () => {
  if (tabTitleOverflowRafId != null) return
  tabTitleOverflowRafId = requestAnimationFrame(() => {
    tabTitleOverflowRafId = null
    updateTabTitleOverflow()
  })
}

const setTabPanelRef = (id: string, el: Element | ComponentPublicInstance | null) => {
  if (el instanceof HTMLElement) {
    tabPanelElMap.set(id, el)
    return
  }
  tabPanelElMap.delete(id)
}

const setTabScrollAreaRef = (el: Element | ComponentPublicInstance | null) => {
  if (el instanceof HTMLElement) {
    tabScrollAreaRef.value = el
    return
  }
  const rootEl =
    el && typeof el === 'object' && '$el' in el ? (el as ComponentPublicInstance).$el : null
  tabScrollAreaRef.value = rootEl instanceof HTMLElement ? rootEl : null
}

const getTabStripViewport = () =>
  tabScrollAreaRef.value?.querySelector<HTMLElement>('[data-slot="scroll-area-viewport"]') || null

const updateTabScrollButtons = () => {
  const viewport = getTabStripViewport()
  if (!viewport) {
    showTabScrollLeft.value = false
    showTabScrollRight.value = false
    return
  }
  const maxLeft = Math.max(0, viewport.scrollWidth - viewport.clientWidth)
  showTabScrollLeft.value = viewport.scrollLeft > 1
  showTabScrollRight.value = viewport.scrollLeft < maxLeft - 1
}

const handleTabStripScroll = () => {
  updateTabScrollButtons()
}

const bindTabStripScroll = () => {
  const viewport = getTabStripViewport()
  if (tabStripViewportEl === viewport) {
    updateTabScrollButtons()
    return
  }
  if (tabStripViewportEl) {
    tabStripViewportEl.removeEventListener('scroll', handleTabStripScroll)
  }
  tabStripViewportEl = viewport
  if (tabStripViewportEl) {
    tabStripViewportEl.addEventListener('scroll', handleTabStripScroll, { passive: true })
  }
  updateTabScrollButtons()
}

const scrollTabStripBy = (delta: number) => {
  const viewport = getTabStripViewport()
  if (!viewport) return
  viewport.scrollBy({ left: delta, behavior: 'smooth' })
  requestAnimationFrame(updateTabScrollButtons)
}

const ensureActiveTabVisible = () => {
  const activeId = tabStore.activeTabId
  const el = tabItemElMap.get(activeId)
  if (!el) return
  el.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' })
}

const getActivePanelScrollViewport = (tabId?: string | null) => {
  if (!tabId) return null
  const panelEl = tabPanelElMap.get(tabId)
  if (!panelEl) return null
  return panelEl.querySelector<HTMLElement>('[data-slot="scroll-area-viewport"]')
}

const getTabScrollSessionKey = (tabId: string) => `tab-scroll-top:${tabId}`

const saveTabInnerScrollTop = (tabId?: string | null) => {
  if (!tabId) return
  const viewport = getActivePanelScrollViewport(tabId)
  if (!viewport) return
  const top = viewport.scrollTop || 0
  try {
    sessionStorage.setItem(getTabScrollSessionKey(tabId), String(top))
  } catch (error) {
    console.error('save tab scroll failed:', error)
  }
}

const restoreTabInnerScrollTop = (tabId?: string | null) => {
  if (!tabId) return
  const viewport = getActivePanelScrollViewport(tabId)
  if (!viewport) return
  let top = 0
  try {
    top = Number(sessionStorage.getItem(getTabScrollSessionKey(tabId)) || '0') || 0
  } catch (error) {
    console.error('restore tab scroll failed:', error)
  }
  viewport.scrollTop = top
}

const scheduleRestoreTabInnerScrollTop = (tabId?: string | null) => {
  if (!tabId) return
  requestAnimationFrame(() => {
    restoreTabInnerScrollTop(tabId)
    requestAnimationFrame(() => {
      restoreTabInnerScrollTop(tabId)
    })
  })
}

const createForageStore = (storeName: string): ForageInstance =>
  localforage.createInstance({
    name: PINIA_PERSIST_DB_NAME,
    storeName
  })

const projectDefaultForage = localforage.createInstance({
  name: PINIA_PERSIST_DB_NAME
})

const getPiniaPersistStoreName = (storeId: string) => `${PINIA_PERSIST_BASE_STORE_NAME}-${storeId}`

const getPiniaPersistStores = () =>
  PINIA_PERSIST_STORE_IDS.map(storeId => {
    const storeName = getPiniaPersistStoreName(storeId)
    return {
      storeName,
      store: createForageStore(storeName)
    }
  })

const loadFactorRowsState = async (storageKey: string) => {
  const [piniaData, kvData] = await Promise.all([
    zxFwPricingStore.loadKeyState<DetailRowsStorageLike<FactorRowLike>>(storageKey),
    kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(storageKey)
  ])
  return {
    piniaData,
    kvData,
    resolved: piniaData || kvData || null
  }
}

const createRichTextCode = (...parts: string[]): unknown => ({
  richText: parts
    .map(item => String(item || '').trim())
    .filter(Boolean)
    .map(text => ({ text }))
})

const resolveMethodEnabled = (value: unknown, fallback: boolean) => {
  if (typeof value === 'boolean') return value
  if (typeof value === 'number') return value === 1
  if (typeof value === 'string') {
    const normalized = value.trim().toLowerCase()
    if (normalized === 'true' || normalized === '1') return true
    if (normalized === 'false' || normalized === '0') return false
  }
  return fallback
}

const getServiceMethodAvailability = (serviceIdText: string) => {
  const dict = getServiceDictItemById(serviceIdText) as {
    scale?: boolean | null
    onlyCostScale?: boolean | null
    amount?: boolean | null
    workDay?: boolean | null
  } | null | undefined
  const scale = resolveMethodEnabled(dict?.scale, false)
  const onlyCostScale = resolveMethodEnabled(dict?.onlyCostScale, false)
  const amount = resolveMethodEnabled(dict?.amount, false)
  const workDay = resolveMethodEnabled(dict?.workDay, false)
  return {
    investmentScale: scale,
    landScale: scale && !onlyCostScale,
    workload: amount,
    hourly: workDay
  }
}

const normalizeHtFeeMainRows = (rows: HtFeeMainRowLike[] | undefined) => {
  if (!Array.isArray(rows)) return [] as Array<{ id: string; name: string; subtotal?: unknown }>
  const normalized = rows.map(row => {
    const id = String(row?.id || '').trim()
    if (!id) return null
    const rowAny = row as { subtotal?: unknown }
    return {
      id,
      name: typeof row?.name === 'string' ? row.name : '',
      subtotal: rowAny?.subtotal
    }
  })
  return normalized.filter(Boolean) as Array<{ id: string; name: string; subtotal?: unknown }>
}

const getHtMainRowSubtotal = (row: { subtotal?: unknown } | null | undefined): number | null => {
  const subtotal = toFiniteNumber(row?.subtotal)
  if (subtotal == null) return null
  return toMoney(subtotal)
}

const loadHtFeeMethodsByRow = async (mainStorageKey: string, rowId: string) => {
  const [rateState, hourlyState, quantityState] = await Promise.all([
    zxFwPricingStore.loadHtFeeMethodState<RateMethodRowLike>(mainStorageKey, rowId, 'rate-fee'),
    zxFwPricingStore.loadHtFeeMethodState<DetailRowsStorageLike<HourlyMethodRowLike>>(mainStorageKey, rowId, 'hourly-fee'),
    zxFwPricingStore.loadHtFeeMethodState<DetailRowsStorageLike<QuantityMethodRowLike>>(mainStorageKey, rowId, 'quantity-unit-price-fee')
  ])
  const m0 = buildMethod0(rateState)


  const m4 = buildMethod4(Array.isArray(hourlyState?.detailRows) ? hourlyState?.detailRows : undefined)
  const m5 = buildMethod5(Array.isArray(quantityState?.detailRows) ? quantityState?.detailRows : undefined)
  if (!m0 && !m4 && !m5) return null
  return {
    fee: sumNumbers([m0?.fee, m4?.fee, m5?.fee]),
    m0,
    m4,
    m5
  }
}

const buildServiceTasks = async (
  contractId: string,
  serviceId: string
): Promise<ExportTaskGroup[]> => {
  const taskState = await zxFwPricingStore.loadKeyState<WorkContentStateLike>(`work-content-${contractId}-${serviceId}`)
  return groupWorkContentTasks(taskState?.detailRows)
}

const buildAdditionalRowTasks = async (contractId: string, rowId: string): Promise<ExportTaskGroup[]> => {
  const taskStorageKey = `work-content-htExtraFee-${contractId}-additional-work-${rowId}`
  const taskState = await zxFwPricingStore.loadKeyState<WorkContentStateLike>(taskStorageKey)
  return groupWorkContentTasks(taskState?.detailRows, { forceUngroup: true })
}

const buildAdditionalExport = async (contractId: string): Promise<ExportAdditional | null> => {
  const storageKey = `htExtraFee-${contractId}-additional-work`
  const mainState = await zxFwPricingStore.loadHtFeeMainState<HtFeeMainRowLike>(storageKey)
  const rows = normalizeHtFeeMainRows(mainState?.detailRows)
  if (rows.length === 0) return null

  const det = (
    await Promise.all(
      rows.map(async row => {
        const methodPayload = await loadHtFeeMethodsByRow(storageKey, row.id)
        const rowSubtotal = getHtMainRowSubtotal(row)
        if (!methodPayload && rowSubtotal == null) return null
        const tasks = await buildAdditionalRowTasks(contractId, row.id)
        const item: ExportAdditionalDetail = {
          id: row.id,
          code: { richText: [{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'C' }, { font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'F' }] },

          name: row.name,
          fee: toMoney(rowSubtotal ?? methodPayload?.fee ?? 0),
          tasks
        }
        if (methodPayload?.m0) item.m0 = methodPayload.m0
        if (methodPayload?.m4) item.m4 = methodPayload.m4
        if (methodPayload?.m5) item.m5 = methodPayload.m5
        return item
      })
    )
  ).filter((item): item is ExportAdditionalDetail => Boolean(item))

  if (det.length === 0) return null
  return {
    code: { richText: [{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'C' }, { font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'C' }] },

    name: t('htSummary.additionalPrefix'),
    fee: toMoney(sumNumbers(det.map(item => item.fee))),
    det
  }
}

const buildReserveExport = async (contractId: string): Promise<ExportReserve | null> => {
  const storageKey = `htExtraFee-${contractId}-reserve`
  const mainState = await zxFwPricingStore.loadHtFeeMainState<HtFeeMainRowLike>(storageKey)
  const rows = normalizeHtFeeMainRows(mainState?.detailRows)
  if (rows.length === 0) return null

  const reserveCodeTemplate = getReserveListEntries(locale.value)?.[0]?.code
    ?? {
      richText: [
        { font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'Y' },
        { font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'B' }
      ]
    }

  for (const row of rows) {
    const methodPayload = await loadHtFeeMethodsByRow(storageKey, row.id)
    const rowSubtotal = getHtMainRowSubtotal(row)
    if (!methodPayload && rowSubtotal == null) continue
    const reserve: ExportReserve = {
      code: reserveCodeTemplate,
      name: row.name || t('htSummary.reservePrefix'),
      fee: toMoney(rowSubtotal ?? methodPayload?.fee ?? 0),
      tasks: []
    }
    if (methodPayload?.m0) reserve.m0 = methodPayload.m0
    if (methodPayload?.m4) reserve.m4 = methodPayload.m4
    if (methodPayload?.m5) reserve.m5 = methodPayload.m5
    return reserve
  }
  return null
}

const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
  const [projectInfoRaw, projectScaleRaw, consultCategoryFactorState, majorFactorState, contractCardsRaw] = await Promise.all([
    kvStore.getItem<XmInfoLike>(PROJECT_INFO_DB_KEY),
    kvStore.getItem<XmInfoStorageLike>(LEGACY_PROJECT_DB_KEY),
    loadFactorRowsState(CONSULT_CATEGORY_FACTOR_DB_KEY),
    loadFactorRowsState(MAJOR_FACTOR_DB_KEY),
    kvStore.getItem<ContractCardItem[]>('ht-card-v1')
  ])

  const projectInfo = projectInfoRaw || {}
  const projectScaleSource = projectScaleRaw || {}

  const projectScale = projectScaleSource.roughCalcEnabled ? [] : toExportScaleRows(projectScaleSource.detailRows)
  const projectScaleCost = toFiniteNumber(projectScaleSource.totalAmount) ?? sumNumbers(projectScale.map(item => item.cost))


  const projectServiceCoes = buildProjectServiceCoes(consultCategoryFactorState.resolved?.detailRows)
  const projectMajorCoes = buildProjectMajorCoes(majorFactorState.resolved?.detailRows)
  const projectName = isNonEmptyString(projectInfo.projectName)
    ? projectInfo.projectName.trim()
    : t('tab.messages.defaultProjectName')
  const writer = isNonEmptyString(projectInfo.preparedBy) ? projectInfo.preparedBy.trim() : ''
  const reviewer = isNonEmptyString(projectInfo.reviewedBy) ? projectInfo.reviewedBy.trim() : ''
  const company = isNonEmptyString(projectInfo.preparedCompany) ? projectInfo.preparedCompany.trim() : ''
  const date = isNonEmptyString(projectInfo.preparedDate) ? projectInfo.preparedDate.trim() : ''
  const industry = mapIndustryCodeToExportIndustry(projectInfo.projectIndustry)
  const overview = isNonEmptyString(projectInfo.overview) ? projectInfo.overview.trim() : ''
  const desc = isNonEmptyString(projectInfo.desc) ? projectInfo.desc.trim() : ''

  const contractCards = (Array.isArray(contractCardsRaw) ? contractCardsRaw : [])
    .filter(item => item && typeof item.id === 'string')
    .sort((a, b) => (typeof a.order === 'number' ? a.order : Number.MAX_SAFE_INTEGER) - (typeof b.order === 'number' ? b.order : Number.MAX_SAFE_INTEGER))

  const contracts: ExportContract[] = []

  for (let index = 0; index < contractCards.length; index++) {
    const contract = contractCards[index]
    const contractId = contract.id
    await zxFwPricingStore.loadContract(contractId)
    const [htInfoRaw, zxFwRaw, htConsultCategoryFactorState, htMajorFactorState, htBaseInfoRaw] = await Promise.all([
      kvStore.getItem<DetailRowsStorageLike<ScaleRowLike>>(`ht-info-v3-${contractId}`),
      kvStore.getItem<ZxFwStorageLike>(`zxFW-${contractId}`),
      loadFactorRowsState(`ht-consult-category-factor-v1-${contractId}`),
      loadFactorRowsState(`ht-major-factor-v1-${contractId}`),
      zxFwPricingStore.loadKeyState<HtBaseInfoLike>(`ht-base-info-${contractId}`)
    ])

    const contractState = zxFwPricingStore.getContractState(contractId)
    const zxRowsFromStore: ZxFwRowLike[] = Array.isArray(contractState?.detailRows)
      ? contractState.detailRows.map(row => ({
        id: String(row.id || ''),
        process: row.process,
        subtotal: row.subtotal,
        finalFee: row.finalFee,
        investScale: row.investScale,
        landScale: row.landScale,
        workload: row.workload,
        hourly: row.hourly
      }))
      : []
    const zxRowsFromKv = Array.isArray(zxFwRaw?.detailRows) ? zxFwRaw.detailRows : []
    const zxRows = zxRowsFromStore.length > 0 ? zxRowsFromStore : zxRowsFromKv
    const selectedIdsFromStore = Array.isArray(contractState?.selectedIds)
      ? contractState.selectedIds.map(id => String(id || '').trim()).filter(Boolean)
      : []
    const selectedIdsFromKv = Array.isArray(zxFwRaw?.selectedIds)
      ? zxFwRaw.selectedIds.map(id => String(id || '').trim()).filter(Boolean)
      : []
    const selectedIds = Array.from(new Set([...selectedIdsFromStore, ...selectedIdsFromKv])).filter(hasServiceId)

    let fixedRow: ZxFwRowLike | undefined
    const serviceRowMap = new Map<string, ZxFwRowLike>()
    for (const row of zxRows) {
      const rowId = String(row?.id || '').trim()
      if (!rowId) continue
      if (rowId === 'fixed-budget-c') {
        fixedRow = row
        continue
      }
      if (!hasServiceId(rowId)) continue
      serviceRowMap.set(rowId, row)
    }
    const fallbackServiceIds = Array.from(serviceRowMap.keys())
    const serviceIdTexts = sortServiceIdsByDict(
      (selectedIds.length > 0 ? selectedIds : fallbackServiceIds).filter(hasServiceId)
    )
    const services = (
      await Promise.all(
        serviceIdTexts.map(async serviceIdText => {
          const serviceId = toSafeInteger(serviceIdText)
          if (serviceId == null) return null
          const sourceRow = serviceRowMap.get(serviceIdText)
          const methodAvailability = getServiceMethodAvailability(serviceIdText)

          const [method1State, method2State, method3State, method4State] = await Promise.all([
            methodAvailability.investmentScale
              ? zxFwPricingStore.loadServicePricingMethodState<ScaleMethodRowLike>(contractId, serviceIdText, 'investScale')
              : Promise.resolve(null),
            methodAvailability.landScale
              ? zxFwPricingStore.loadServicePricingMethodState<ScaleMethodRowLike>(contractId, serviceIdText, 'landScale')
              : Promise.resolve(null),
            methodAvailability.workload
              ? zxFwPricingStore.loadServicePricingMethodState<WorkloadMethodRowLike>(contractId, serviceIdText, 'workload')
              : Promise.resolve(null),
            methodAvailability.hourly
              ? zxFwPricingStore.loadServicePricingMethodState<HourlyMethodRowLike>(contractId, serviceIdText, 'hourly')
              : Promise.resolve(null)
          ])

          const method1Raw = method1State ? { detailRows: method1State.detailRows } : null
          const method2Raw = method2State ? { detailRows: method2State.detailRows } : null
          const method3Raw = method3State ? { detailRows: method3State.detailRows } : null
          const method4Raw = method4State ? { detailRows: method4State.detailRows } : null
          const method2RawRows = Array.isArray(method2Raw?.detailRows) ? method2Raw.detailRows : []
          const method1 = methodAvailability.investmentScale ? buildMethod1(method1Raw?.detailRows) : null
          const method2 = methodAvailability.landScale ? buildMethod2(method2Raw?.detailRows) : null
          const method3 = methodAvailability.workload ? buildMethod3(method3Raw?.detailRows) : null
          const method4 = methodAvailability.hourly ? buildMethod4(method4Raw?.detailRows) : null

          if (methodAvailability.landScale && method2RawRows.length > 0 && method2?.det?.length != null) {
            const rawLen = method2RawRows.length
            const detLen = method2.det.length
            if (rawLen > detLen) {
              console.warn('[export][landScale-duplicate-rows-deduped]', {
                contractId,
                serviceId: serviceIdText,
                rawRows: rawLen,
                exportedRows: detLen
              })
            }
          }
          const sanitizedSourceRow = sourceRow
            ? {
              ...sourceRow,
              investScale: methodAvailability.investmentScale ? sourceRow.investScale : null,
              landScale: methodAvailability.landScale ? sourceRow.landScale : null,
              workload: methodAvailability.workload ? sourceRow.workload : null,
              hourly: methodAvailability.hourly ? sourceRow.hourly : null,
              subtotal: sumNumbers([
                methodAvailability.investmentScale ? toFiniteNumber(sourceRow.investScale) : 0,
                methodAvailability.landScale ? toFiniteNumber(sourceRow.landScale) : 0,
                methodAvailability.workload ? toFiniteNumber(sourceRow.workload) : 0,
                methodAvailability.hourly ? toFiniteNumber(sourceRow.hourly) : 0
              ])
            }
            : sourceRow
          const fee = buildServiceFee(sanitizedSourceRow, method1, method2, method3, method4)
          const finalFee = buildServiceFinalFee(sanitizedSourceRow, method1, method2, method3, method4)
          const tasks = await buildServiceTasks(contractId, serviceIdText)
          const process = Number(sourceRow?.process) === 1 ? 1 : 0

          const disabledMethodLeak = {
            investScale: !methodAvailability.investmentScale && (toFiniteNumber(sourceRow?.investScale) != null || Boolean(method1Raw)),
            landScale: !methodAvailability.landScale && (toFiniteNumber(sourceRow?.landScale) != null || Boolean(method2Raw)),
            workload: !methodAvailability.workload && (toFiniteNumber(sourceRow?.workload) != null || Boolean(method3Raw)),
            hourly: !methodAvailability.hourly && (toFiniteNumber(sourceRow?.hourly) != null || Boolean(method4Raw))
          }
          if (disabledMethodLeak.investScale || disabledMethodLeak.landScale || disabledMethodLeak.workload || disabledMethodLeak.hourly) {
            console.warn('[export][method-disabled-leak-detected]', {
              contractId,
              serviceId: serviceIdText,
              methodAvailability,
              sourceRow,
              hasMethodPayload: {
                method1: Boolean(method1Raw),
                method2: Boolean(method2Raw),
                method3: Boolean(method3Raw),
                method4: Boolean(method4Raw)
              },
              disabledMethodLeak
            })
          }

          console.log('[export][service-methods]', {
            contractId,
            serviceId: serviceIdText,
            methodAvailability,
            exported: {
              method1: Boolean(method1),
              method2: Boolean(method2),
              method3: Boolean(method3),
              method4: Boolean(method4)
            },
            fee,
            finalFee
          })

          const service: ExportService = {
            id: serviceId,
            process,
            fee,
            finalFee,
            tasks
          }
          if (method1) service.method1 = method1
          if (method2) service.method2 = method2
          if (method3) service.method3 = method3
          if (method4) service.method4 = method4
          return service
        })
      )
    ).filter((item): item is ExportService => Boolean(item))

    const fixedFinalFee = toFiniteNumber(fixedRow?.finalFee)
    const fixedSubtotal = toFiniteNumber(fixedRow?.subtotal)
    const serviceFeeRaw = fixedFinalFee ?? fixedSubtotal ?? 0
    const serviceFee = toMoney(serviceFeeRaw)
    const [addtional, reserve] = await Promise.all([
      buildAdditionalExport(contractId),
      buildReserveExport(contractId)
    ])
    const addtionalFee = toMoney(addtional ? addtional.fee : 0)
    const reserveFee = toMoney(reserve ? reserve.fee : 0)
    const contractFee = toMoney(addNumbers(serviceFee, addtionalFee, reserveFee))
    const contractScale = htInfoRaw?.roughCalcEnabled ? [] : toExportScaleRows(htInfoRaw?.detailRows)

    const contractServiceCoesRaw = buildProjectServiceCoes(htConsultCategoryFactorState.resolved?.detailRows)
    const contractMajorCoesRaw = buildProjectMajorCoes(htMajorFactorState.resolved?.detailRows)
    console.log('[export][contract factor rows]', {
      contractId,
      consultFactorPinia: htConsultCategoryFactorState.piniaData,
      consultFactorKv: htConsultCategoryFactorState.kvData,
      consultFactorResolved: htConsultCategoryFactorState.resolved,
      majorFactorPinia: htMajorFactorState.piniaData,
      majorFactorKv: htMajorFactorState.kvData,
      majorFactorResolved: htMajorFactorState.resolved,
      contractServiceCoesRaw,
      contractMajorCoesRaw
    })

    contracts.push({
      name: isNonEmptyString(contract.name)
        ? contract.name
        : t('tab.messages.contractFallbackName', { index: index + 1 }),
      serviceFee,
      addtionalFee,
      reserveFee,
      fee: contractFee,
      quality: isNonEmptyString(htBaseInfoRaw?.quality) ? htBaseInfoRaw.quality.trim() : '',
      duration: isNonEmptyString(htBaseInfoRaw?.duration) ? htBaseInfoRaw.duration.trim() : '',
      scale: contractScale,
      serviceCoes: contractServiceCoesRaw,
      majorCoes: contractMajorCoesRaw,
      services,
      addtional,
      reserve
    })
  }

  return {
    name: projectName,
    writer,
    reviewer,
    company,
    date,
    industry,
    fee: toMoney(sumNumbers(contracts.map(item => item.fee))),
    scaleCost: projectScaleCost,
    overview,
    desc,
    scale: projectScale,
    serviceCoes: projectServiceCoes,
    majorCoes: projectMajorCoes,
    contracts
  }
}

const exportData = async () => {
  try {
    const now = new Date()
    const currentProjectId = readCurrentProjectId()
    const piniaForageStores = await Promise.all(
      getPiniaPersistStores().map(async ({ storeName, store }) => ({
        storeName,
        entries: await readForage(store)
      }))
    )
    const payload: DataPackage = {
      version: 3,
      packageType: 'project-snapshot',
      exportedAt: now.toISOString(),
      projectId: currentProjectId,
      localStorage: [],
      sessionStorage: [],
      localforageDefault: await readForage(projectDefaultForage),
      localforageStores: piniaForageStores
    }

    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
    const projectName = getExportProjectName(payload.localforageDefault, PROJECT_INFO_DB_KEY, LEGACY_PROJECT_DB_KEY)
    const timestamp = formatExportTimestamp(now)
    link.download = `${projectName}-${timestamp}${ZW_FILE_EXTENSION}`
    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
    URL.revokeObjectURL(url)
  } catch (error) {
    console.error('export failed:', error)
    showMessageDialog(t('tab.toast.failed'), t('ht.retry'))
  } finally {
    dataMenuOpen.value = false
  }
}

const exportReport = async () => {
  try {
    const now = new Date()
    const projectInfoRaw = await kvStore.getItem<XmInfoLike>(PROJECT_INFO_DB_KEY)
    const projectName = isNonEmptyString(projectInfoRaw?.projectName)
      ? sanitizeFileNamePart(projectInfoRaw.projectName)
      : t('tab.messages.defaultProjectName')
    const fileName = `${formatExportTimestamp(now)}-${projectName}${t('tab.messages.reportFileSuffix')}`
    const blobUrl = await exportFile(fileName, () => buildExportReportPayload(), () => {
      showReportExportProgress(30, t('tab.messages.reportGenerating'))
    })
    finishReportExportProgress(true, t('tab.messages.reportExportDone'), blobUrl)
  } catch (error) {
    console.error('export report failed:', error)
    finishReportExportProgress(false, t('tab.messages.reportExportFailedRetry'))

  } finally {
    dataMenuOpen.value = false
  }
}
const triggerImport = () => {
  importFileRef.value?.click()
}

const prepareImportPayloadFromFile = async (
  file: File,
  options?: { skipConfirm?: boolean }
) => {
  if (!file.name.toLowerCase().endsWith(ZW_FILE_EXTENSION)) {
    throw new Error('INVALID_FILE_EXT')
  }
  const buffer = await file.arrayBuffer()
  const payload = await decodeZwArchive<DataPackage>(buffer)
  if (!isDataPackageLike(payload)) {
    throw new Error('INVALID_DATA_PACKAGE')
  }
  pendingImportPayload.value = payload
  pendingImportFileName.value = file.name
  if (options?.skipConfirm) {
    await confirmImportOverride()
    return
  }
  importConfirmOpen.value = true
}

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

  try {
    await prepareImportPayloadFromFile(file)
  } catch (error) {
    console.error('import failed:', error)
    showMessageDialog(t('tab.messages.importFailedTitle'), t('tab.messages.importInvalidFile'))
  } finally {
    input.value = ''
  }
}

const cancelImportConfirm = () => {
  importConfirmOpen.value = false
  pendingImportPayload.value = null
  pendingImportFileName.value = ''
}

const confirmImportOverride = async () => {
  const payload = pendingImportPayload.value
  if (!payload) return
  try {
    await writeForage(projectDefaultForage, normalizeEntries(payload.localforageDefault))
    const piniaSnapshots = normalizeForageStoreSnapshots(payload.localforageStores)
    const snapshotMap = new Map(piniaSnapshots.map(item => [item.storeName, item.entries]))
    await Promise.all(
      getPiniaPersistStores().map(async ({ storeName, store }) => {
        const entries = snapshotMap.get(storeName) || []
        await writeForage(store, entries)
      })
    )

    const readPersistedState = (storeId: string) => {
      const storeName = getPiniaPersistStoreName(storeId)
      const entries = snapshotMap.get(storeName) || []
      const hit = entries.find(entry => entry.key === storeName)
      return hit && hit.value && typeof hit.value === 'object' ? hit.value : null
    }

    const tabsState = readPersistedState('tabs')
    if (tabsState) {
      tabStore.$patch(tabsState as any)
    } else {
      tabStore.resetTabs()
    }

    const zxFwPricingState = readPersistedState('zxFwPricing')
    if (zxFwPricingState) {
      zxFwPricingStore.$patch(zxFwPricingState as any)
    }

    const zxFwPricingKeysState = readPersistedState('zxFwPricingKeys')
    if (zxFwPricingKeysState) {
      zxFwPricingKeysStore.$patch(zxFwPricingKeysState as any)
    }

    const zxFwPricingHtFeeState = readPersistedState('zxFwPricingHtFee')
    if (zxFwPricingHtFeeState) {
      zxFwPricingHtFeeStore.$patch(zxFwPricingHtFeeState as any)
    }

    const kvState = readPersistedState('kv')
    if (kvState) {
      kvStore.$patch(kvState as any)
    }

    // 导入快照可能携带旧语言标题，先按当前 locale 归一化一次再持久化。
    syncTabLabelsByLocale()

    await Promise.all([
      tabStore.$persistNow?.(),
      zxFwPricingStore.$persistNow?.(),
      zxFwPricingKeysStore.$persistNow?.(),
      zxFwPricingHtFeeStore.$persistNow?.(),
      kvStore.$persistNow?.()
    ])
    dataMenuOpen.value = false
    window.location.reload()
  } catch (error) {
    console.error('import apply failed:', error)
    showMessageDialog(t('tab.messages.importFailedTitle'), t('tab.messages.importWriteError'))
  } finally {
    cancelImportConfirm()
  }
}

const handleReset = async () => {
  if (isResetting.value) return
  resetUiWorkspaceMode.value = readWorkspaceMode()
  const wait = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms))
  const resetStartedAt = Date.now()
  const allProjectIds = Array.from(new Set(['default', 'quick', ...listProjects().map(item => item.id)]))
  const deleteIndexedDBByName = (dbName: string) =>
    new Promise<void>((resolve) => {
      try {
        const request = window.indexedDB?.deleteDatabase(dbName)
        if (!request) {
          resolve()
          return
        }
        request.onsuccess = () => resolve()
        request.onerror = () => resolve()
        request.onblocked = () => {
          // 被阻塞时也继续流程，刷新后浏览器会重试清理。
          resolve()
        }
      } catch (_error) {
        resolve()
      }
    })

  const purgeKnownIndexedDB = async () => {
    await Promise.all(allProjectIds.map(id => deleteIndexedDBByName(getProjectDbName(id))))
  }

  try {
    isResetting.value = true
    dataMenuOpen.value = false
    projectMenuOpen.value = false
    emitResetAll()
    window.dispatchEvent(new CustomEvent('jgjs-release-project-lock'))
    const themePreference =
      typeof localStorage !== 'undefined' ? localStorage.getItem(THEME_PREFERENCE_KEY) : null
    const localePreference =
      typeof localStorage !== 'undefined' ? localStorage.getItem(I18N_LOCALE_KEY) : null
    const uiPrefsSnapshot =
      typeof localStorage !== 'undefined' ? localStorage.getItem(UI_PREFS_STORAGE_KEY) : null

    // 1) 仅清持久化层，不先改当前页面内存态，避免“先切首页再刷新”的二次闪烁。
    localStorage.clear()
    if (themePreference != null) {
      localStorage.setItem(THEME_PREFERENCE_KEY, themePreference)
    }
    if (localePreference != null) {
      localStorage.setItem(I18N_LOCALE_KEY, localePreference)
    }
    if (uiPrefsSnapshot != null) {
      localStorage.setItem(UI_PREFS_STORAGE_KEY, uiPrefsSnapshot)
    }
    sessionStorage.clear()
    await projectDefaultForage.clear()

    // 2) 清 pinia 分库持久化
    await Promise.all(
      getPiniaPersistStores().map(async ({ store }) => {
        await store.clear()
      })
    )

    // 3) 清插件按 store 维护的持久化 key（双保险）
    const clearTasks: Promise<void>[] = []
    if (tabStore.$clearPersisted) clearTasks.push(tabStore.$clearPersisted())
    if (zxFwPricingStore.$clearPersisted) clearTasks.push(zxFwPricingStore.$clearPersisted())
    if (kvStore.$clearPersisted) clearTasks.push(kvStore.$clearPersisted())
    await Promise.all(clearTasks)

    // 4) 强制删除已知 IndexedDB，避免页面卸载阶段旧组件回写脏数据
    await purgeKnownIndexedDB()

    // 5) 需要保留的最小标记恢复
    writeProjectIdToUrl('default')
    writeWorkspaceMode('project')
    localStorage.setItem(USER_GUIDE_COMPLETED_KEY, '1')

    // 6) 保证重置按钮进度动画至少可见 2s，避免“刚点就消失”。
    const elapsed = Date.now() - resetStartedAt
    if (elapsed < RESET_MIN_LOADING_MS) {
      await wait(RESET_MIN_LOADING_MS - elapsed)
    }

    // 7) 直接刷新，交由默认初始化重建空状态
    window.location.reload()
  } catch (error) {
    console.error('reset failed:', error)
    isResetting.value = false
    resetUiWorkspaceMode.value = ''
  }
}

const handleResetConfirmOpenChange = (open: boolean) => {
  if (isResetting.value) return
  resetConfirmOpen.value = open
}

onMounted(() => {
  currentProjectId.value = readCurrentProjectId()
  upsertProject(currentProjectId.value)
  void refreshProjectList()
  window.addEventListener('mousedown', handleGlobalMouseDown)
  window.addEventListener('keydown', handleGlobalKeyDown)
  window.addEventListener('resize', scheduleUpdateTabTitleOverflow)
  const skipWorkspaceImportConfirm = consumePendingHomeImportSkipConfirm()
  void nextTick(() => {
    bindTabStripScroll()
    ensureActiveTabVisible()
    updateTabScrollButtons()
    scheduleUpdateTabTitleOverflow()
    scheduleRestoreTabInnerScrollTop(tabStore.activeTabId)
  })
  void (async () => {
    const pendingHomeImportFile = await consumePendingHomeImportFile()
    if (!pendingHomeImportFile) return
    await prepareImportPayloadFromFile(pendingHomeImportFile, { skipConfirm: skipWorkspaceImportConfirm }).catch(error => {
      console.error('home import failed:', error)
      showMessageDialog(t('tab.messages.importFailedTitle'), t('tab.messages.importInvalidFile'))
    })
  })()
  void (async () => {
    if (await shouldAutoOpenGuide()) {
      openUserGuide(0)
    }
  })()
})

onBeforeUnmount(() => {
  window.removeEventListener('mousedown', handleGlobalMouseDown)
  window.removeEventListener('keydown', handleGlobalKeyDown)
  window.removeEventListener('resize', scheduleUpdateTabTitleOverflow)
  if (tabStripViewportEl) {
    tabStripViewportEl.removeEventListener('scroll', handleTabStripScroll)
    tabStripViewportEl = null
  }
  if (tabTitleOverflowRafId != null) {
    cancelAnimationFrame(tabTitleOverflowRafId)
    tabTitleOverflowRafId = null
  }
})

watch(
  () => workspaceModeForUi.value,
  (mode, prevMode) => {
    if (typeof window === 'undefined') return
    if (mode === 'quick') {
      if (quickModeThemePreferenceSnapshot.value === undefined) {
        quickModeThemePreferenceSnapshot.value = localStorage.getItem(THEME_PREFERENCE_KEY)
      }
      isDark.value = false
      document.documentElement.classList.remove('dark')
      return
    }
    if (prevMode !== 'quick' || quickModeThemePreferenceSnapshot.value === undefined) return
    const snapshot = quickModeThemePreferenceSnapshot.value
    quickModeThemePreferenceSnapshot.value = undefined
    if (snapshot == null) {
      localStorage.removeItem(THEME_PREFERENCE_KEY)
      isDark.value = window.matchMedia('(prefers-color-scheme: dark)').matches
      return
    }
    localStorage.setItem(THEME_PREFERENCE_KEY, snapshot)
    isDark.value = snapshot === 'true'
  },
  { immediate: true }
)

watch(
  () => tabStore.activeTabId,
  (nextId, prevId) => {
    saveTabInnerScrollTop(prevId)
    void nextTick(() => {
      bindTabStripScroll()
      ensureActiveTabVisible()
      updateTabScrollButtons()
      scheduleUpdateTabTitleOverflow()
      scheduleRestoreTabInnerScrollTop(nextId)
    })
  }
)

watch(
  () => tabStore.tabs.map((t:any) => t.id),
  (ids) => {
    const idSet = new Set(ids)
    try {
      for (let i = sessionStorage.length - 1; i >= 0; i--) {
        const key = sessionStorage.key(i)
        if (!key || !key.startsWith('tab-scroll-top:')) continue
        const tabId = key.slice('tab-scroll-top:'.length)
        if (!idSet.has(tabId)) sessionStorage.removeItem(key)
      }
    } catch (error) {
      console.error('cleanup tab scroll cache failed:', error)
    }
    void nextTick(() => {
      bindTabStripScroll()
      updateTabScrollButtons()
      scheduleUpdateTabTitleOverflow()
    })
  }
)

watch(
  () => locale.value,
  () => {
    syncTabLabelsByLocale()
  },
  { immediate: true }
)
</script>

<template>
  <ToastProvider>
  <TooltipProvider>
    <div class="flex flex-col w-full h-screen bg-background overflow-hidden">
      <div class="grid grid-cols-[minmax(0,1fr)_auto] items-start gap-2 border-b bg-muted/30 px-2 pt-1 h-15 flex-none">
        <div class="flex min-w-0 items-start gap-1 h-full" @mouseenter="isTabStripHover = true"
          @mouseleave="isTabStripHover = false">
          <button type="button" :class="[
            'h-9 w-8  self-center  shrink-0 cursor-pointer rounded border bg-background text-sm text-muted-foreground transition hover:bg-muted',
            isTabStripHover && showTabScrollLeft ? 'opacity-100' : 'pointer-events-none opacity-0'
          ]" @click="scrollTabStripBy(-260)">
            &lt;
          </button>
          <ScrollArea :ref="setTabScrollAreaRef" type="auto"
            class="h-full tab-strip-scroll-area min-w-0 flex-1 whitespace-nowrap">
            <draggable v-model="tabsModel" item-key="id" tag="div"
              :class="['tab-strip-sortable h-[calc(3.50rem)] flex w-max gap-0', isTabDragging ? 'is-dragging' : '']"
              :animation="260" easing="cubic-bezier(0.22, 1, 0.36, 1)" ghost-class="tab-drag-ghost"
              chosen-class="tab-drag-chosen" drag-class="tab-drag-active" :move="canMoveTab" @start="handleTabDragStart"
              @end="handleTabDragEnd">
              <template #item="{ element: tab, index }">
                <div :ref="el => setTabItemRef(tab.id, el)" @mousedown.left="tabStore.activeTabId = tab.id"
                  @contextmenu.prevent="openTabContextMenu($event, tab.id)" :class="[
                    'tab-item group relative -mb-px -ml-px first:ml-0 flex items-center h-full px-4 min-w-[120px] max-w-[220px] cursor-pointer rounded-t-md border border-transparent transition-[background-color,border-color,color,box-shadow,transform] duration-200 text-sm',
                    tabStore.activeTabId === tab.id && !isTabDragging
                      ? 'z-10 bg-background text-foreground !border-border !border-b-0 font-medium'
                      : 'bg-muted/25 text-muted-foreground hover:bg-muted/40 hover:text-foreground hover:border-border/70',
                    index !== 0 ? 'cursor-move' : ''
                  ]">
                  <TooltipRoot>
                    <TooltipTrigger as-child>
                      <span :ref="el => setTabTitleRef(tab.id, el)" class="truncate mr-2">
                        {{ tab.title }}
                      </span>
                    </TooltipTrigger>
                    <TooltipContent v-if="tabTitleOverflowMap[tab.id]" side="bottom">{{ tab.title }}</TooltipContent>
                  </TooltipRoot>

                  <Button v-if="index !== 0" variant="ghost" size="icon"
                    class="h-4 w-4 ml-auto opacity-0 group-hover:opacity-100 hover:bg-destructive hover:text-destructive-foreground transition-opacity"
                    @click.stop="tabStore.removeTab(tab.id)">
                    <X class="h-3 w-3" />
                  </Button>
                </div>
              </template>
            </draggable>
          </ScrollArea>
          <button type="button" :class="[
            ' self-center  h-9 w-8 shrink-0 cursor-pointer rounded border bg-background text-sm text-muted-foreground transition hover:bg-muted ',
            isTabStripHover && showTabScrollRight ? 'opacity-100' : 'pointer-events-none opacity-0'
          ]" @click="scrollTabStripBy(260)">
            &gt;
          </button>
        </div>
        <div class="flex shrink-0 self-center items-center gap-1">
          <Button
            v-if="workspaceModeForUi !== 'quick'"
            variant="ghost"
            size="icon"
            class="h-9 w-9 shrink-0 cursor-pointer text-muted-foreground transition-all duration-200 hover:text-foreground"
            :disabled="isResetting"
            :title="isDark ? t('tab.toolbar.dark') : t('tab.toolbar.light')"
            :aria-label="isDark ? t('tab.toolbar.dark') : t('tab.toolbar.light')"
            @click="handleToggleTheme"
          >
            <component
              :is="isDark ? Moon : Sun"
              class="h-4 w-4 transition-transform duration-200"
              :class="isDark ? 'rotate-0' : 'rotate-180'"
            />
          </Button>
          <Button
            v-if="workspaceModeForUi !== 'quick'"
            variant="ghost"
            size="sm"
            class="h-9 min-w-9 px-2 shrink-0 cursor-pointer text-muted-foreground transition-all duration-200 hover:text-foreground"
            :disabled="isResetting"
            :title="t('tab.toolbar.language')"
            :aria-label="t('tab.toolbar.language')"
            @click="toggleLocale"
          >
            {{ localeLabel }}
          </Button>

          <div v-if="workspaceModeForUi !== 'quick'" ref="dataMenuRef" class="relative shrink-0">
            <Button variant="outline" size="sm"
              class="app-toolbar-btn shrink-0 cursor-pointer transition-all duration-200"
              :class="dataMenuOpen ? 'border-primary/40 bg-primary/5 text-foreground' : ''"
              :disabled="isResetting"
              @click="dataMenuOpen = !dataMenuOpen">
              <ChevronDown
                class="mr-1 h-4 w-4 transition-transform duration-200"
                :class="dataMenuOpen ? 'rotate-180' : 'rotate-0'"
              />
              {{ t('tab.toolbar.importExport') }}
            </Button>
            <Transition name="toolbar-dropdown">
              <div v-if="dataMenuOpen"
                class="absolute right-0 top-full mt-1 z-50 rounded-md border bg-background p-1 shadow-md">
                <button class="w-full cursor-pointer rounded px-3 py-1.5 text-left text-sm hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50"
                  :disabled="isResetting"
                  @click="triggerImport">
                  {{ t('tab.toolbar.importData') }}
                </button>
                <button class="w-full cursor-pointer rounded px-3 py-1.5 text-left text-sm hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50"
                  :disabled="isResetting"
                  @click="exportData">
                  {{ t('tab.toolbar.exportData') }}
                </button>
                <button class="w-full cursor-pointer rounded px-3 py-1.5 text-left text-sm hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50"
                :disabled="isResetting"
                v-if="workspaceModeForUi !== 'quick'"
                @click="exportReport">
                  {{ t('tab.toolbar.exportReport') }}
                </button>
              </div>
            </Transition>
            <input ref="importFileRef" type="file" accept=".zw" class="hidden" @change="importData" />
          </div>

          <Button v-if="workspaceModeForUi !== 'quick'" variant="outline" size="sm" class="app-toolbar-btn shrink-0 cursor-pointer"
            :disabled="isResetting"
            @click="openUserGuide(0)">
            <CircleHelp class="h-4 w-4 mr-1" />
            {{ t('tab.toolbar.userGuide') }}
          </Button>
          <Button
            v-if="workspaceModeForUi === 'quick'"
            variant="ghost"
            size="sm"
            class="h-9 min-w-9 px-2 shrink-0 cursor-pointer text-muted-foreground transition-all duration-200 hover:text-foreground"
            :disabled="isResetting"
            :title="t('tab.toolbar.language')"
            :aria-label="t('tab.toolbar.language')"
            @click="toggleLocale"
          >
            {{ localeLabel }}
          </Button>
          <Button
            v-if="workspaceModeForUi === 'quick'"
            variant="outline"
            size="sm"
            class="app-toolbar-btn shrink-0 cursor-pointer"
            @click="returnToHome"
          >
            {{ t('tab.toolbar.backHome') }}
          </Button>

          <div v-if="workspaceModeForUi !== 'quick'" ref="projectMenuRef" class="relative shrink-0">
            <Button
              variant="outline"
              size="sm"
              class="app-toolbar-btn shrink-0 cursor-pointer transition-all duration-200"
              :class="projectMenuOpen ? 'border-primary/40 bg-primary/5 text-foreground' : ''"
              :disabled="isResetting"
              @click="projectMenuOpen = !projectMenuOpen; if (projectMenuOpen) void refreshProjectList()"
            >
              <ChevronDown
                class="mr-1 h-4 w-4 transition-transform duration-200"
                :class="projectMenuOpen ? 'rotate-180' : 'rotate-0'"
              />
              {{ t('tab.toolbar.projectList') }}
            </Button>
            <Transition name="toolbar-dropdown">
              <div
                v-if="projectMenuOpen"
                class="absolute right-0 top-full z-50 mt-1 w-[420px] rounded-md border bg-background p-2 shadow-md"
              >
                <div class="max-h-56 space-y-1 overflow-auto">
                  <div
                    v-for="project in projectList"
                    :key="project.id"
                    class="flex items-center gap-2 rounded px-2 py-1.5"
                    :class="isProjectOpen(project.id) ? 'opacity-60' : 'hover:bg-muted'"
                  >
                    <button
                      class="flex-1 text-left text-sm"
                      :class="isProjectOpen(project.id) ? 'cursor-not-allowed' : 'cursor-pointer'"
                      :disabled="isProjectOpen(project.id)"
                      @click="openProjectInNewTab(project.id)"
                    >
                      <div class="font-medium leading-5">
                        {{ project.name }}
                        <span v-if="isProjectOpen(project.id)" class="ml-1 text-xs text-muted-foreground">{{ t('tab.toolbar.opened') }}</span>
                      </div>
                      <div class="text-xs text-muted-foreground">{{ t('tab.toolbar.lastEdited', { time: formatProjectEditedTime(project.updatedAt) }) }}</div>
                    </button>
                    <Button
                      variant="ghost"
                      size="sm"
                      class="h-7 px-2 text-xs"
                      @click="requestRemoveProjectItem(project)"
                    >
                      {{ t('common.delete') }}
                    </Button>
                  </div>
                </div>
                <div class="mt-2 flex items-center justify-end gap-2 border-t pt-2">
                  <span class="mr-auto text-xs text-muted-foreground">{{ projectCountText }}</span>
                  <Button size="sm" class="h-8 px-3 text-xs" @click="openCreateProjectDialog">
                    {{ t('tab.toolbar.createProject') }}
                  </Button>
                  <Button variant="outline" size="sm" class="h-8 px-3 text-xs" @click="returnToHome">
                    {{ t('tab.toolbar.backHome') }}
                  </Button>
                  <Button variant="destructive" size="sm" class="h-8 px-3 text-xs" @click="resetConfirmOpen = true">
                    {{ t('tab.toolbar.resetAll') }}
                  </Button>
                </div>
              </div>
            </Transition>
          </div>

          <AlertDialogRoot :open="resetConfirmOpen" @update:open="handleResetConfirmOpenChange">
            <AlertDialogPortal>
              <AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
              <AlertDialogContent
                class="fixed left-1/2 top-1/2 z-50 w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
                <AlertDialogTitle class="text-base font-semibold">{{ t('tab.dialog.resetTitle') }}</AlertDialogTitle>
                <AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
                  {{ t('tab.dialog.resetDesc') }}
                </AlertDialogDescription>
                <div class="mt-4 flex items-center justify-end gap-2">
                  <Button variant="outline" :disabled="isResetting" @click="resetConfirmOpen = false">{{ t('common.cancel') }}</Button>
                  <Button variant="destructive" :disabled="isResetting" @click="handleReset">
                    <Loader2 v-if="isResetting" class="mr-1 h-4 w-4 animate-spin" />
                    {{ isResetting ? t('tab.toolbar.resetting') : t('tab.dialog.confirmReset') }}
                  </Button>
                </div>
              </AlertDialogContent>
            </AlertDialogPortal>
          </AlertDialogRoot>

          <AlertDialogRoot :open="importConfirmOpen" @update:open="importConfirmOpen = $event">
            <AlertDialogPortal>
              <AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
              <AlertDialogContent
                class="fixed left-1/2 top-1/2 z-50 w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
                <AlertDialogTitle class="text-base font-semibold">{{ t('tab.dialog.importOverrideTitle') }}</AlertDialogTitle>
                <AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
                  {{ t('tab.dialog.importOverrideDesc', { file: pendingImportFileName || t('home.cards.pickFile') }) }}
                </AlertDialogDescription>
                <div class="mt-4 flex items-center justify-end gap-2">
                  <AlertDialogCancel as-child>
                    <Button variant="outline" @click="cancelImportConfirm">{{ t('common.cancel') }}</Button>
                  </AlertDialogCancel>
                  <AlertDialogAction as-child>
                    <Button variant="destructive" @click="confirmImportOverride">{{ t('tab.dialog.confirmOverride') }}</Button>
                  </AlertDialogAction>
                </div>
              </AlertDialogContent>
            </AlertDialogPortal>
          </AlertDialogRoot>

          <AlertDialogRoot :open="newProjectDialogOpen" @update:open="handleCreateProjectDialogOpenChange">
            <AlertDialogPortal>
              <AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
              <AlertDialogContent
                class="fixed left-1/2 top-1/2 z-50 w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
                <AlertDialogTitle class="text-base font-semibold">{{ t('tab.dialog.newProjectTitle') }}</AlertDialogTitle>
                <AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
                  {{ t('tab.dialog.newProjectDesc') }}
                </AlertDialogDescription>
                <div class="mt-4 space-y-2">
                  <label class="text-sm font-medium text-foreground">{{ t('home.dialog.industry') }}</label>
                  <SelectRoot v-model="newProjectIndustry">
                    <SelectTrigger
                      class="inline-flex h-11 w-full items-center justify-between rounded-xl border border-border/70 bg-[linear-gradient(180deg,rgba(248,250,252,0.98),rgba(241,245,249,0.92))] px-4 text-sm text-foreground shadow-sm outline-none transition hover:border-border focus-visible:ring-2 focus-visible:ring-slate-300"
                    >
                      <span :class="newProjectIndustryLabel ? 'text-foreground' : 'text-muted-foreground'">
                        {{ newProjectIndustryLabel || t('home.dialog.selectIndustry') }}
                      </span>
                      <SelectIcon as-child>
                        <ChevronDown class="h-4 w-4 text-muted-foreground" />
                      </SelectIcon>
                    </SelectTrigger>
                    <SelectPortal>
                      <SelectContent
                        :side-offset="6"
                        position="popper"
                        class="z-[120] w-[var(--reka-select-trigger-width)] overflow-hidden rounded-xl border border-border bg-popover text-popover-foreground shadow-xl"
                      >
                        <SelectViewport class="p-1">
                          <SelectItem
                            v-for="item in industryTypeList"
                            :key="`new-project-${item.id}`"
                            :value="String(item.id)"
                            class="relative flex h-9 w-full cursor-default select-none items-center rounded-md pl-3 pr-8 text-sm outline-none data-[highlighted]:bg-muted data-[highlighted]:text-foreground data-[state=checked]:bg-slate-100"
                          >
                            <SelectItemText>{{ getIndustryDisplayName(item.id, locale) }}</SelectItemText>
                            <SelectItemIndicator class="absolute right-2 inline-flex items-center text-slate-700">
                              <Check class="h-4 w-4" />
                            </SelectItemIndicator>
                          </SelectItem>
                        </SelectViewport>
                      </SelectContent>
                    </SelectPortal>
                  </SelectRoot>
                </div>
                <div class="mt-4 flex items-center justify-end gap-2">
                  <AlertDialogCancel as-child>
                    <Button variant="outline" :disabled="newProjectSubmitting" @click="closeCreateProjectDialog">{{ t('common.cancel') }}</Button>
                  </AlertDialogCancel>
                  <AlertDialogAction as-child>
                    <Button :disabled="newProjectSubmitting || !newProjectIndustry" @click="handleCreateProjectConfirm">
                      <Loader2 v-if="newProjectSubmitting" class="mr-1 h-4 w-4 animate-spin" />
                      {{ newProjectSubmitting ? t('tab.dialog.creating') : t('tab.dialog.createAndOpen') }}
                    </Button>
                  </AlertDialogAction>
                </div>
              </AlertDialogContent>
            </AlertDialogPortal>
          </AlertDialogRoot>

          <AlertDialogRoot :open="projectLimitDialogOpen" @update:open="projectLimitDialogOpen = $event">
            <AlertDialogPortal>
              <AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
              <AlertDialogContent
                class="fixed left-1/2 top-1/2 z-50 w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
                <AlertDialogTitle class="text-base font-semibold">{{ t('tab.dialog.projectLimitTitle') }}</AlertDialogTitle>
                <AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
                  {{ t('tab.dialog.projectLimitDesc', { max: MAX_PROJECT_COUNT }) }}
                </AlertDialogDescription>
                <div class="mt-4 flex items-center justify-end gap-2">
                  <AlertDialogAction as-child>
                    <Button @click="projectLimitDialogOpen = false">{{ t('tab.dialog.iKnow') }}</Button>
                  </AlertDialogAction>
                </div>
              </AlertDialogContent>
            </AlertDialogPortal>
          </AlertDialogRoot>

          <AlertDialogRoot :open="projectDeleteConfirmOpen" @update:open="handleProjectDeleteDialogOpenChange">
            <AlertDialogPortal>
              <AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
              <AlertDialogContent
                class="fixed left-1/2 top-1/2 z-50 w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
                <AlertDialogTitle class="text-base font-semibold">{{ t('tab.dialog.deleteProjectTitle') }}</AlertDialogTitle>
                <AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
                  {{ pendingDeleteProject?.id === currentProjectId
                    ? t('tab.dialog.deleteCurrentProjectDesc', { name: pendingDeleteProject?.name || '' })
                    : t('tab.dialog.deleteProjectDesc', { name: pendingDeleteProject?.name || '' }) }}
                </AlertDialogDescription>
                <div class="mt-4 flex items-center justify-end gap-2">
                  <Button variant="outline" @click="cancelRemoveProjectItem">{{ t('common.cancel') }}</Button>
                  <Button variant="destructive" @click="confirmRemoveProjectItem">{{ t('common.confirm') }}</Button>
                </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-50 w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
                <AlertDialogTitle class="text-base font-semibold">{{ 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>
        </div>

      </div>
      <div v-if="isResetting" class="fixed inset-0 z-40 cursor-wait bg-transparent" />

      <div class="flex-1 overflow-auto relative">
        <div
          v-if="activeTab"
          :key="`${activeTab.id}-${locale}`"
          :ref="el => setTabPanelRef(activeTabId, el)"
          class="h-full w-full animate-in fade-in duration-300"
        >
          <component :is="componentMap[activeTab.componentName]" v-bind="activeTab.props || {}" />
        </div>
      </div>

      <div v-if="tabContextOpen" ref="tabContextRef"
        class="fixed z-[70] w-max rounded-lg border border-border/80 bg-background/95 p-1.5 shadow-xl ring-1 ring-black/5 backdrop-blur-sm"
        :style="{ left: `${tabContextX}px`, top: `${tabContextY}px` }">
        <button
          class="flex cursor-pointer items-center whitespace-nowrap rounded-md px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/80 disabled:cursor-not-allowed disabled:opacity-50"
          :disabled="!hasClosableTabs" @click="runTabMenuAction('all')">
          {{ t('tab.menu.closeAll') }}
        </button>
        <button
          class="mt-0.5 flex cursor-pointer items-center whitespace-nowrap rounded-md px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/80 disabled:cursor-not-allowed disabled:opacity-50"
          :disabled="!canCloseLeft" @click="runTabMenuAction('left')">
          {{ t('tab.menu.closeLeft') }}
        </button>
        <button
          class="mt-0.5 flex cursor-pointer items-center whitespace-nowrap rounded-md px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/80 disabled:cursor-not-allowed disabled:opacity-50"
          :disabled="!canCloseRight" @click="runTabMenuAction('right')">
          {{ t('tab.menu.closeRight') }}
        </button>
        <button
          class="mt-0.5 flex cursor-pointer items-center whitespace-nowrap rounded-md px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/80 disabled:cursor-not-allowed disabled:opacity-50"
          :disabled="!canCloseOther" @click="runTabMenuAction('other')">
          {{ t('tab.menu.closeOther') }}
        </button>
      </div>

      <div v-if="userGuideOpen" class="fixed inset-0 z-[120] flex items-center justify-center bg-black/45 p-4"
        @click.self="closeUserGuide(false)">
        <div class="w-full max-w-3xl rounded-xl border bg-background shadow-2xl">
          <div class="flex items-start justify-between border-b px-6 py-5">
            <div>
              <p class="text-xs text-muted-foreground">{{ t('tab.guide.title') }} · {{ guideProgressText }}</p>
              <h3 class="mt-1 text-lg font-semibold">{{ activeGuideStep.title }}</h3>
            </div>
            <Button variant="ghost" size="icon" class="h-8 w-8" @click="closeUserGuide(false)">
              <X class="h-4 w-4" />
            </Button>
          </div>

          <div class="space-y-4 px-6 py-5">
            <p class="text-sm leading-6 text-foreground">{{ activeGuideStep.description }}</p>
            <ul class="list-disc space-y-2 pl-5 text-sm text-muted-foreground">
              <li v-for="(point, index) in activeGuideStep.points" :key="`${activeGuideStep.title}-${index}`">
                {{ point }}
              </li>
            </ul>
          </div>

          <div class="flex flex-col gap-3 border-t px-6 py-4 sm:flex-row sm:items-center sm:justify-between">
            <div class="flex items-center gap-1.5">
              <button v-for="(_step, index) in userGuideSteps" :key="`guide-dot-${index}`"
                class="h-2.5 w-2.5 cursor-pointer rounded-full transition-colors"
                :class="index === userGuideStepIndex ? 'bg-primary' : 'bg-muted'" :aria-label="t('tab.guide.jumpToStep', { index: index + 1 })"
                @click="jumpToGuideStep(index)" />
            </div>
            <div class="flex items-center justify-end gap-2">
              <Button variant="ghost" @click="closeUserGuide(false)">{{ t('tab.guide.later') }}</Button>
              <Button variant="outline" :disabled="isFirstGuideStep" @click="prevUserGuideStep">{{ t('tab.guide.prev') }}</Button>
              <Button @click="nextUserGuideStep">{{ isLastGuideStep ? t('tab.guide.finish') : t('tab.guide.next') }}</Button>
            </div>
          </div>
        </div>
      </div>
    </div>
    <ToastRoot
      v-model:open="reportExportToastOpen"
      :duration="0"
      class="pointer-events-auto rounded-xl border border-border bg-card px-4 py-3 text-foreground shadow-lg"
      @update:open="(val) => { if (!val) dismissReportToast() }"
    >
      <div class="flex items-start justify-between gap-2">
        <ToastTitle class="text-sm font-semibold text-foreground">
          {{ reportExportStatus === 'running' ? t('tab.toast.export') : (reportExportStatus === 'success' ? t('tab.toast.success') : t('tab.toast.failed')) }}
        </ToastTitle>
        <Button
          variant="ghost"
          size="sm"
          class="h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
          @click="dismissReportToast"
        >
          <X class="h-3.5 w-3.5" />
        </Button>
      </div>
      <ToastDescription class="mt-1 text-xs text-muted-foreground">{{ reportExportText }}</ToastDescription>
      <!-- <div v-if="reportExportStatus === 'success' && reportExportBlobUrl" class="mt-2 flex items-center gap-2">
        <Button size="sm" class="h-7 rounded-md px-3 text-xs" @click="openExportedReport">
          {{ t('tab.messages.openFile') }}
        </Button>
        <Button variant="ghost" size="sm" class="h-7 rounded-md px-2 text-xs text-muted-foreground" @click="dismissReportToast">
          {{ t('common.close') }}
        </Button>
      </div> -->
      <div  class="mt-2 flex items-center gap-2">
        <div class="h-1.5 flex-1 overflow-hidden rounded-full bg-muted">
          <div
            class="h-full transition-all duration-300"
            :class="reportExportStatus === 'error'
              ? 'bg-red-500'
              : (reportExportStatus === 'success' ? 'bg-foreground/70' : 'bg-foreground')"
            :style="{ width: `${reportExportProgress}%` }"
          />
        </div>
        <span class="shrink-0 text-[11px] tabular-nums text-muted-foreground">{{ reportExportProgress }}%</span>
      </div>
    </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/tab/tab.css"></style>
