'use strict'

/**
 * Electron 应用启动器
 * 通过 CDP 端口启动 Electron 实例以供 E2E 测试使用
 * 支持 TEST_MODE 绕过单实例锁，允许并行运行多个实例
 */

const { spawn } = require('node:child_process')
const path = require('node:path')
const fs = require('node:fs')
const os = require('node:os')
const net = require('node:net')

const DEFAULT_START_PORT = 9222
const DEFAULT_MAX_PORT_RETRIES = 100
const DEFAULT_PORT_PROBE_TIMEOUT_MS = 100
const DEFAULT_STARTUP_WAIT_MS = 2000
const DEFAULT_FORCE_KILL_TIMEOUT_MS = 5000

function safeCloseServer(server) {
  if (!server) return
  try {
    server.close()
  } catch {
    // ignore close errors for probing server
  }
}

async function findAvailablePortWithReservation(
  startPort = DEFAULT_START_PORT,
  maxRetries = DEFAULT_MAX_PORT_RETRIES,
  currentRetry = 0,
  options = {}
) {
  const createServer = options.createServer || net.createServer
  const listenTimeoutMs = options.listenTimeoutMs ?? DEFAULT_PORT_PROBE_TIMEOUT_MS
  const lastErrorCode = options.lastErrorCode

  if (currentRetry >= maxRetries) {
    const errorSuffix = lastErrorCode ? ` Last error: ${lastErrorCode}.` : ''
    throw new Error(
      `Unable to find available port after ${maxRetries} attempts (tried ports ${startPort}-${startPort + maxRetries - 1}).${errorSuffix}`
    )
  }

  const port = startPort + currentRetry
  const result = await new Promise((resolve) => {
    const server = createServer()
    let completed = false
    let timer = null

    const done = (value) => {
      if (completed) return
      completed = true
      if (timer) clearTimeout(timer)
      resolve(value)
    }

    server.on('error', (error) => {
      safeCloseServer(server)
      done({ ok: false, errorCode: error?.code || 'UNKNOWN' })
    })

    server.listen(port, () => {
      done({ ok: true, port, reservationServer: server })
    })

    timer = setTimeout(() => {
      safeCloseServer(server)
      done({ ok: false, errorCode: 'TIMEOUT' })
    }, listenTimeoutMs)
  })

  if (result.ok) {
    return { port: result.port, reservationServer: result.reservationServer }
  }

  return findAvailablePortWithReservation(startPort, maxRetries, currentRetry + 1, {
    ...options,
    lastErrorCode: result.errorCode,
  })
}

async function terminateProcess(proc, { forceKillTimeoutMs = DEFAULT_FORCE_KILL_TIMEOUT_MS } = {}) {
  if (!proc) return

  if (proc.exitCode !== null || proc.signalCode !== null) {
    return
  }

  await new Promise((resolve) => {
    let resolved = false
    let forceKillTimer = null

    const exitHandler = () => {
      if (!resolved) {
        resolved = true
        if (forceKillTimer) clearTimeout(forceKillTimer)
        resolve()
      }
    }

    proc.once('exit', exitHandler)
    proc.kill('SIGTERM')

    forceKillTimer = setTimeout(() => {
      if (!resolved) {
        try {
          proc.kill(0)
          proc.kill('SIGKILL')
        } catch {
          // process already exited
        }
      }

      if (!resolved) {
        resolved = true
        resolve()
      }
    }, forceKillTimeoutMs)
  })
}

function releaseReservation(reservationServer) {
  safeCloseServer(reservationServer)
}

/**
 * 启动 Electron 应用
 */
async function launchApp(options = {}, deps = {}) {
  const spawnFn = deps.spawnFn || spawn
  const findPortFn = deps.findPortFn || findAvailablePortWithReservation
  const sleepFn = deps.sleepFn || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)))
  const fsImpl = deps.fsImpl || fs

  let reservationServer = null
  let port = options.port
  const startPort = options.startPort ?? DEFAULT_START_PORT
  const maxPortRetries = options.maxPortRetries ?? DEFAULT_MAX_PORT_RETRIES
  const portProbeTimeoutMs = options.portProbeTimeoutMs ?? DEFAULT_PORT_PROBE_TIMEOUT_MS
  const startupWaitTime = options.startupWaitTime ?? DEFAULT_STARTUP_WAIT_MS
  const forceKillTimeoutMs = options.forceKillTimeoutMs ?? DEFAULT_FORCE_KILL_TIMEOUT_MS

  if (!port) {
    const reservation = await findPortFn(startPort, maxPortRetries, 0, {
      listenTimeoutMs: portProbeTimeoutMs,
    })

    if (!reservation) {
      throw new Error('[AppLauncher] 无法找到可用端口')
    }
    port = reservation.port
    reservationServer = reservation.reservationServer
  }

  const userDataDir =
    options.userDataDir ||
    (process.env.CHATLAB_E2E_USER_DATA_DIR
      ? path.join(process.env.CHATLAB_E2E_USER_DATA_DIR, `instance-${port}`)
      : path.join(os.tmpdir(), `chatlab-e2e-${port}`))

  if (!fsImpl.existsSync(userDataDir)) {
    fsImpl.mkdirSync(userDataDir, { recursive: true })
  }

  const appPath = path.resolve(__dirname, '../../..')
  if (!fsImpl.existsSync(appPath)) {
    throw new Error(`[AppLauncher] 应用目录不存在: ${appPath}`)
  }

  const electronExe =
    process.platform === 'win32'
      ? path.resolve(appPath, 'node_modules/.bin/electron.cmd')
      : path.resolve(appPath, 'node_modules/.bin/electron')

  if (!fsImpl.existsSync(electronExe)) {
    throw new Error(`Electron 可执行文件不存在: ${electronExe}`)
  }

  console.log(`[AppLauncher] 启动 Electron，CDP 端口: ${port}`)

  const electronArgs = [`--remote-debugging-port=${port}`, appPath]

  let proc
  let launchError = null
  let exitCode = null
  let exitSignal = null

  try {
    proc = spawnFn(electronExe, electronArgs, {
      stdio: 'inherit',
      env: {
        ...process.env,
        TEST_MODE: 'true',
        CHATLAB_E2E_USER_DATA_DIR: userDataDir,
        ELECTRON_ENABLE_LOGGING: '1',
      },
    })
  } finally {
    releaseReservation(reservationServer)
    reservationServer = null
  }

  if (proc.exitCode !== null && proc.exitCode !== 0) {
    throw new Error(`[AppLauncher] Electron 启动失败，退出码: ${proc.exitCode}`)
  }

  proc.on('error', (error) => {
    launchError = error
    console.error('[AppLauncher] Electron 进程错误:', error.message)
  })

  proc.on('exit', (code, signal) => {
    exitCode = code
    exitSignal = signal

    if (code !== null && code !== 0) {
      console.error(`[AppLauncher] Electron 进程异常退出，退出码: ${code}`)
    }
    if (signal !== null) {
      console.error(`[AppLauncher] Electron 进程被信号杀死: ${signal}`)
    }
  })

  await sleepFn(startupWaitTime)

  if (launchError) {
    await terminateProcess(proc, { forceKillTimeoutMs })
    throw new Error(`[AppLauncher] Electron 启动期间发生错误: ${launchError.message}`)
  }

  if (exitCode !== null && exitCode !== 0) {
    throw new Error(`[AppLauncher] Electron 启动期间异常退出，退出码: ${exitCode}`)
  }

  if (exitSignal !== null) {
    throw new Error(`[AppLauncher] Electron 启动期间被信号杀死: ${exitSignal}`)
  }

  return {
    proc,
    port,
    async close() {
      console.log(`[AppLauncher] 关闭应用 (PID: ${proc.pid})`)

      if (proc.exitCode !== null || proc.signalCode !== null) {
        console.log(`[AppLauncher] 应用已退出 (exit code: ${proc.exitCode}, signal: ${proc.signalCode})`)
        return
      }

      await terminateProcess(proc, { forceKillTimeoutMs })
    },
  }
}

module.exports = {
  launchApp,
  __test__: {
    findAvailablePortWithReservation,
    terminateProcess,
    releaseReservation,
  },
}
