const fs = require('node:fs/promises')
const fsSync = require('node:fs')
const os = require('node:os')
const path = require('node:path')
const { spawnSync } = require('node:child_process')
const { app, BrowserWindow, dialog, ipcMain, Menu, shell } = require('electron')
const { ChromeAdapter } = require('./chrome-adapter.cjs')
const { PiAdapter } = require('./pi-adapter.cjs')
const packageJson = require('../package.json')

const isDev = !app.isPackaged && process.env.HYPER_DESK_DEV === '1'
const shouldOpenDevTools = process.env.HYPER_DESK_DEVTOOLS === '1'

let mainWindow
let chromeAdapter
let piAdapter
let appSettings = { externalPiPackagesEnabled: false }

function pathParts(value) {
  return String(value || '')
    .split(path.delimiter)
    .map(part => part.trim())
    .filter(Boolean)
}

function expandHome(value) {
  const text = String(value || '')
  if (text === '~') return os.homedir()
  if (text.startsWith('~/')) return path.join(os.homedir(), text.slice(2))
  return text
}

function isDirectory(value) {
  try {
    return fsSync.statSync(expandHome(value)).isDirectory()
  } catch (_error) {
    return false
  }
}

function compareNodeVersionNames(left, right) {
  const leftParts = String(left).replace(/^v/, '').split('.').map(part => Number(part) || 0)
  const rightParts = String(right).replace(/^v/, '').split('.').map(part => Number(part) || 0)
  for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
    const delta = (rightParts[index] || 0) - (leftParts[index] || 0)
    if (delta !== 0) return delta
  }
  return String(right).localeCompare(String(left))
}

function nvmBinPaths() {
  const nvmDir = expandHome(process.env.NVM_DIR || '~/.nvm')
  const versionsDir = path.join(nvmDir, 'versions', 'node')
  try {
    return fsSync.readdirSync(versionsDir, { withFileTypes: true })
      .filter(entry => entry.isDirectory())
      .map(entry => entry.name)
      .sort(compareNodeVersionNames)
      .map(version => path.join(versionsDir, version, 'bin'))
      .filter(isDirectory)
  } catch (_error) {
    return []
  }
}

function pathFromLoginShell() {
  if (process.platform !== 'darwin') return []
  const shellPath = process.env.SHELL || '/bin/zsh'
  if (!fsSync.existsSync(shellPath)) return []

  const markerStart = '__HYPER_DESK_PATH_START__'
  const markerEnd = '__HYPER_DESK_PATH_END__'
  const result = spawnSync(shellPath, [
    '-ilc',
    `printf '%s%s%s' '${markerStart}' "$PATH" '${markerEnd}'`
  ], {
    encoding: 'utf8',
    timeout: 3000,
    env: process.env
  })

  const output = `${result.stdout || ''}${result.stderr || ''}`
  const start = output.lastIndexOf(markerStart)
  const end = output.lastIndexOf(markerEnd)
  if (start < 0 || end <= start) return []
  return pathParts(output.slice(start + markerStart.length, end))
}

function envFlag(name) {
  const value = process.env[name]
  if (value === undefined) return undefined
  return ['1', 'true', 'yes', 'on'].includes(String(value).toLowerCase())
}

function appSettingsPath() {
  return path.join(app.getPath('userData'), 'hyperdesk-settings.json')
}

async function loadAppSettings() {
  const defaults = {
    externalPiPackagesEnabled: envFlag('HYPER_DESK_ENABLE_PI_PACKAGES') === true
  }

  try {
    const parsed = JSON.parse(await fs.readFile(appSettingsPath(), 'utf8'))
    return {
      ...defaults,
      ...parsed,
      externalPiPackagesEnabled: envFlag('HYPER_DESK_ENABLE_PI_PACKAGES') ?? Boolean(parsed.externalPiPackagesEnabled)
    }
  } catch (error) {
    if (error?.code !== 'ENOENT') console.warn('Could not load HyperDesk settings:', error)
    return defaults
  }
}

async function saveAppSettings() {
  await fs.mkdir(path.dirname(appSettingsPath()), { recursive: true })
  await fs.writeFile(appSettingsPath(), JSON.stringify(appSettings, null, 2), 'utf8')
}

function sessionStatePath() {
  return path.join(app.getPath('userData'), 'hyperdesk-sessions.tsv')
}

async function loadSessionState() {
  try {
    return await fs.readFile(sessionStatePath(), 'utf8')
  } catch (error) {
    if (error?.code === 'ENOENT') return ''
    throw error
  }
}

async function saveSessionState(value) {
  const target = sessionStatePath()
  const dir = path.dirname(target)
  const temp = path.join(dir, `.hyperdesk-sessions.${process.pid}.${Date.now()}.tmp`)
  await fs.mkdir(dir, { recursive: true })
  await fs.writeFile(temp, String(value || ''), 'utf8')
  await fs.rename(temp, target)
  return { ok: true }
}

async function setExternalPiPackagesEnabled(enabled) {
  appSettings = { ...appSettings, externalPiPackagesEnabled: Boolean(enabled) }
  piAdapter?.setExternalPackagesEnabled(appSettings.externalPiPackagesEnabled)
  await saveAppSettings()
}

function installShellPath() {
  const commonPaths = [
    '~/.local/bin',
    '~/.volta/bin',
    '~/.asdf/shims',
    '~/.bun/bin',
    '~/.deno/bin',
    '~/.cargo/bin',
    '/opt/homebrew/bin',
    '/opt/homebrew/sbin',
    '/usr/local/bin',
    '/usr/local/sbin',
    '/opt/local/bin',
    '/usr/bin',
    '/bin',
    '/usr/sbin',
    '/sbin'
  ].map(expandHome).filter(isDirectory)

  const merged = [
    ...pathFromLoginShell(),
    ...nvmBinPaths(),
    ...commonPaths,
    ...pathParts(process.env.PATH)
  ]

  const seen = new Set()
  const deduped = []
  for (const entry of merged) {
    const resolved = expandHome(entry)
    if (seen.has(resolved)) continue
    seen.add(resolved)
    deduped.push(resolved)
  }

  if (deduped.length > 0) {
    process.env.PATH = deduped.join(path.delimiter)
  }
}

function navigationPolicy(rawUrl) {
  const value = String(rawUrl || '')

  try {
    const parsed = new URL(value)

    if (isAppNavigationUrl(parsed)) {
      return { action: 'allow' }
    }

    if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
      return { action: 'external', url: parsed.toString() }
    }
  } catch (_error) {
    // malformed URLs are blocked below
  }

  return { action: 'block' }
}

function isAppNavigationUrl(parsed) {
  if (isDev && parsed.protocol === 'http:' && parsed.hostname === '127.0.0.1' && parsed.port === '5173') {
    return true
  }

  if (parsed.protocol !== 'file:') return false

  const appIndex = path.resolve(__dirname, '..', 'dist', 'index.html')
  const target = decodeURIComponent(parsed.pathname)
  return path.resolve(target) === appIndex
}

function installExternalLinkHandling(win) {
  win.webContents.setWindowOpenHandler(({ url }) => {
    const policy = navigationPolicy(url)
    if (policy.action === 'external') {
      shell.openExternal(policy.url)
    }
    return { action: 'deny' }
  })

  win.webContents.on('will-navigate', (event, url) => {
    const policy = navigationPolicy(url)
    if (policy.action === 'allow') return

    event.preventDefault()
    if (policy.action === 'external') {
      shell.openExternal(policy.url)
    }
  })
}

function notifyUi(channel, payload) {
  if (mainWindow && !mainWindow.webContents.isDestroyed()) {
    mainWindow.webContents.send(channel, payload)
  }
}

function sendMenuAction(action, payload = {}) {
  notifyUi('menu:action', { action, ...payload })
}

function showAbout() {
  notifyUi('app:about', {
    appVersion: packageJson.version || 'unknown',
    piVersion: packageJson.dependencies?.['@earendil-works/pi-coding-agent'] || 'unknown',
    electronVersion: process.versions.electron || 'unknown',
    nodeVersion: process.versions.node || 'unknown',
    platform: `${process.platform} ${process.arch}`
  })
}

function openProjectReadme() {
  shell.openPath(path.join(__dirname, '..', 'README.md'))
}

function openPiDocs() {
  shell.openExternal('https://pi.dev')
}

function profileDir(scope, cwd = process.cwd()) {
  return scope === 'global'
    ? path.join(os.homedir(), '.pi', 'agent', 'hyper-desk')
    : path.join(path.resolve(cwd), '.pi', 'hyper-desk')
}

function profilePath(scope, name, cwd = process.cwd()) {
  return path.join(profileDir(scope, cwd), `${name}.md`)
}

async function readProfileFile(scope, name, cwd) {
  const filePath = profilePath(scope, name, cwd)
  try {
    return { path: filePath, content: await fs.readFile(filePath, 'utf8') }
  } catch (error) {
    if (error?.code === 'ENOENT') return { path: filePath, content: '' }
    throw error
  }
}

const configurableProviders = [
  ['openai-codex', 'ChatGPT Plus/Pro Codex'],
  ['ollama', 'Ollama'],
  ['github-copilot', 'GitHub Copilot'],
  ['openai', 'OpenAI'],
  ['anthropic', 'Anthropic'],
  ['google', 'Google Gemini'],
  ['openrouter', 'OpenRouter'],
  ['xai', 'xAI'],
  ['groq', 'Groq'],
  ['mistral', 'Mistral'],
  ['deepseek', 'DeepSeek']
]

async function createPiConfigServices(cwd = process.cwd()) {
  const { AuthStorage, getAgentDir, ModelRegistry, SettingsManager } = await import('@earendil-works/pi-coding-agent')
  const agentDir = getAgentDir()
  const authStorage = AuthStorage.create(path.join(agentDir, 'auth.json'))
  const modelRegistry = ModelRegistry.create(authStorage, path.join(agentDir, 'models.json'))
  const settingsManager = SettingsManager.create(path.resolve(cwd), agentDir)
  return { authStorage, modelRegistry, settingsManager }
}

function modelLabel(model) {
  return `${model.provider}/${model.id || model.modelId || model.name || 'unknown'}`
}

async function loadPiConfig(cwd = process.cwd()) {
  const { modelRegistry, settingsManager } = await createPiConfigServices(cwd)
  const allModels = modelRegistry.getAll()
  const available = modelRegistry.getAvailable()
  const availableSet = new Set(available.map(modelLabel))
  const providers = configurableProviders.map(([id, fallbackLabel]) => {
    const status = modelRegistry.getProviderAuthStatus(id)
    return {
      id,
      label: modelRegistry.getProviderDisplayName(id) || fallbackLabel,
      configured: Boolean(status.configured),
      source: status.source || 'missing',
      models: allModels
        .filter(model => model.provider === id)
        .slice(0, 80)
        .map(model => ({ id: model.id, label: model.name || model.id, available: availableSet.has(modelLabel(model)), reasoning: Boolean(model.reasoning) }))
    }
  })

  return {
    providers,
    defaultProvider: settingsManager.getDefaultProvider() || '',
    defaultModel: settingsManager.getDefaultModel() || '',
    defaultThinkingLevel: settingsManager.getDefaultThinkingLevel() || 'off',
    externalPiPackagesEnabled: Boolean(appSettings.externalPiPackagesEnabled)
  }
}

function createAppMenu() {
  const isMac = process.platform === 'darwin'
  const template = [
    ...(isMac ? [{
      label: app.name,
      submenu: [
        { label: 'About HyperDesk', click: showAbout },
        { type: 'separator' },
        { label: 'Configure Pi…', click: () => sendMenuAction('pi-config') },
        { label: 'Agent Profile…', click: () => sendMenuAction('agent-profile') },
        { type: 'separator' },
        { role: 'hide' },
        { role: 'hideOthers' },
        { role: 'unhide' },
        { type: 'separator' },
        { role: 'quit' }
      ]
    }] : []),
    {
      label: 'File',
      submenu: [
        { label: 'New Session', accelerator: 'CmdOrCtrl+N', click: () => sendMenuAction('new-session') },
        { label: 'Attach Files…', accelerator: 'CmdOrCtrl+O', click: () => sendMenuAction('attach-files') },
        { label: 'Choose Working Directory…', click: () => sendMenuAction('choose-session-folder') },
        { type: 'separator' },
        { label: 'Close Session', accelerator: 'CmdOrCtrl+W', click: () => sendMenuAction('close-session') },
        ...(!isMac ? [{ type: 'separator' }, { role: 'quit' }] : [])
      ]
    },
    {
      label: 'Edit',
      submenu: [
        { role: 'undo' },
        { role: 'redo' },
        { type: 'separator' },
        { role: 'cut' },
        { role: 'copy' },
        { role: 'paste' },
        { role: 'selectAll' },
        { type: 'separator' },
        { label: 'Clear Composer', click: () => sendMenuAction('clear-composer') }
      ]
    },
    {
      label: 'View',
      submenu: [
        { label: 'Toggle Chat Panel', accelerator: 'CmdOrCtrl+\\', click: () => sendMenuAction('toggle-chat') },
        { label: 'Toggle Tool History', accelerator: 'CmdOrCtrl+Shift+T', click: () => sendMenuAction('toggle-tools') },
        { label: 'Show Context Stats', accelerator: 'CmdOrCtrl+I', click: () => sendMenuAction('show-stats') },
        { type: 'separator' },
        {
          label: 'Theme',
          submenu: [
            { label: 'System', click: () => sendMenuAction('theme-system') },
            { label: 'Light', click: () => sendMenuAction('theme-light') },
            { label: 'Dark', click: () => sendMenuAction('theme-dark') }
          ]
        },
        { type: 'separator' },
        { role: 'reload' },
        ...(isDev ? [{ role: 'toggleDevTools' }] : [])
      ]
    },
    {
      label: 'Session',
      submenu: [
        { label: 'Stop Current Run', accelerator: 'Esc', click: () => sendMenuAction('stop-current-run') },
        { label: 'Approve Pending Action', click: () => sendMenuAction('approve-pending') },
        { label: 'Reject Pending Action', click: () => sendMenuAction('reject-pending') },
        { label: 'Approve All Local Effects', click: () => sendMenuAction('approve-all-local-effects') },
        { type: 'separator' },
        { label: 'Open Session Folder', click: () => sendMenuAction('open-session-folder') },
        { type: 'separator' },
        {
          label: 'Enable External Pi Packages',
          type: 'checkbox',
          checked: Boolean(appSettings.externalPiPackagesEnabled),
          click: item => setExternalPiPackagesEnabled(item.checked).catch(error => {
            console.error('Could not update external Pi package setting:', error)
          })
        },
        { type: 'separator' },
        { label: 'Agent Profile…', accelerator: 'CmdOrCtrl+Shift+P', click: () => sendMenuAction('agent-profile') }
      ]
    },
    {
      label: 'Help',
      submenu: [
        ...(!isMac ? [{ label: 'About HyperDesk', click: showAbout }, { type: 'separator' }] : []),
        { label: 'README', click: openProjectReadme },
        { label: 'Pi Documentation', click: openPiDocs },
        { type: 'separator' },
        { label: 'Configure Pi / Login', click: () => sendMenuAction('pi-config') },
        { label: 'Report Issue', click: () => shell.openExternal('https://github.com/twilson63/hyper-desk/issues') }
      ]
    }
  ]

  Menu.setApplicationMenu(Menu.buildFromTemplate(template))
}

function createWindow() {
  const win = new BrowserWindow({
    width: 1200,
    height: 820,
    title: 'HyperDesk',
    icon: path.join(__dirname, '..', 'assets', 'icon.png'),
    backgroundColor: '#120f1d',
    webPreferences: {
      contextIsolation: true,
      nodeIntegration: false,
      sandbox: true,
      preload: path.join(__dirname, 'preload.cjs')
    }
  })

  mainWindow = win

  chromeAdapter = new ChromeAdapter()
  piAdapter = new PiAdapter((channel, payload) => notifyUi(channel, payload), chromeAdapter, {
    externalPackagesEnabled: appSettings.externalPiPackagesEnabled
  })

  installExternalLinkHandling(win)

  if (isDev) {
    win.loadURL('http://127.0.0.1:5173')
    if (shouldOpenDevTools) {
      win.webContents.openDevTools({ mode: 'detach' })
    }
  } else {
    win.loadFile(path.join(__dirname, '..', 'dist', 'index.html'))
  }
}

ipcMain.on('pi:send-message', (_event, payload) => {
  const threadId = Number(payload?.threadId)
  const cwd = String(payload?.cwd || process.cwd())
  const message = String(payload?.message || '')
  const modelName = String(payload?.modelName || '')

  if (!Number.isFinite(threadId) || !message.trim()) return

  piAdapter?.sendMessage(threadId, cwd, message, modelName)
})

ipcMain.on('folder:choose', async (_event, payload) => {
  const current = String(payload?.current || process.cwd())
  const result = await dialog.showOpenDialog(mainWindow, {
    defaultPath: current,
    properties: ['openDirectory', 'createDirectory']
  })

  if (result.canceled || !result.filePaths[0]) return
  notifyUi('folder:chosen', { path: result.filePaths[0] })
})

ipcMain.on('files:choose', async (_event, payload) => {
  const current = String(payload?.current || process.cwd())
  const result = await dialog.showOpenDialog(mainWindow, {
    defaultPath: current,
    properties: ['openFile', 'multiSelections', 'showHiddenFiles']
  })

  if (result.canceled || result.filePaths.length === 0) return
  notifyUi('files:chosen', { paths: result.filePaths })
})

ipcMain.handle('pi-config:load', async (_event, payload) => {
  return await loadPiConfig(String(payload?.cwd || process.cwd()))
})

ipcMain.handle('pi-config:save-key', async (_event, payload) => {
  const provider = String(payload?.provider || '')
  const key = String(payload?.key || '')
  if (!provider || !key.trim()) throw new Error('Provider and API key are required')
  const { authStorage } = await createPiConfigServices(String(payload?.cwd || process.cwd()))
  authStorage.set(provider, { type: 'api_key', key: key.trim() })
  return await loadPiConfig(String(payload?.cwd || process.cwd()))
})

ipcMain.handle('pi-config:remove-key', async (_event, payload) => {
  const provider = String(payload?.provider || '')
  if (!provider) throw new Error('Provider is required')
  const { authStorage } = await createPiConfigServices(String(payload?.cwd || process.cwd()))
  authStorage.remove(provider)
  return await loadPiConfig(String(payload?.cwd || process.cwd()))
})

ipcMain.handle('pi-config:oauth-login', async (_event, payload) => {
  const cwd = String(payload?.cwd || process.cwd())
  const provider = String(payload?.provider || '')
  if (provider !== 'openai-codex') {
    throw new Error(`Subscription login is not available for ${provider}`)
  }

  const { authStorage } = await createPiConfigServices(cwd)
  await authStorage.login(provider, {
    onAuth: info => {
      notifyUi('pi-config:oauth-progress', { provider, message: info.instructions || 'Complete login in your browser.' })
      shell.openExternal(info.url)
    },
    onDeviceCode: info => {
      notifyUi('pi-config:oauth-progress', { provider, message: `Enter code ${info.userCode} at ${info.verificationUri}` })
      shell.openExternal(info.verificationUri)
    },
    onPrompt: async prompt => {
      throw new Error(prompt?.message || 'Manual OAuth input is not available in HyperDesk yet.')
    },
    onSelect: async prompt => prompt?.options?.[0]?.id,
    onProgress: message => notifyUi('pi-config:oauth-progress', { provider, message })
  })

  return await loadPiConfig(cwd)
})

ipcMain.handle('pi-config:set-default', async (_event, payload) => {
  const provider = String(payload?.provider || '')
  const model = String(payload?.model || '')
  const thinking = String(payload?.thinking || 'off')
  if (!provider || !model) throw new Error('Provider and model are required')
  const { settingsManager } = await createPiConfigServices(String(payload?.cwd || process.cwd()))
  settingsManager.setDefaultModelAndProvider(provider, model)
  if (['off', 'minimal', 'low', 'medium', 'high', 'xhigh'].includes(thinking)) {
    settingsManager.setDefaultThinkingLevel(thinking)
  }
  await settingsManager.flush()
  return await loadPiConfig(String(payload?.cwd || process.cwd()))
})

ipcMain.handle('pi-config:save-ollama-model', async (_event, payload) => {
  const cwd = String(payload?.cwd || process.cwd())
  const model = String(payload?.model || '').trim()
  const thinking = String(payload?.thinking || 'off')
  if (!model) throw new Error('Ollama model is required')
  await saveOllamaModel(model)
  const { settingsManager } = await createPiConfigServices(cwd)
  settingsManager.setDefaultModelAndProvider('ollama', model)
  if (['off', 'minimal', 'low', 'medium', 'high', 'xhigh'].includes(thinking)) {
    settingsManager.setDefaultThinkingLevel(thinking)
  }
  await settingsManager.flush()
  return await loadPiConfig(cwd)
})

async function saveOllamaModel(modelId) {
  const { getAgentDir } = await import('@earendil-works/pi-coding-agent')
  const modelsPath = path.join(getAgentDir(), 'models.json')
  let modelsConfig = {}
  try {
    modelsConfig = JSON.parse(await fs.readFile(modelsPath, 'utf8'))
  } catch (error) {
    if (error?.code !== 'ENOENT') throw error
  }

  const providers = { ...(modelsConfig.providers || {}) }
  const existingOllama = providers.ollama || {}
  const existingModels = Array.isArray(existingOllama.models) ? existingOllama.models : []
  const hasModel = existingModels.some(model => (model?.id || model?.name) === modelId)
  providers.ollama = {
    ...existingOllama,
    baseUrl: existingOllama.baseUrl || 'http://localhost:11434/v1',
    api: existingOllama.api || 'openai-completions',
    apiKey: existingOllama.apiKey || 'ollama',
    compat: {
      supportsDeveloperRole: false,
      supportsReasoningEffort: false,
      ...(existingOllama.compat || {})
    },
    models: hasModel ? existingModels : [...existingModels, { id: modelId }]
  }

  const nextConfig = { ...modelsConfig, providers }
  await fs.mkdir(path.dirname(modelsPath), { recursive: true })
  await fs.writeFile(modelsPath, JSON.stringify(nextConfig, null, 2), 'utf8')
}

ipcMain.handle('sessions:load', async () => {
  return await loadSessionState()
})

ipcMain.handle('sessions:save', async (_event, payload) => {
  return await saveSessionState(payload?.value)
})

ipcMain.handle('profile:load', async (_event, payload) => {
  const scope = String(payload?.scope || 'project') === 'global' ? 'global' : 'project'
  const cwd = String(payload?.cwd || process.cwd())
  const [identity, user, soul] = await Promise.all([
    readProfileFile(scope, 'identity', cwd),
    readProfileFile(scope, 'user', cwd),
    readProfileFile(scope, 'soul', cwd)
  ])

  return { scope, identity, user, soul }
})

ipcMain.handle('profile:save', async (_event, payload) => {
  const scope = String(payload?.scope || 'project') === 'global' ? 'global' : 'project'
  const cwd = String(payload?.cwd || process.cwd())
  const dir = profileDir(scope, cwd)
  await fs.mkdir(dir, { recursive: true })

  const files = {
    identity: String(payload?.identity || ''),
    user: String(payload?.user || ''),
    soul: String(payload?.soul || '')
  }

  await Promise.all(Object.entries(files).map(([name, content]) => (
    fs.writeFile(profilePath(scope, name, cwd), content, 'utf8')
  )))

  return {
    ok: true,
    scope,
    identity: await readProfileFile(scope, 'identity', cwd),
    user: await readProfileFile(scope, 'user', cwd),
    soul: await readProfileFile(scope, 'soul', cwd)
  }
})

const maxDroppedFiles = 10
const maxDroppedFileBytes = 10 * 1024 * 1024
const maxDroppedTotalBytes = 25 * 1024 * 1024

ipcMain.handle('files:save-dropped', async (_event, payload) => {
  const files = Array.isArray(payload?.files) ? payload.files : []
  if (files.length === 0) return { paths: [] }
  if (files.length > maxDroppedFiles) {
    throw new Error(`Too many dropped files: maximum is ${maxDroppedFiles}.`)
  }

  const dropDir = path.join(os.tmpdir(), 'hyper-desk-dropped-files', String(Date.now()))
  await fs.mkdir(dropDir, { recursive: true })

  const savedPaths = []
  let totalBytes = 0
  for (let index = 0; index < files.length; index += 1) {
    const file = files[index]
    const name = safeDroppedFileName(file?.name || `dropped-file-${index + 1}`)
    const dataBase64 = String(file?.dataBase64 || '')
    if (!dataBase64) continue

    const bytes = Buffer.byteLength(dataBase64, 'base64')
    totalBytes += bytes
    if (bytes > maxDroppedFileBytes) {
      throw new Error(`Dropped file is too large: ${name}. Maximum is ${Math.round(maxDroppedFileBytes / 1024 / 1024)} MB.`)
    }
    if (totalBytes > maxDroppedTotalBytes) {
      throw new Error(`Dropped files are too large. Maximum total is ${Math.round(maxDroppedTotalBytes / 1024 / 1024)} MB.`)
    }

    const outputPath = path.join(dropDir, name)
    await fs.writeFile(outputPath, Buffer.from(dataBase64, 'base64'))
    savedPaths.push(outputPath)
  }

  return { paths: savedPaths }
})

function safeDroppedFileName(name) {
  const base = path.basename(String(name || 'dropped-file'))
  return base.replace(/[^a-zA-Z0-9._ -]/g, '_') || 'dropped-file'
}

ipcMain.on('pi:abort', (_event, payload) => {
  const threadId = Number(payload?.threadId)

  if (!Number.isFinite(threadId)) return

  piAdapter?.abort(threadId)
})

ipcMain.on('pi:list-models', (_event, payload) => {
  piAdapter?.listModels(String(payload?.provider || 'any'))
})

ipcMain.on('pi:open-login', (_event, payload) => {
  piAdapter?.openLogin(String(payload?.provider || 'any'))
})

ipcMain.on('shell:open-path', (_event, payload) => {
  const target = path.resolve(String(payload?.path || process.cwd()))
  shell.openPath(target)
})

ipcMain.on('approval:resolve', (_event, payload) => {
  piAdapter?.resolveApproval(String(payload?.id || ''), Boolean(payload?.approved))
})

ipcMain.on('approval:approve-all-browser', (_event, payload) => {
  piAdapter?.approveAllBrowserActions(String(payload?.id || ''))
})

ipcMain.on('approval:approve-all-local-effects', (_event, payload) => {
  piAdapter?.approveAllLocalEffects(String(payload?.id || ''))
})

ipcMain.on('chrome:launch', async () => {
  try {
    const summary = await chromeAdapter.launch('about:blank')
    notifyUi('chrome:status', { connected: true, summary })
  } catch (error) {
    notifyUi('chrome:status', { connected: false, summary: error?.message || String(error) })
  }
})

ipcMain.on('chrome:new-tab', async () => {
  try {
    const summary = await chromeAdapter.newTab('about:blank')
    notifyUi('chrome:status', { connected: true, summary })
  } catch (error) {
    notifyUi('chrome:status', { connected: false, summary: error?.message || String(error) })
  }
})

ipcMain.on('chrome:list-tabs', async () => {
  try {
    const tabs = await chromeAdapter.listTabs()
    const summary = tabs.length === 0
      ? 'Connected to Chrome. No page tabs found.'
      : `Connected to Chrome. ${tabs.length} tab${tabs.length === 1 ? '' : 's'}:\n` + tabs.slice(0, 8).map(tab => `• ${tab.title || '(untitled)'} — ${tab.url}`).join('\n')
    notifyUi('chrome:status', { connected: true, summary })
  } catch (error) {
    notifyUi('chrome:status', { connected: false, summary: error?.message || String(error) })
  }
})

app.whenReady().then(async () => {
  installShellPath()
  appSettings = await loadAppSettings()

  const cliWorkspace = process.env.HYPER_DESK_WORKSPACE_CWD
  const workspaceRoot = cliWorkspace
    ? path.resolve(cliWorkspace)
    : isDev ? path.join(__dirname, '..') : app.getPath('userData')
  await fs.mkdir(path.join(workspaceRoot, 'projects'), { recursive: true })
  if (cliWorkspace || !isDev) process.chdir(workspaceRoot)

  createAppMenu()
  createWindow()

  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
}).catch(error => {
  console.error('Failed to start HyperDesk:', error)
  app.quit()
})

app.on('window-all-closed', () => {
  app.quit()
})
