const {
  app,
  BrowserWindow,
  Menu,
  ipcMain,
  net,
  safeStorage,
  shell,
  nativeImage,
  dialog,
  powerMonitor
} = require('electron')
const path = require('path')
const fs = require('fs')
const os = require('os')
const crypto = require('node:crypto')
const { spawn, spawnSync } = require('child_process')
const { EventEmitter } = require('events')
const appMetadata = require('./package.json')

// Electron/V8 内存设置:默认约 1.5GB,设为 4GB 足够存放调试变量值等
app.commandLine.appendSwitch('js-flags', '--max-old-space-size=4096')

const APP_ID = String(appMetadata.build?.appId || 'com.cide.app')
const APP_ICON_PATH = path.join(
  __dirname,
  'assets',
  'branding',
  'wangpeng-cangjie-icon.ico'
)

if (process.platform === 'win32') {
  app.setAppUserModelId(APP_ID)
  app.disableHardwareAcceleration()
  // 禁用 GPU 磁盘缓存,消除 cache_util_win / gpu_disk_cache 错误日志
  app.commandLine.appendSwitch('disable-gpu-shader-disk-cache')
}

function quoteWindowsCommandArgument(value) {
  const text = String(value)
  return `"${text
    .replace(/(\\*)"/g, '$1$1\\"')
    .replace(/(\\+)$/, '$1$1')}"`
}

function getWindowsRelaunchCommand() {
  const command = [process.execPath]
  if (!app.isPackaged) command.push(app.getAppPath())
  return command.map(quoteWindowsCommandArgument).join(' ')
}

function applyWindowsTaskbarBranding(win) {
  if (
    process.platform !== 'win32' ||
    typeof win?.setAppDetails !== 'function'
  ) {
    return
  }
  win.setAppDetails({
    appId: APP_ID,
    appIconPath: app.isPackaged ? process.execPath : APP_ICON_PATH,
    appIconIndex: 0,
    relaunchCommand: getWindowsRelaunchCommand(),
    relaunchDisplayName: 'CIDE'
  })
}

if (process.env.CIDE_E2E_USER_DATA) {
  app.setPath(
    'userData',
    path.resolve(process.env.CIDE_E2E_USER_DATA)
  )
} else if (app.isPackaged) {
  // 免安装版:exe 同目录存在 CIDE.portable 标记时,
  // 所有用户数据写入程序目录下的 data(可随 U 盘迁移、删目录即卸载)
  const {
    resolvePortableUserData
  } = require('./js/core/portable-core')
  const portableUserData = resolvePortableUserData(
    process.execPath,
    fs.existsSync
  )
  if (portableUserData) {
    fs.mkdirSync(portableUserData, { recursive: true })
    app.setPath('userData', portableUserData)
  }
}

const {
  CangjieLspManager,
  LSP_FILE_CHANGE_TYPE
} = require('./js/cangjie-lsp-client')
const { CangjieDebugManager } = require('./js/cangjie-debug-client')
const {
  getGitConflictLabel,
  getSafeGitRemoteLabel,
  parseGitStatus,
  splitGitRemoteBranch
} = require('./js/core/git-core')
const {
  OUTPUT_ROUTE,
  classifyBuildOutputLine,
  isCompileFailureEvidence,
  isCompilerDecorationLine,
  normalizePtyOutput,
  parseRunStatusLine,
  shouldEmitRuntimeOutputLine
} = require('./js/core/service-output-core')
const {
  injectCangjieSdkPath,
  resolveCangjieExecutable,
  resolveServiceWorkDir,
  shouldUsePtyForServiceRun
} = require('./js/core/service-run-core')
const {
  translateCjCompilerLine,
  translateExpectedFoundLine,
  translateCjRuntimeLine,
  formatZhHint
} = require('./js/core/cj-error-zh')
const {
  isCangjieSdkRoot,
  resolveCangjieSdk,
  enumerateSdkVersions
} = require('./js/core/cangjie-sdk-core')
const {
  findGitExecutable,
  resolveGitInstallation
} = require('./js/core/git-executable-core')
const { registerGitIpc } = require('./js/main/git-ipc')
const {
  createGitConflictService
} = require('./js/main/git-conflict-service')
const {
  registerPlatformIpc,
  registerProjectRoot,
  clearWebContentsRoots,
  isPathAllowedForSender
} = require('./js/main/platform-ipc')
const {
  createProjectTreeScanner
} = require('./js/main/project-tree-scanner')
const {
  createProjectSearchService,
  registerProjectSearchIpc
} = require('./js/main/project-search-service')
const {
  createCangjieWorkspaceIndexer,
  registerCangjieWorkspaceIndexIpc
} = require('./js/main/cangjie-workspace-indexer')
const {
  registerCangjieThrowsScannerIpc
} = require('./js/main/cangjie-throws-scanner')
const {
  registerCangjieLspIpc
} = require('./js/main/cangjie-lsp-ipc')
const {
  registerCangjieCjoIpc
} = require('./js/main/cangjie-cjo-ipc')
const {
  registerCangjieDocIpc
} = require('./js/main/cangjie-doc-ipc')
const {
  registerStdxDocIpc
} = require('./js/main/stdx-doc-ipc')
const {
  registerCourseContentIpc
} = require('./js/main/course-content-ipc')
const {
  registerContactQrcodeIpc
} = require('./js/main/contact-qrcode-ipc')
const {
  applyLspWorkspaceEdit
} = require('./js/main/lsp-workspace-edit-service')
const { createSettingsStore } = require('./js/main/settings-store')
const {
  createSecretCrypto,
  createPassthroughCrypto
} = require('./js/main/secret-crypto')
const {
  createWorkspaceSessionStore,
  registerWorkspaceSessionIpc
} = require('./js/main/workspace-session-store')
const {
  attachWindowSecurity,
  createSecureWebPreferences
} = require('./js/main/window-security')
const { registerAiProxyIpc } = require('./js/main/ai-proxy-ipc')
const {
  createProcessLifecycle
} = require('./js/main/process-lifecycle')
const {
  captureProcessErrors,
  createAppLogger
} = require('./js/main/app-logger')
const {
  createRendererCrashGuard
} = require('./js/main/renderer-crash-guard')
const {
  createRecoveryStore,
  registerRecoveryIpc
} = require('./js/main/recovery-store')
const {
  EditorFileError,
  MISSING_VERSION,
  readEditorFile,
  saveEditorFile
} = require('./js/main/editor-file-store')
const {
  createUpdateService,
  registerUpdateIpc
} = require('./js/main/update-service')
const {
  registerTerminalIpc
} = require('./js/main/terminal-ipc')
const {
  createNotesService,
  registerNotesIpc
} = require('./js/main/notes-service')
const {
  createCangjieSdkManager
} = require('./js/main/cangjie-sdk')
const {
  createBuildStateManager,
  isPathInside,
  IGNORED_BUILD_DIRECTORIES
} = require('./js/main/build-state')
const {
  registerTestRunnerIpc
} = require('./js/main/test-runner-ipc')
const {
  registerCangjieFormatFolderIpc
} = require('./js/main/cangjie-format-folder-ipc')
const {
  registerCjpmDependencyIpc
} = require('./js/main/cjpm-dependency-ipc')
const {
  registerTodoScanIpc
} = require('./js/main/todo-scan-ipc')
const {
  registerRunConfigIpc
} = require('./js/main/run-config-ipc')
const {
  registerProjectPickerIpc
} = require('./js/main/project-picker')
const {
  registerCangjieDebugIpc
} = require('./js/main/cangjie-debug')
const {
  registerCangjieHeapIpc
} = require('./js/main/cangjie-heap')
const {
  registerEditorFileIpc
} = require('./js/main/editor-file-io')
const {
  registerTreeOperationsIpc
} = require('./js/main/tree-operations')
const {
  createGitOperations
} = require('./js/main/git-operations')
const {
  createServiceRun
} = require('./js/main/service-run')
const {
  createOpenSSLDownloader
} = require('./js/main/openssl-downloader')
const {
  createProjectCreation
} = require('./js/main/project-creation')

const settingsStoreOptions = process.env.CIDE_E2E_USER_DATA
  ? {
    settingsDir: path.join(app.getPath('userData'), '.cang-ide')
  }
  : {}
// 密钥静态加密:只要 safeStorage 存在就用 createSecretCrypto(它在每次读写时惰性
// 检测可用性,避开“模块加载期 app 未 ready 导致误判不可用”),加密不可用时自动退回
// 明文读写(可用性优先,绝不因加密失败而丢 Key);safeStorage 完全缺失才用直通。
settingsStoreOptions.secretCrypto = safeStorage
  ? createSecretCrypto(safeStorage)
  : createPassthroughCrypto()
const { loadSettings, saveSettings } = createSettingsStore(
  settingsStoreOptions
)
// 文件日志(userData/logs):下载器等模块统一写文件,不向 CMD 控制台打印
const logger = createAppLogger({
  getDirectory: () => path.join(app.getPath('userData'), 'logs')
})
captureProcessErrors(logger)
// OpenSSL 运行时与下载安装器:内置出厂兜底;用户可在设置里下载官方二进制版本
const opensslRuntime = require('./js/main/openssl-runtime')
const opensslDownloader = createOpenSSLDownloader({
  app,
  logger,
  safeSend,
  getWindows: () => windows,
  opensslRuntime
})
opensslDownloader.restoreOnStartup().catch(() => { /* OpenSSL 启动恢复失败在使用时会再次尝试 */ })

// STDX 自动下载安装器:从 GitCode releases 拉预编译二进制 zip,
// 完整 stdx 目录(含 .cjo/.dll)安装到用户选择的位置
const { createStdxDownloader } = require('./js/main/stdx-downloader')
const stdxDownloader = createStdxDownloader({ app, logger, safeSend })
const cangjieSdkManager = createCangjieSdkManager({ loadSettings })
const {
  getFreshPathAsync: _getFreshPathAsync,
  getFreshPath: _getFreshPath,
  setEnvPath: _setEnvPath,
  resolveSdk: _resolveCangjieSdk,
  getCjHome: _getCjHome
} = cangjieSdkManager
const buildStateManager = createBuildStateManager()
const {
  getWindowBuildStates,
  getCangjieBuildState,
  clearCangjieBuildStates,
  clearAllBuildStates,
  markCangjieBuildInputsChanged,
  markBuildStateChanged,
  getCangjieBuildMode,
  completeCangjieBuild,
  normalizeBuildPath,
  isIgnoredBuildInput
} = buildStateManager
const processLifecycle = createProcessLifecycle({
  onEvent: event => logger.info('process-lifecycle', event)
})
const projectTreeScanner = createProjectTreeScanner()
const projectSearchService = createProjectSearchService()
const cangjieWorkspaceIndexer = createCangjieWorkspaceIndexer()
function getDefaultNotesDirectory() {
  try {
    return path.join(app.getPath('documents'), 'CIDE Notes')
  } catch (error) {
    return path.join(app.getPath('userData'), 'CIDE Notes')
  }
}
const notesService = createNotesService({
  loadSettings,
  defaultDirectory: getDefaultNotesDirectory()
})
const recoveryStore = createRecoveryStore({
  directory: path.join(app.getPath('userData'), 'recovery')
})
const workspaceSessionStore = createWorkspaceSessionStore({
  sessionFile: path.join(
    app.getPath('userData'),
    'workspace-session.json'
  )
})
recoveryStore.cleanup()
registerPlatformIpc(ipcMain, {
  userDataRoot: app.getPath('userData'),
  sdkRootProvider: () => cangjieSdkManager.getCjHome()
})
// preload 运行在沙箱中,require('electron') 只提供 contextBridge/ipcRenderer,
// 拿不到 app 模块。版本号经同步通道提供——若在 preload 里直接调
// app.getVersion() 会抛 TypeError 并导致整个 contextBridge 桥接挂载失败。
ipcMain.on('cide-app-version-sync', event => {
  event.returnValue = app.getVersion()
})
registerAiProxyIpc(ipcMain, { logger, net })
registerRecoveryIpc(ipcMain, recoveryStore, logger)
registerWorkspaceSessionIpc(ipcMain, workspaceSessionStore)
registerProjectSearchIpc(ipcMain, projectSearchService, logger)
registerNotesIpc(ipcMain, notesService, {
  showItemInFolder: targetPath => shell.showItemInFolder(targetPath)
})
registerCangjieWorkspaceIndexIpc(
  ipcMain,
  cangjieWorkspaceIndexer,
  logger
)
registerCangjieThrowsScannerIpc(ipcMain, cangjieSdkManager)
const updateService = createUpdateService({
  app,
  electronNet: net,
  shell,
  loadSettings,
  logger,
  releaseChannel: appMetadata.releaseChannel,
  defaultUpdateUrl: appMetadata.defaultUpdateUrl,
  manifestPublicKey: appMetadata.updatePublicKey,
  requireManifestSignature: false,
  downloadDirectory: path.join(
    app.getPath('userData'),
    'updates'
  )
})
registerUpdateIpc(ipcMain, updateService, {
  dialog,
  BrowserWindow,
  app
})

// —— 自动检查更新:启动后 10 秒检查,之后每 1 小时检查一次 ——
const UPDATE_CHECK_STARTUP_DELAY_MS = 10 * 1000
const UPDATE_CHECK_INTERVAL_MS = 60 * 60 * 1000
let lastNotifiedVersion = ''

async function autoCheckForUpdates() {
  try {
    const result = await updateService.check()
    if (result.success && result.available) {
      const newVersion = result.update.version
      // 同一版本只通知一次
      if (newVersion !== lastNotifiedVersion) {
        lastNotifiedVersion = newVersion
        const win = getFocusedWindow()
        if (win) {
          safeSend(win.webContents, 'update-available', {
            version: newVersion,
            notes: result.update.notes || '',
            isPortable: result.isPortable
          })
        }
      }
    }
  } catch (error) {
    logger.info('auto-update-check-skipped', {
      error: error.message
    })
  }
}

setTimeout(() => {
  void autoCheckForUpdates()
}, UPDATE_CHECK_STARTUP_DELAY_MS)
setInterval(() => {
  void autoCheckForUpdates()
}, UPDATE_CHECK_INTERVAL_MS)
logger.info('application-starting', {
  version: app.getVersion(),
  packaged: app.isPackaged
})

ipcMain.on('app-log', (event, payload = {}) => {
  const allowedLevels = new Set([
    'debug',
    'info',
    'warn',
    'error'
  ])
  const level = allowedLevels.has(payload.level)
    ? payload.level
    : 'info'
  logger[level](
    `renderer:${String(payload.message || '').slice(0, 1000)}`,
    {
      webContentsId: event.sender.id,
      context: payload.context
    }
  )
})

ipcMain.handle('app-open-logs', async () => {
  const error = await shell.openPath(logger.getDirectory())
  return {
    success: !error,
    error: error || '',
    path: logger.getDirectory()
  }
})

// 在系统默认浏览器打开外部链接。渲染进程的 <a target=_blank>/window.open
// 已被 setWindowOpenHandler 无条件 deny,外链只能经此白名单通道交给 shell。
ipcMain.handle('open-external-url', async (_event, payload) => {
  const raw = String(payload?.url || '')
  let parsed
  try {
    parsed = new URL(raw)
  } catch (_error) {
    return { success: false, error: 'Invalid URL.' }
  }
  if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
    return { success: false, error: 'Only http/https URLs can be opened.' }
  }
  try {
    await shell.openExternal(parsed.toString())
    return { success: true, error: '' }
  } catch (error) {
    logger?.warn?.('open-external-url-failed', {
      webContentsId: _event?.sender?.id,
      message: error?.message || String(error)
    })
    return { success: false, error: error?.message || String(error) }
  }
})

// ============== Git 操作(模块化) ==============
const gitOps = createGitOperations({
  path,
  fs,
  spawn,
  loadSettings,
  resolveGitInstallation,
  parseGitStatus,
  getGitConflictLabel,
  getSafeGitRemoteLabel,
  splitGitRemoteBranch,
  isPathInside
})
const {
  getGitRepositoryRoot,
  getGitState,
  runGit,
  validateGitRelativePath,
  createGitPullConflictResult,
  ensureExpectedGitBranch,
  getCurrentGitBranch,
  getGitCommitDetail,
  getGitCompareFile,
  getGitCompareFiles,
  getGitCompareRefs,
  getGitDiff,
  getGitIncomingPaths,
  getGitLog,
  getGitLogAuthors,
  getGitOperationContext,
  getGitPushContext,
  validateGitRemoteTarget
} = gitOps

// 用 Map 跟踪所有窗口,key 为 webContents.id
const windows = new Map()
const projectWatchers = new Map()
const startupControllers = new Map()
const approvedWindowCloses = new WeakSet()
const pendingWindowCloses = new WeakMap()
const windowStates = new Map() // 保存 Welcome 之前的窗口状态 { isMaximized, bounds }
let nextWindowCloseRequestId = 0
const ignoredBuildDirectories = IGNORED_BUILD_DIRECTORIES

// 渲染进程崩溃自动重载(带时间窗口内的次数上限,防止崩溃循环)
const rendererCrashGuard = createRendererCrashGuard()

// ============== 服务运行(模块化) ==============
const serviceRun = createServiceRun({
  path,
  fs,
  spawn,
  spawnSync,
  EventEmitter,
  classifyBuildOutputLine,
  isCompileFailureEvidence,
  isCompilerDecorationLine,
  normalizePtyOutput,
  parseRunStatusLine,
  shouldEmitRuntimeOutputLine,
  OUTPUT_ROUTE,
  translateCjCompilerLine,
  translateExpectedFoundLine,
  translateCjRuntimeLine,
  formatZhHint,
  resolveCangjieExecutable,
  resolveServiceWorkDir,
  shouldUsePtyForServiceRun,
  safeSend,
  logger,
  processLifecycle,
  getWindows: () => windows,
  getProjectWatchers: () => projectWatchers,
  getFreshPathAsync: _getFreshPathAsync,
  setEnvPath: _setEnvPath,
  getCjHome: _getCjHome,
  getCangjieBuildState,
  markCangjieBuildInputsChanged,
  getCangjieBuildMode,
  completeCangjieBuild,
  prependOpenSSLToPath: (env) => require('./js/main/openssl-runtime').prependOpenSSLToPath(env)
})

// ============== 项目创建(模块化) ==============
const projectCreation = createProjectCreation({
  path,
  fs,
  spawn,
  app,
  loadSettings,
  saveSettings,
  enumerateSdkVersions,
  resolveCangjieExecutable,
  resolveGitInstallation,
  resolveCangjieSdk,
  safeSend,
  setEnvPath: _setEnvPath,
  getFreshPath: _getFreshPath,
  getCjHome: _getCjHome
})

function handleLspWorkspaceEditApplied(
  webContentsId,
  projectRoot,
  result,
  sendResult = false
) {
  const changedPaths = new Set()
  let hasResourceOperations = false
  for (const operation of Array.isArray(result?.fileOperations)
    ? result.fileOperations
    : []) {
    if (operation.filePath) changedPaths.add(operation.filePath)
    if (operation.oldPath) changedPaths.add(operation.oldPath)
    if (operation.newPath) changedPaths.add(operation.newPath)
    if (['create', 'rename', 'delete'].includes(operation.kind)) {
      hasResourceOperations = true
    }
  }
  for (const document of Array.isArray(result?.documents)
    ? result.documents
    : []) {
    changedPaths.add(document.filePath)
  }

  const win = windows.get(webContentsId)
  if (!win) return
  if (sendResult) {
    safeSend(
      win.webContents,
      'lsp-workspace-edit-applied',
      result
    )
  }
  const paths = Array.from(changedPaths)
  if (paths.length > 0) {
    markCangjieBuildInputsChanged(webContentsId, paths)
    safeSend(win.webContents, 'project-files-changed', {
      projectPath: projectRoot,
      changedPaths: paths
    })
  }
  if (hasResourceOperations) {
    safeSend(win.webContents, 'project-tree-changed', {
      projectPath: projectRoot
    })
  }
}

const cangjieLspManager = new CangjieLspManager({
  processLifecycle,
  logger,
  workspaceEditHandler: payload => {
    const result = applyLspWorkspaceEdit(payload)
    handleLspWorkspaceEditApplied(
      payload.webContentsId,
      payload.projectRoot,
      result,
      true
    )
    return result
  }
})
// 构建完成后需要让 .cjo 清单重扫 target(发现新编译出的三方依赖),
// 而 .cjo IPC 在 LSP IPC 之后注册,这里用前向引用承接其 invalidate。
let cangjieCjoIpcApi = null
const cangjieLspIpcApi = registerCangjieLspIpc(ipcMain, {
  manager: cangjieLspManager,
  getSdkRoot: () => _getCjHome(),
  getStdxRoot: () => String(loadSettings().stdxLibPath || '').trim(),
  getAutoStdxDir: () => stdxDownloader.getLatestInstalledDir(),
  findProjectRoot: projectCreation.findCangjieProjectDir,
  onWorkspaceEditApplied: (
    webContentsId,
    projectRoot,
    result
  ) => {
    handleLspWorkspaceEditApplied(
      webContentsId,
      projectRoot,
      result
    )
  },
  onBuildComplete: () => {
    cangjieCjoIpcApi?.invalidate()
    // import 路径补全清单同样缓存了 target 依赖包,需要一并重扫
    cangjieLspIpcApi?.invalidateSdkPackages()
  },
  logger
})
cangjieCjoIpcApi = registerCangjieCjoIpc(ipcMain, {
  getSdkRoot: () => _getCjHome(),
  getStdxRoot: () => String(loadSettings().stdxLibPath || '').trim(),
  getAutoStdxDir: () => stdxDownloader.getLatestInstalledDir(),
  getUserDataDir: () => app.getPath('userData'),
  logger
})
registerCangjieDocIpc(ipcMain, {
  net,
  logger,
  getSdkRoot: () => _getCjHome()
})
registerStdxDocIpc(ipcMain, {
  net,
  logger,
  cacheDir: path.join(app.getPath('userData'), 'stdx-docs')
})
registerCourseContentIpc(ipcMain, {
  net,
  logger,
  getSnapshotDir: () => app.isPackaged
    ? path.join(process.resourcesPath, 'course')
    : path.join(__dirname, 'cangjie-course'),
  cacheDir: path.join(app.getPath('userData'), 'cangjie-course')
})
registerContactQrcodeIpc(ipcMain, {
  net,
  logger,
  getBundledFile: () => app.isPackaged
    ? path.join(process.resourcesPath, 'qrcode', 'wangpeng_wx.jpg')
    : path.join(__dirname, 'qrcode', 'wangpeng_wx.jpg'),
  cacheDir: path.join(app.getPath('userData'), 'contact-qrcode')
})
const cangjieDebugManager = new CangjieDebugManager({
  getSdkRoot: () => _getCjHome(),
  getBasePath: () => _getFreshPath(),
  processLifecycle,
  send: (webContentsId, payload) => {
    const win = windows.get(webContentsId)
    if (win) safeSend(win.webContents, 'cangjie-debug-event', payload)
  }
})

// 辅助函数:安全地向 webContents 发送消息
function safeSend(webContents, channel, ...args) {
  try {
    if (webContents && !webContents.isDestroyed()) {
      webContents.send(channel, ...args)
    }
  } catch (e) {
    // 忽略已销毁的 webContents
  }
}

// ── 启动引导:首次打开仓颉项目时通知渲染端 ──
const guideFlagPath = () => path.join(app.getPath('userData'), 'guide-shown.flag')

function checkAndNotifyGuide(webContents) {
  try {
    if (fs.existsSync(guideFlagPath())) return
    safeSend(webContents, 'guide-check', { shouldShow: true })
  } catch {
    // 读取失败不阻塞
  }
}

ipcMain.handle('guide-mark-shown', () => {
  try {
    fs.writeFileSync(guideFlagPath(), '1', 'utf8')
  } catch {
    // 写入失败不阻塞
  }
  return { ok: true }
})

function hashCjpmContent(filePath) {
  const content = fs.readFileSync(filePath)
  return crypto.createHash('sha256').update(content).digest('hex')
}

function stopProjectWatcher(webContentsId) {
  const state = projectWatchers.get(webContentsId)
  if (!state) return

  if (state.refreshTimer) {
    clearTimeout(state.refreshTimer)
  }
  if (state.gitRefreshTimer) {
    clearTimeout(state.gitRefreshTimer)
  }
  if (state.lspTimer) {
    clearTimeout(state.lspTimer)
  }
  try {
    state.watcher.close()
  } catch (error) {
    logger.warn('Failed to close project watcher', { webContentsId, error: error.message })
  }
  projectWatchers.delete(webContentsId)
}

function startProjectWatcher(webContents, projectPath) {
  if (!webContents || webContents.isDestroyed()) return

  const webContentsId = webContents.id
  stopProjectWatcher(webContentsId)

  let resolvedPath
  try {
    resolvedPath = path.resolve(projectPath)
    if (!fs.statSync(resolvedPath).isDirectory()) return
  } catch (error) {
    return
  }

  // cjpm.toml 内容哈希(normalizeBuildPath -> sha256):重启前比对,
  // watcher 误触发但内容没变时不杀 LSPServer
  const cjpmHashes = new Map()
  try {
    const rootCjpm = path.join(resolvedPath, 'cjpm.toml')
    if (fs.existsSync(rootCjpm)) {
      cjpmHashes.set(
        normalizeBuildPath(rootCjpm),
        hashCjpmContent(rootCjpm)
      )
    }
  } catch (error) { /* 初始哈希失败不阻塞监听 */ }

  const state = {
    watcher: null,
    refreshTimer: null,
    gitRefreshTimer: null,
    lspTimer: null,
    changedPaths: new Set(),
    lspChanges: new Map(),
    cjpmHashes,
    projectPath: resolvedPath
  }

  const handleChange = (eventType, filename) => {
    const changedPath = filename ? String(filename).replace(/\\/g, '/') : ''
    const resolvedChangedPath = changedPath
      ? path.resolve(state.projectPath, changedPath)
      : ''
    if (changedPath) {
      markCangjieBuildInputsChanged(
        webContentsId,
        [resolvedChangedPath]
      )
    } else {
      const buildStates = getWindowBuildStates(webContentsId)
      if (buildStates) {
        for (const buildState of buildStates.values()) {
          markBuildStateChanged(
            buildState,
            [buildState.workDir]
          )
        }
      }
    }

    const isGitMetadata = changedPath
      .split('/')
      .some(segment => segment === '.git')
    if (!isGitMetadata) {
      state.changedPaths.add(changedPath)
      if (state.gitRefreshTimer) clearTimeout(state.gitRefreshTimer)
      state.gitRefreshTimer = setTimeout(() => {
        state.gitRefreshTimer = null
        const changedPaths = Array.from(state.changedPaths)
        state.changedPaths.clear()
        safeSend(webContents, 'project-files-changed', {
          projectPath: state.projectPath,
          changedPaths
        })
      }, 100)
    }

    // 内容修改不影响项目结构,只处理新增、删除和重命名。
    const pathSegments = changedPath.split('/').filter(Boolean)
    const isIgnoredLspPath = pathSegments.some(segment => {
      return (
        segment === '.git' ||
        ignoredBuildDirectories.has(segment.toLowerCase())
      )
    })
    const fileName = path.basename(changedPath).toLowerCase()
    const isCangjieWorkspaceInput = (
      /\.cj$/i.test(changedPath) ||
      fileName === 'cjpm.toml' ||
      fileName === 'cjpm.lock'
    )
    if (
      resolvedChangedPath &&
      !isIgnoredLspPath &&
      isCangjieWorkspaceInput
    ) {
      const type = eventType === 'change'
        ? LSP_FILE_CHANGE_TYPE.CHANGED
        : (
          fs.existsSync(resolvedChangedPath)
            ? LSP_FILE_CHANGE_TYPE.CREATED
            : LSP_FILE_CHANGE_TYPE.DELETED
        )
      state.lspChanges.set(
        normalizeBuildPath(resolvedChangedPath),
        { filePath: resolvedChangedPath, type }
      )
      if (state.lspTimer) clearTimeout(state.lspTimer)
      state.lspTimer = setTimeout(() => {
        state.lspTimer = null
        const changes = Array.from(state.lspChanges.values())
        state.lspChanges.clear()

        // cjpm.toml 是否真要重启:读文件做内容哈希比对。watcher 在
        // Windows 上会因访问时间/编辑器临时文件等误触发 change,
        // 内容没变就不重启(也不通知服务器)
        let restart = false
        let effectiveChanges = changes
        for (const change of changes) {
          if (
            path.basename(change.filePath).toLowerCase() !== 'cjpm.toml'
          ) {
            continue
          }
          const hashKey = normalizeBuildPath(change.filePath)
          if (change.type === LSP_FILE_CHANGE_TYPE.CHANGED) {
            let nextHash = ''
            try {
              nextHash = hashCjpmContent(change.filePath)
            } catch (error) {
              nextHash = ''
            }
            const previousHash = state.cjpmHashes.get(hashKey) || ''
            if (nextHash && nextHash === previousHash) {
              // 内容未变:剔除该事件,不重启
              effectiveChanges = effectiveChanges.filter(
                item => item !== change
              )
              continue
            }
            if (nextHash) state.cjpmHashes.set(hashKey, nextHash)
            restart = true
          } else {
            // 新建/删除 cjpm.toml:更新哈希并重启。
            // 注意:编辑器保存是"临时文件+原子替换",watcher 会报 add(CREATED)
            // 而非 change,因此这里同样要做内容哈希比对——内容没变就不重启、
            // 不弹同步提示条。
            const previousHash = state.cjpmHashes.get(hashKey) || ''
            state.cjpmHashes.delete(hashKey)
            if (change.type === LSP_FILE_CHANGE_TYPE.CREATED) {
              let nextHash = ''
              try {
                nextHash = hashCjpmContent(change.filePath)
                state.cjpmHashes.set(hashKey, nextHash)
              } catch (error) { /* 文件刚建又不可读时忽略 */ }
              if (nextHash && nextHash === previousHash) {
                effectiveChanges = effectiveChanges.filter(
                  item => item !== change
                )
                continue
              }
            }
            restart = true
          }
        }

        // cjpm.toml 内容哈希在上方循环中已确认变化,或 cjpm.lock 变化
        // 导致依赖状态可能过期。通知渲染进程显示同步提示条。
        const hasWorkspaceConfigChange = effectiveChanges.some(change => {
          const name = path.basename(change.filePath).toLowerCase()
          return name === 'cjpm.toml' || name === 'cjpm.lock'
        })
        if (restart || hasWorkspaceConfigChange) {
          const hashKey = normalizeBuildPath(
            path.join(state.projectPath, 'cjpm.toml')
          )
          const latestHash = state.cjpmHashes.get(hashKey) || ''
          safeSend(webContents, 'cjpm-toml-changed', {
            projectPath: state.projectPath,
            contentHash: latestHash
          })
        }

        if (!restart && effectiveChanges.length === 0) return
        cangjieLspManager.notifyWorkspaceFilesChanged(
          webContentsId,
          state.projectPath,
          effectiveChanges,
          { restart }
        ).catch(error => {
          logger.warn('cangjie-lsp-workspace-change-failed', {
            webContentsId,
            message: error?.message || String(error)
          })
        })
      }, 120)
    }

    if (eventType !== 'rename') return

    if (pathSegments.some(segment => {
      return segment.startsWith('.') || segment === 'node_modules'
    })) {
      return
    }

    if (state.refreshTimer) clearTimeout(state.refreshTimer)
    state.refreshTimer = setTimeout(() => {
      state.refreshTimer = null
      safeSend(webContents, 'project-tree-changed', {
        projectPath: state.projectPath
      })
    }, 500)
  }

  try {
    state.watcher = fs.watch(
      resolvedPath,
      { recursive: process.platform === 'win32' || process.platform === 'darwin' },
      handleChange
    )
  } catch (error) {
    try {
      state.watcher = fs.watch(resolvedPath, handleChange)
    } catch (fallbackError) {
      return
    }
  }

  state.watcher.on('error', () => {
    stopProjectWatcher(webContentsId)
  })
  projectWatchers.set(webContentsId, state)
}

// 辅助函数:从 event.sender 获取对应的 BrowserWindow
function getWindowFromEvent(event) {
  return BrowserWindow.fromWebContents(event.sender)
}

function createSplashWindow(appIcon) {
  const splash = new BrowserWindow({
    width: 760,
    height: 460,
    useContentSize: true,
    frame: false,
    transparent: true,
    backgroundColor: '#00000000',
    show: false,
    resizable: false,
    maximizable: false,
    minimizable: false,
    fullscreenable: false,
    alwaysOnTop: true,
    skipTaskbar: true,
    hasShadow: true,
    webPreferences: {
      // 与主窗口一致沿用安全基线(沙箱开启):splash 仅加载本地可信 splash.html,
      // 其 preload.js 只用 contextBridge/ipcRenderer(沙箱兼容,主窗口已实证),
      // 无需此前对启动窗关闭沙箱的降级——那层沙箱隔离本是可省的纵深防御。
      ...createSecureWebPreferences(__dirname),
      devTools: false
    },
    icon: appIcon.isEmpty() ? APP_ICON_PATH : appIcon
  })
  if (!appIcon.isEmpty()) splash.setIcon(appIcon)
  splash.setMenuBarVisibility(false)
  splash.center()
  attachWindowSecurity(splash)
  splash.once('ready-to-show', () => {
    if (!splash.isDestroyed()) splash.show()
  })
  splash.loadFile('splash.html').catch(() => {
    if (!splash.isDestroyed()) splash.destroy()
  })
  return splash
}

function createStartupController(win, splash) {
  const startedAt = Date.now()
  const minimumVisibleMs = 1100
  const transitionMs = 180
  let completed = false
  let disposed = false
  let readyTimer = null
  let closeTimer = null

  const update = payload => {
    if (!splash.isDestroyed()) {
      safeSend(splash.webContents, 'startup-status', payload)
    }
  }

  const showWorkbench = () => {
    startupControllers.delete(win.webContents.id)
    if (!win.isDestroyed()) {
      win.show()
      win.focus()
    }
    if (!splash.isDestroyed()) splash.destroy()
  }

  const complete = (fallback = false) => {
    if (completed || disposed) return
    completed = true
    clearTimeout(controller.fallbackTimer)
    update({
      phase: 'editor',
      progress: 100,
      label: fallback ? '正在打开 CIDE' : 'CIDE 就绪',
      ready: true
    })
    const remaining = Math.max(
      0,
      minimumVisibleMs - (Date.now() - startedAt)
    )
    readyTimer = setTimeout(() => {
      update({
        phase: 'editor',
        progress: 100,
        label: fallback ? 'Opening CIDE' : 'CIDE Ready',
        ready: true,
        closing: true
      })
      closeTimer = setTimeout(showWorkbench, transitionMs)
    }, remaining)
  }

  const dispose = () => {
    if (disposed) return
    disposed = true
    clearTimeout(controller.fallbackTimer)
    clearTimeout(readyTimer)
    clearTimeout(closeTimer)
    startupControllers.delete(win.webContents.id)
    if (!splash.isDestroyed()) splash.destroy()
  }

  const controller = {
    update,
    complete,
    dispose,
    fallbackTimer: null
  }
  controller.fallbackTimer = setTimeout(() => complete(true), 15000)
  controller.fallbackTimer.unref?.()

  splash.webContents.once('did-finish-load', () => {
    update({
      phase: 'core',
      progress: 20,
      label: '正在启动核心服务'
    })
  })
  splash.once('closed', () => {
    if (completed || disposed) return
    completed = true
    clearTimeout(controller.fallbackTimer)
    showWorkbench()
  })
  return controller
}

function createWindow(projectPath, options = {}) {
  const activeProjectPath =
    workspaceSessionStore.normalizeProjectPath(projectPath)
  const hasProject = Boolean(activeProjectPath)
  const appIcon = nativeImage.createFromPath(APP_ICON_PATH)
  const showStartupSplash =
    options.showStartupSplash === true &&
    process.env.CIDE_DISABLE_SPLASH !== '1'
  const splash = showStartupSplash
    ? createSplashWindow(appIcon)
    : null
  const win = new BrowserWindow({
    width: hasProject ? 1200 : 960,
    height: hasProject ? 800 : 650,
    minWidth: 800,
    minHeight: 450,
    titleBarStyle: 'hidden',
    frame: false,
    show: !showStartupSplash,
    webPreferences: {
      // 打包版本对用户彻底关闭 DevTools(含 F12 快捷键入口);
      // 开发模式(npm start 未打包)保持可用。
      ...createSecureWebPreferences(__dirname),
      devTools: !app.isPackaged
    },
    icon: appIcon.isEmpty() ? APP_ICON_PATH : appIcon
  })
  if (!appIcon.isEmpty()) win.setIcon(appIcon)
  applyWindowsTaskbarBranding(win)
  attachWindowSecurity(win)

  const wcId = win.webContents.id
  windows.set(wcId, win)
  if (activeProjectPath) {
    registerProjectRoot(wcId, activeProjectPath)
    // 启动时自动打开的工程(命令行参数/上次会话)也记入最近项目
    workspaceSessionStore.recordRecentProject(activeProjectPath)
  }
  const startupController = splash
    ? createStartupController(win, splash)
    : null
  if (startupController) {
    startupControllers.set(wcId, startupController)
  }
  const sendWindowState = () => {
    safeSend(win.webContents, 'window-maximized-changed', {
      maximized: win.isMaximized()
    })
  }

  // Enable F12 / Ctrl+Shift+I to open DevTools —— 仅限开发模式,
  // 打包版本(app.isPackaged)不注册该快捷键,用户无法打开 DevTools。
  if (!app.isPackaged) {
    win.webContents.on('before-input-event', (event, input) => {
      if (input.key === 'F12' || (input.control && input.shift && input.key === 'I')) {
        win.webContents.toggleDevTools()
      }
    })
  }

  win.on('close', event => {
    if (approvedWindowCloses.has(win)) {
      approvedWindowCloses.delete(win)
      pendingWindowCloses.delete(win)
      return
    }
    if (win.webContents.isLoadingMainFrame()) return

    event.preventDefault()
    if (pendingWindowCloses.has(win)) return
    const requestId = ++nextWindowCloseRequestId
    pendingWindowCloses.set(win, requestId)
    safeSend(win.webContents, 'window-close-requested', { requestId })
  })

  win.on('closed', () => {
    startupController?.dispose()
    pendingWindowCloses.delete(win)
    stopProjectWatcher(wcId)
    rendererCrashGuard.cleanup(wcId)
    projectTreeScanner.cancelOwner(wcId, 'window-closed')
    projectSearchService.cancelOwner(wcId, 'window-closed')
    cangjieWorkspaceIndexer.cancelOwner(wcId, 'window-closed')
    clearCangjieBuildStates(wcId)
    clearWebContentsRoots(wcId)
    windows.delete(wcId)
    cangjieLspManager.stopForWebContents(wcId)
    cangjieDebugManager.stopForWebContents(wcId).catch(() => { })
    // 清理该窗口关联的服务进程
    serviceRun.cleanupWindowServices(wcId)
    processLifecycle.stopOwner(wcId, 'window-closed')
    // 更新菜单引用
    updateMenuForFocusedWindow()
  })

  win.on('focus', () => {
    updateMenuForFocusedWindow()
  })
  win.on('maximize', sendWindowState)
  win.on('unmaximize', sendWindowState)
  win.webContents.on('dom-ready', () => {
    startupController?.update({
      phase: 'workspace',
      progress: 48,
      label: '正在加载工作区'
    })
  })
  win.webContents.on('did-finish-load', () => {
    sendWindowState()
    startupController?.update({
      phase: 'editor',
      progress: 76,
      label: '正在初始化编辑器'
    })
    // 如果传入了项目路径,在页面加载完成后发送
    if (activeProjectPath) {
      startProjectWatcher(win.webContents, activeProjectPath)
      safeSend(
        win.webContents,
        'project-opened',
        activeProjectPath
      )
      // 首次打开仓颉项目时弹出引导
      checkAndNotifyGuide(win.webContents)
    }
  })
  win.webContents.on(
    'did-fail-load',
    (_event, _errorCode, _description, _url, isMainFrame) => {
      if (isMainFrame) startupController?.complete(true)
    }
  )
  // 渲染进程崩溃/OOM:自动重载并经 recovery 恢复;短时间反复崩溃则放弃重载
  rendererCrashGuard.attach(win, { logger, dialog })

  // 有项目直接进工作台;无项目直接进 Welcome 页(避免先加载整个工作台再跳转)。
  // 例外:e2e 自动化(CIDE_E2E_USER_DATA)始终加载工作台壳——用例依赖无项目时
  // 也能直接操作 #menu-bar 并经 open-project-current-window 在壳内打开项目。
  const showWelcomePage = !hasProject && !process.env.CIDE_E2E_USER_DATA
  win.loadFile(showWelcomePage ? 'welcome.html' : 'index.html')

  // 设置菜单(基于当前聚焦窗口)
  updateMenuForFocusedWindow()

  return win
}

ipcMain.on('renderer-ready', event => {
  startupControllers.get(event.sender.id)?.complete()
})

// Renderer主动查询Python状态(避免推送时序问题)
ipcMain.handle('get-python-status', async () => {
  // 确保检查已完成
  return await projectCreation.checkPythonForDebugger()
})

function getFocusedWindow() {
  return BrowserWindow.getFocusedWindow() || Array.from(windows.values())[windows.size - 1] || null
}

function updateMenuForFocusedWindow() {
  const focusedWin = getFocusedWindow()

  const menu = Menu.buildFromTemplate([
    {
      label: '文件',
      submenu: [
        {
          label: '新建项目', click: () => {
            const win = getFocusedWindow()
            if (win) safeSend(win.webContents, 'menu-action', 'new-project')
          }
        },
        { label: '打开项目', click: () => openProject() },
        {
          label: '关闭项目', click: () => {
            const win = getFocusedWindow()
            if (win) safeSend(win.webContents, 'menu-action', 'close-project')
          }
        },
        { type: 'separator' },
        {
          label: '设置', click: () => {
            const win = getFocusedWindow()
            if (win) safeSend(win.webContents, 'menu-action', 'settings')
          }
        },
        { type: 'separator' },
        {
          label: '退出', click: () => {
            const win = getFocusedWindow()
            if (win) {
              safeSend(win.webContents, 'menu-action', 'quit')
            } else {
              app.quit()
            }
          }
        }
      ]
    },
    {
      label: '编辑',
      submenu: [
        { label: '撤销', accelerator: 'CmdOrCtrl+Z' },
        { label: '重做', accelerator: 'CmdOrCtrl+Shift+Z' },
        { type: 'separator' },
        { label: '剪切', accelerator: 'CmdOrCtrl+X' },
        { label: '复制', accelerator: 'CmdOrCtrl+C' },
        { label: '粘贴', accelerator: 'CmdOrCtrl+V' }
      ]
    },
    {
      label: '运行',
      submenu: [
        {
          label: '运行', click: () => {
            const win = getFocusedWindow()
            if (win) safeSend(win.webContents, 'menu-action', 'run')
          }, accelerator: 'CmdOrCtrl+R'
        },
        {
          label: '调试', click: () => {
            const win = getFocusedWindow()
            if (win) safeSend(win.webContents, 'menu-action', 'debug')
          }
        }
      ]
    },
    {
      label: '查看',
      submenu: [
        {
          label: '切换项目面板', click: () => {
            const win = getFocusedWindow()
            if (win) safeSend(win.webContents, 'menu-action', 'toggle-project')
          }
        },
        {
          label: '切换终端', click: () => {
            const win = getFocusedWindow()
            if (win) safeSend(win.webContents, 'menu-action', 'toggle-terminal')
          }
        },
        {
          label: '切换侧边栏', click: () => {
            const win = getFocusedWindow()
            if (win) safeSend(win.webContents, 'menu-action', 'toggle-sidebar')
          }
        }
      ]
    },
    {
      label: '帮助',
      submenu: [
        {
          label: '检查更新',
          click: () => {
            const win = getFocusedWindow()
            if (win) safeSend(win.webContents, 'menu-action', 'check-updates')
          }
        },
        { type: 'separator' },
        {
          label: '打开日志文件夹',
          click: () => {
            try {
              const openError = shell.openPath(logger.getDirectory())
              if (openError) {
                logger.warn('open-logs-folder-failed', { error: openError })
              }
            } catch (error) {
              logger.error('open-logs-folder-failed', {
                error: error.message,
                stack: error.stack
              })
            }
          }
        },
        { type: 'separator' },
        { label: '关于', click: () => showAbout() }
      ]
    }
  ])
  Menu.setApplicationMenu(menu)
}

function openProject() {
  const focusedWin = getFocusedWindow()
  if (focusedWin) {
    safeSend(focusedWin.webContents, 'show-project-picker')
  }
}

// 获取示例项目列表
ipcMain.handle('get-example-projects', async () => {
  try {
    const examplesDir = path.join(app.getAppPath(), 'examples')
    if (!fs.existsSync(examplesDir)) {
      return []
    }

    const entries = fs.readdirSync(examplesDir, { withFileTypes: true })
    const examples = []

    for (const entry of entries) {
      if (!entry.isDirectory()) continue

      const examplePath = path.join(examplesDir, entry.name)
      const cjpmTomlPath = path.join(examplePath, 'cjpm.toml')

      // 检查是否是有效的仓颉项目(有 cjpm.toml)
      if (!fs.existsSync(cjpmTomlPath)) continue

      // 读取 cjpm.toml 获取项目名称和描述
      let name = entry.name
      let description = ''
      try {
        const content = fs.readFileSync(cjpmTomlPath, 'utf8')
        // 简单解析:获取 name 和 description 字段
        const nameMatch = content.match(/name\s*=\s*"([^"]+)"/)
        const descMatch = content.match(/description\s*=\s*"([^"]*)"/)
        if (nameMatch) name = nameMatch[1]
        if (descMatch) description = descMatch[1]
      } catch {
        // 读取失败,使用默认值
      }

      examples.push({
        path: examplePath,
        name: name,
        description: description
      })
    }

    return examples
  } catch (error) {
    logger.error('Failed to load example projects', { error: error.message })
    return []
  }
})

// 通过路径打开项目的共用实现(用于示例项目 / Welcome 页面 / index.html?project= 跳转)
function openProjectPathInWebContents(sender, projectPath) {
  const activeProjectPath = workspaceSessionStore.normalizeProjectPath(projectPath)
  if (!activeProjectPath) {
    safeSend(sender, 'project-scanned', {
      success: false,
      path: String(projectPath || ''),
      source: 'open',
      error: '项目文件夹不存在或无法访问。'
    })
    return false
  }
  cangjieLspManager.stopForWebContents(sender.id)
  clearCangjieBuildStates(sender.id)
  workspaceSessionStore.saveLastProject(activeProjectPath)
  workspaceSessionStore.recordRecentProject(activeProjectPath)
  registerProjectRoot(sender.id, activeProjectPath)

  // 页面加载完成后再推送 project-opened——否则监听器还没挂上,消息会丢失
  const notifyProjectReady = () => {
    if (sender.isDestroyed()) return
    startProjectWatcher(sender, activeProjectPath)
    safeSend(sender, 'project-opened', activeProjectPath)
    // 首次打开仓颉项目时弹出引导
    checkAndNotifyGuide(sender)
  }

  // 从 Welcome 打开项目时,恢复关闭项目前保存的窗口尺寸/最大化状态
  const openWin = windows.get(sender.id)
  const savedState = windowStates.get(sender.id)
  if (savedState && openWin && !openWin.isDestroyed()) {
    if (savedState.isMaximized) {
      openWin.maximize()
    } else {
      openWin.setBounds(savedState.bounds)
    }
    windowStates.delete(sender.id)
  }

  // 已在工作台页:直接推送;在 Welcome 页:先导航到工作台,did-finish-load 后再推送
  if (!sender.isDestroyed() && sender.getURL().includes('index.html')) {
    notifyProjectReady()
  } else if (!sender.isDestroyed()) {
    sender.once('did-finish-load', notifyProjectReady)
    sender.loadFile('index.html')
  }
  return true
}

ipcMain.on('open-project-path', (event, projectPath) => {
  openProjectPathInWebContents(event.sender, projectPath)
})

// 创建新临时项目(用于 Welcome 页面的 New Project)
ipcMain.handle('create-new-project', async () => {
  try {
    const tempDir = path.join(os.tmpdir(), `cide-project-${Date.now()}`)
    fs.mkdirSync(path.join(tempDir, 'src'), { recursive: true })
    // 字段必须完整,否则 cjpm 1.1.3 报 cjc-version/output-type 错误
    fs.writeFileSync(
      path.join(tempDir, 'cjpm.toml'),
      `[package]
  cjc-version = "1.1.3"
  name = "untitled"
  organization = ""
  description = ""
  version = "0.1.0"
  target-dir = ""
  script-dir = ""
  output-type = "executable"
  compile-option = ""
  override-compile-option = ""
  link-option = ""
  package-configuration = {}
`,
      'utf8'
    )
    fs.writeFileSync(
      path.join(tempDir, 'src', 'main.cj'),
      `package untitled

main(): Int64 {
    println("Hello, Cangjie!")
    return 0
}
`,
      'utf8'
    )
    return { success: true, path: tempDir }
  } catch (error) {
    logger.error('Failed to create new project', { error: error.message })
    return { success: false, error: String(error) }
  }
})

// 关闭当前项目(回到 Welcome 页面)
ipcMain.on('close-project', (event) => {
  const webContents = event.sender
  if (webContents.isDestroyed()) return

  // 如果当前已经在 Welcome 页面,直接返回(不需要关闭项目)
  const currentUrl = webContents.getURL()
  if (currentUrl.includes('welcome.html')) {
    return
  }

  // 多实例逻辑:检查窗口数量
  const windowCount = windows.size
  const win = getWindowFromEvent(event)
  if (windowCount <= 1) {
    // 单实例:显示 Welcome 页面,不关闭窗口,缩小窗口
    // 仅清 lastProjectPath(下次启动不再自动进项目),保留 recentProjects
    workspaceSessionStore.clearLastProject()
    // 保存当前窗口状态(最大化/大小/位置)
    const currentState = {
      isMaximized: win.isMaximized(),
      bounds: win.getBounds()
    }
    windowStates.set(webContents.id, currentState)
    cangjieLspManager.stopForWebContents(webContents.id)
    clearCangjieBuildStates(webContents.id)
    stopProjectWatcher(webContents.id)
    clearWebContentsRoots(webContents.id)
    if (!webContents.isDestroyed() && win) {
      win.setSize(960, 650)
      win.webContents.loadFile('welcome.html')
    }
  } else {
    // 多实例:关闭当前窗口,显示剩余实例
    const win = getWindowFromEvent(event)
    if (win && !win.isDestroyed()) {
      // 先清理资源
      cangjieLspManager.stopForWebContents(webContents.id)
      clearCangjieBuildStates(webContents.id)
      stopProjectWatcher(webContents.id)
      clearWebContentsRoots(webContents.id)
      // 标记为已批准关闭
      approvedWindowCloses.add(win)
      win.close()
    }
  }
})

ipcMain.on('open-project-current-window', (event, projectPath) => {
  const activeProjectPath =
    workspaceSessionStore.normalizeProjectPath(projectPath)
  if (!activeProjectPath) {
    safeSend(event.sender, 'project-scanned', {
      success: false,
      path: String(projectPath || ''),
      source: 'open',
      error: '项目文件夹不存在或无法访问。'
    })
    return
  }
  cangjieLspManager.stopForWebContents(event.sender.id)
  clearCangjieBuildStates(event.sender.id)
  workspaceSessionStore.saveLastProject(activeProjectPath)
  workspaceSessionStore.recordRecentProject(activeProjectPath)
  registerProjectRoot(event.sender.id, activeProjectPath)
  startProjectWatcher(event.sender, activeProjectPath)

  // 恢复之前保存的窗口状态(从 Welcome 页面打开项目时)
  const savedState = windowStates.get(event.sender.id)
  if (savedState) {
    const openWin = getWindowFromEvent(event)
    if (openWin && !openWin.isDestroyed()) {
      if (savedState.isMaximized) {
        openWin.maximize()
      } else {
        openWin.setBounds(savedState.bounds)
      }
    }
    windowStates.delete(event.sender.id)
  }

  safeSend(event.sender, 'project-opened', activeProjectPath)

  // 首次打开仓颉项目时弹出引导(通过 guide-shown.flag 判断是否已看过)
  checkAndNotifyGuide(event.sender)
})

ipcMain.on('open-project-new-window', (event, projectPath) => {
  const activeProjectPath =
    workspaceSessionStore.normalizeProjectPath(projectPath)
  if (!activeProjectPath) {
    safeSend(event.sender, 'project-scanned', {
      success: false,
      path: String(projectPath || ''),
      source: 'open',
      error: 'The project folder does not exist or cannot be accessed.'
    })
    return
  }
  workspaceSessionStore.saveLastProject(activeProjectPath)
  workspaceSessionStore.recordRecentProject(activeProjectPath)
  createWindow(activeProjectPath)
})

function showAbout() {
  const focusedWin = getFocusedWindow()
  if (focusedWin) {
    safeSend(focusedWin.webContents, 'show-about-dialog')
  }
}

// ============== 窗口操作 IPC - 使用 event.sender 定位窗口 ==============
ipcMain.on('minimize', (event) => {
  const win = getWindowFromEvent(event)
  logger.info(`[IPC] minimize: win=${win ? 'exists' : 'null'}, destroyed=${win?.isDestroyed()}`)
  if (win && !win.isDestroyed()) {
    logger.info('[IPC] minimize: calling win.minimize()')
    win.minimize()
    logger.info('[IPC] minimize: win.minimize() called successfully')
  } else {
    logger.error('[IPC] minimize: window is null or already destroyed!')
  }
})

ipcMain.on('quit', (event) => {
  const win = getWindowFromEvent(event)
  logger.info(`[IPC] quit: win=${win ? 'exists' : 'null'}, approved=${win ? approvedWindowCloses.has(win) : 'N/A'}, destroyed=${win?.isDestroyed()}`)
  if (win && !win.isDestroyed()) {
    logger.info('[IPC] quit: adding to approvedWindowCloses and calling win.close()')
    approvedWindowCloses.add(win)
    logger.info('[IPC] quit: added to approved list, calling close()')
    win.close()
    logger.info('[IPC] quit: close() called successfully')
  } else {
    logger.error('[IPC] quit: window is null or already destroyed!')
  }
})

ipcMain.on('show-settings', (event) => {
  const win = getWindowFromEvent(event)
  if (win && !win.isDestroyed()) {
    win.webContents.once('did-finish-load', () => {
      if (!win.isDestroyed()) {
        safeSend(win.webContents, 'menu-action', 'show-settings')
      }
    })
    win.webContents.loadFile('index.html')
  }
})

ipcMain.on('maximize', (event) => {
  const win = getWindowFromEvent(event)
  if (win) {
    if (win.isMaximized()) {
      win.unmaximize()
    } else {
      win.maximize()
    }
  }
})

ipcMain.on('close', (event) => {
  const win = getWindowFromEvent(event)
  if (!win) return
  pendingWindowCloses.delete(win)
  approvedWindowCloses.add(win)
  win.close()
})

ipcMain.on('window-close-response', (event, payload = {}) => {
  const win = getWindowFromEvent(event)
  if (!win) return
  const pendingRequestId = pendingWindowCloses.get(win)
  if (
    pendingRequestId === undefined ||
    payload.requestId !== pendingRequestId
  ) {
    return
  }

  pendingWindowCloses.delete(win)
  if (payload.approved !== true) return
  approvedWindowCloses.add(win)
  win.close()
})

ipcMain.on('get-directory', async (event, dirPath) => {
  try {
    if (!dirPath || typeof dirPath !== 'string') {
      event.reply('directory-content', [])
      return
    }
    const resolved = path.resolve(dirPath)
    if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {
      event.reply('directory-content', [])
      return
    }
    const files = fs.readdirSync(resolved, { withFileTypes: true })
    const tree = files.map(file => ({
      name: file.name,
      isDirectory: file.isDirectory(),
      path: path.join(resolved, file.name)
    }))
    event.reply('directory-content', tree)
  } catch (error) {
    event.reply('directory-content', [])
  }
})

ipcMain.on('scan-project', async (event, request) => {
  const projectPath = typeof request === 'string'
    ? request
    : request && request.projectPath
  const source = typeof request === 'object' && request
    ? request.source || ''
    : ''
  if (!projectPath) return

  // 工程扫描必然意味着该窗口打开了此工程:登记为路径围栏授权根
  registerProjectRoot(event.sender.id, projectPath)

  try {
    const result = await projectTreeScanner.scan(projectPath, {
      ownerId: event.sender.id
    })
    safeSend(event.sender, 'project-scanned', {
      success: true,
      name: path.basename(projectPath),
      path: projectPath,
      children: result.children,
      stats: result.stats,
      source
    })
  } catch (error) {
    if (error?.code === 'PROJECT_SCAN_CANCELLED') return
    logger.error('project-tree-scan', error, {
      projectPath,
      webContentsId: event.sender.id
    })
    safeSend(event.sender, 'project-scanned', {
      success: false,
      name: path.basename(projectPath),
      path: projectPath,
      error: error.message,
      errorCode: error.code || 'PROJECT_SCAN_FAILED',
      source
    })
  }
})

registerEditorFileIpc(ipcMain, {
  markCangjieBuildInputsChanged
})

ipcMain.on('open-project', event => {
  const sender = event.sender
  // Welcome 页没有内嵌项目选择器:先进入工作台壳,加载完再弹选择器
  if (!sender.isDestroyed() && !sender.getURL().includes('index.html')) {
    sender.once('did-finish-load', () => {
      if (!sender.isDestroyed()) {
        safeSend(sender, 'show-project-picker')
      }
    })
    sender.loadFile('index.html')
    return
  }
  safeSend(sender, 'show-project-picker')
})

registerProjectPickerIpc(ipcMain, { safeSend, shell })

ipcMain.on('new-project', (event) => {
  safeSend(event.sender, 'menu-action', 'new-project')
})

ipcMain.on('run', (event) => {
  safeSend(event.sender, 'menu-action', 'run')
})

ipcMain.on('debug', (event) => {
  safeSend(event.sender, 'menu-action', 'debug')
})

ipcMain.on('about', () => {
  showAbout()
})

registerTreeOperationsIpc(ipcMain, { shell })

// ============== 服务面板(模块化) ==============
serviceRun.registerServiceIpc(ipcMain)

registerCangjieDebugIpc(ipcMain, { cangjieDebugManager, safeSend })
registerCangjieHeapIpc(ipcMain, {
  loadSettings,
  safeSend,
  getServicePid: (wcId, serviceId) => {
    const procs = serviceRun.getServiceProcesses()
    const key = `${wcId}:${serviceId}`
    return procs[key]?.pid || null
  }
})

// ============== 设置 IPC ==============
function _validateStdxLibPath(dir) {
  if (!dir) return { valid: false, resolved: '' }
  try {
    const fs = require('fs')
    const path = require('path')
    const stat = fs.statSync(dir)
    if (!stat.isDirectory()) return { valid: false, resolved: '' }
    // 递归扫一层看有没有 .cjo(cjo-catalog-core 会扫更深,这里只做快速判断)
    const dirs = [dir]
    for (let depth = 0; depth < 2 && dirs.length; depth++) {
      const entries = fs.readdirSync(dirs.shift(), { withFileTypes: true })
      for (const e of entries) {
        if (e.isFile() && /\.cjo$/i.test(e.name)) {
          return { valid: true, resolved: path.resolve(dir) }
        }
        if (e.isDirectory()) dirs.push(path.join(dir, e.name))
      }
    }
    return { valid: false, resolved: path.resolve(dir) }
  } catch (_) {
    return { valid: false, resolved: '' }
  }
}

function loadSettingsForRenderer() {
  const settings = loadSettings()
  const configuredPath = String(settings.cjHome || '').trim()
  const resolved = _resolveCangjieSdk(configuredPath)
  const configuredGitPath = String(settings.gitPath || '').trim()
  const resolvedGit = resolveGitInstallation({
    configuredPath: configuredGitPath
  })
  const stdxCheck = _validateStdxLibPath(String(settings.stdxLibPath || '').trim())
  return {
    ...settings,
    cjHome: configuredPath || resolved.path,
    cjHomeResolved: resolved.path,
    cjHomeSource: resolved.source,
    cjHomeValid: configuredPath
      ? isCangjieSdkRoot(configuredPath)
      : Boolean(resolved.path),
    stdxLibPathResolved: stdxCheck.resolved,
    stdxLibPathValid: stdxCheck.valid,
    gitPath: configuredGitPath || resolvedGit.path,
    gitPathResolved: resolvedGit.path,
    gitPathSource: resolvedGit.source,
    gitPathValid: configuredGitPath
      ? Boolean(findGitExecutable(configuredGitPath))
      : Boolean(resolvedGit.path),
    opensslPathResolved: opensslRuntime.getOpenSSLStatus().dir,
    opensslPathSource: opensslRuntime.getOpenSSLStatus().source,
    opensslPathVersion: opensslRuntime.getOpenSSLStatus().version,
    opensslPathValid: opensslRuntime.getOpenSSLStatus().available,
    stdxAutoDir: stdxDownloader.getLatestInstalledDir()
  }
}

ipcMain.on('settings-load', (event) => {
  safeSend(event.sender, 'settings-loaded', loadSettingsForRenderer())
})

// ============== OpenSSL 下载安装 IPC ==============
ipcMain.handle('openssl-download-list', async () => ({
  distros: opensslDownloader.OPENSSL_DISTROS,
  installed: await opensslDownloader.getInstalledInfo(),
  busy: opensslDownloader.isBusy()
}))
ipcMain.handle('openssl-download-start', async (event, payload) => {
  const id = payload && typeof payload.id === 'string' ? payload.id : ''
  const installDir = payload && typeof payload.installDir === 'string' ? payload.installDir : ''
  return opensslDownloader.download({ id, installDir }, event)
})
ipcMain.handle('openssl-download-remove', () => opensslDownloader.removeDownloaded())

// ============== STDX 自动下载安装 IPC ==============
function _resolveSdkVersionFromMain() {
  const cjHome = _getCjHome()
  if (!cjHome) return ''
  const cjc = path.join(cjHome, 'bin', process.platform === 'win32' ? 'cjc.exe' : 'cjc')
  if (!fs.existsSync(cjc)) return ''
  try {
    const { spawnSync } = require('child_process')
    const out = spawnSync(cjc, ['--version'], {
      windowsHide: true, encoding: 'utf8', timeout: 5000
    })
    const m = String(out.stdout || '').match(/(\d+\.\d+\.\d+)/)
    return m ? m[1] : ''
  } catch (_) { return '' }
}
ipcMain.handle('stdx-download-list', async () => {
  const sdkVersion = _resolveSdkVersionFromMain()
  return stdxDownloader.getAvailable({ sdkVersion })
})
ipcMain.handle('stdx-download-start', async (event, payload) => {
  const tag = payload && typeof payload.tag === 'string' ? payload.tag : ''
  const installDir = payload && typeof payload.installDir === 'string' ? payload.installDir.trim() : ''
  if (!tag) return { success: false, error: 'Missing tag' }
  if (!installDir) return { success: false, error: '请先选择 STDX 的安装目录' }
  return stdxDownloader.download({ tag, installDir }, event)
})
ipcMain.handle('stdx-download-remove', async (_event, payload) => {
  const tag = payload && typeof payload.tag === 'string' ? payload.tag : ''
  if (!tag) return { success: false, error: 'Missing tag' }
  return stdxDownloader.remove({ tag })
})
// 暴露给 cangjie-cjo-ipc 取自动安装路径
ipcMain.handle('stdx-auto-dir', () => stdxDownloader.getLatestInstalledDir())

ipcMain.handle('settings-get', () => loadSettingsForRenderer())

// ============== 设置落盘前的入口收敛(纵深防御) ==============
// 这些路径/URL 字段在使用处已各有二次校验(git/sdk 走可执行存在性、update 走
// requireHttpsUrl),此处再补一道入口防线:拒绝控制字符(换行可注入下游命令行 /
// 配置解析器)、限制单字段长度(防超大值),URL 类仅接受 http(s)。
// 非法值退回上一已存值或空,绝不因净化而崩溃。
const MAX_SETTING_FIELD_CHARS = 4096
const MAX_SETTING_BLOB_CHARS = 2 * 1024 * 1024
function _sanitizePathLike(raw, fallback) {
  const source = typeof raw === 'string' ? raw : String(fallback == null ? '' : fallback)
  const trimmed = source.trim()
  if (!trimmed) return ''
  // 含 NUL/换行等控制字符:视为异常,退回已存值(本身已在既往保存时被净化过)。

  if (/[\u0000-\u001f\u007f]/.test(trimmed)) {
    return String(fallback == null ? '' : fallback).trim().slice(0, MAX_SETTING_FIELD_CHARS)
  }
  return trimmed.slice(0, MAX_SETTING_FIELD_CHARS)
}
function _sanitizeHttpUrl(raw, fallback) {
  const value = _sanitizePathLike(raw, fallback)
  if (!value) return ''
  try {
    const u = new URL(value)
    if (u.protocol === 'http:' || u.protocol === 'https:') return value
  } catch (_) { /* 非绝对 URL:视为无效 */ }
  return ''
}

ipcMain.on('settings-save', (event, settings) => {
  if (!settings || typeof settings !== 'object') return
  try {
    const current = loadSettings()
    const nextSettings = {
      ...current,
      ...(settings || {}),
      cjHome: _sanitizePathLike(settings?.cjHome, current.cjHome),
      stdxLibPath: _sanitizePathLike(settings?.stdxLibPath, current.stdxLibPath),
      gitPath: _sanitizePathLike(settings?.gitPath, current.gitPath),
      notesPath: _sanitizePathLike(settings?.notesPath, current.notesPath),
      opensslInstallDir: _sanitizePathLike(settings?.opensslInstallDir, current.opensslInstallDir),
      updateUrl: _sanitizeHttpUrl(settings?.updateUrl, current.updateUrl),
      aiApiUrl: _sanitizeHttpUrl(settings?.aiApiUrl, current.aiApiUrl)
    }
    // 体积封顶:拒绝异常/被注入的超大负载写满磁盘或拖慢每次读取。
    if (JSON.stringify(nextSettings).length > MAX_SETTING_BLOB_CHARS) {
      logger.warn('Rejected oversized settings payload')
      return
    }
    saveSettings(nextSettings)
    // Send merged settings back to renderer to keep _loadedSettings in sync
    safeSend(event.sender, 'settings-loaded', loadSettingsForRenderer())
  } catch (e) {
    logger.warn('Failed to save settings', { error: e.message })
  }
})

const gitConflictService = createGitConflictService({
  getGitRepositoryRoot,
  getGitState,
  runGit,
  validateGitRelativePath
})

registerGitIpc(ipcMain, {
  createGitPullConflictResult,
  ensureExpectedGitBranch,
  getCurrentGitBranch,
  getGitCommitDetail,
  getGitCompareFile,
  getGitCompareFiles,
  getGitCompareRefs,
  getGitConflictContent: gitConflictService.getContent,
  getGitDiff,
  getGitIncomingPaths,
  getGitLog,
  getGitLogAuthors,
  getGitOperationContext,
  getGitPushContext,
  getGitRepositoryRoot,
  getGitState,
  runGit,
  resolveGitConflict: gitConflictService.resolve,
  validateGitRelativePath,
  validateGitRemoteTarget
})

// ============== SDK 版本 + 项目创建(模块化) ==============
projectCreation.registerSdkVersionIpc(ipcMain)
projectCreation.registerProjectCreationIpc(ipcMain)

registerTestRunnerIpc(ipcMain, {
  safeSend,
  logger,
  processLifecycle,
  sdkManager: cangjieSdkManager
})

registerCangjieFormatFolderIpc(ipcMain, {
  safeSend,
  logger,
  getSdkRoot: () => _getCjHome()
})

registerCjpmDependencyIpc(ipcMain, {
  logger,
  sdkManager: cangjieSdkManager,
  isPathAllowed: isPathAllowedForSender
})

registerTodoScanIpc(ipcMain, {
  logger
})

registerRunConfigIpc(ipcMain, {
  logger
})


// 单实例锁:防止双开同一项目产生两套 LSPServer/pty 进程,
// 以及 recovery 快照/会话存储被两个实例互相覆盖。
const gotSingleInstanceLock = app.requestSingleInstanceLock()
if (!gotSingleInstanceLock) {
  app.quit()
} else {
  app.on('second-instance', () => {
    const existing = getFocusedWindow()
    if (!existing || existing.isDestroyed()) return
    if (existing.isMinimized()) existing.restore()
    existing.show()
    existing.focus()
  })

  app.whenReady().then(() => {
    projectCreation.persistAutoDetectedToolPaths()

    // 休眠唤醒后 LSPServer 可能已死或假死(管道冻结):对所有 client
    // 做一次活性探测,死了再复活
    powerMonitor.on('resume', () => {
      try {
        cangjieLspManager.probeAndRecoverAll()
      } catch (error) {
        logger.warn('power-monitor-resume-failed', {
          message: error?.message || String(error)
        })
      }
    })

    createWindow(
      workspaceSessionStore.loadLastProject(),
      { showStartupSplash: true }
    )

    // 启动Python预检(结果会被缓存,handler中await获取)
    void projectCreation.checkPythonForDebugger()

    app.on('activate', () => {
      if (windows.size === 0) {
        createWindow(
          workspaceSessionStore.loadLastProject(),
          { showStartupSplash: true }
        )
      }
    })
  }).catch(err => {
    logger.error('app.whenReady failed', { error: err.message, stack: err.stack })
    app.exit(1)
  })
}

app.on('before-quit', () => {
  cangjieLspManager.stopAll()
  cangjieDebugManager.stopAll()
  projectTreeScanner.cancelAll('application-quit')
  projectSearchService.cancelAll('application-quit')
  cangjieWorkspaceIndexer.cancelAll('application-quit')
  Array.from(projectWatchers.keys()).forEach(stopProjectWatcher)
  clearAllBuildStates()
  processLifecycle.stopAll('application-quit')
  serviceRun.clearAll()
})

registerTerminalIpc(ipcMain, {
  processLifecycle,
  spawnProcess: spawn,
  getEnvironmentPath: _getFreshPath,
  getSdkRoot: _getCjHome
})

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})