#!/usr/bin/env node

/**
 * CodePocket — 你的全能 AI 随身控制台
 *
 *   npx codepocket          启动 Claude Code + 自动连接手机
 *   npx codepocket --key X  首次配对（保存 Key）
 *   npx cpkt                短命令
 */

const { WebSocket } = require('ws')
const { spawn, execSync } = require('child_process')
const fs = require('fs')
const path = require('path')
const os = require('os')
const readline = require('readline')
const { startMcpServer, handlePhoneActionResponse: onPhoneActionResponse } = require('../src/mcp-server')
const { runTestCli } = require('../src/mcp-client')

// ── Config ──

const CONFIG_DIR = path.join(os.homedir(), '.codepocket')
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
const SESSION_FILE = path.join(CONFIG_DIR, 'session.json')
const HOME_ENV = { CODEPOCKET_HOME: path.join(os.homedir(), '.codepocket') }

function loadConfig() {
  try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8')) } catch { return {} }
}

function saveConfig(cfg) {
  fs.mkdirSync(CONFIG_DIR, { recursive: true })
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2))
}

function injectMcpConfig(port) {
  const settingsPath = path.join(os.homedir(), '.claude', 'settings.json')
  let settings = {}
  try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')) } catch {}
  if (!settings.mcpServers) settings.mcpServers = {}
  if (settings.mcpServers['codepocket-phone']) return
  settings.mcpServers['codepocket-phone'] = {
    type: 'sse',
    url: `http://127.0.0.1:${port}/sse`,
  }
  fs.mkdirSync(path.dirname(settingsPath), { recursive: true })
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2))
}

// ── Helpers ──

function findHubBin() {
  const bundledBin = path.join(__dirname, '..', 'vendor', process.platform === 'win32' ? 'cpkt-hub.exe' : 'cpkt-hub')
  if (fs.existsSync(bundledBin)) return bundledBin
  try { const r = execSync('which cpkt-hub 2>/dev/null').toString().trim(); if (r) return r } catch {}
  return null
}

function isHubRunning(hubUrl) {
  try {
    return execSync(`curl -s -o /dev/null -w "%{http_code}" ${hubUrl}/ 2>/dev/null`).toString().trim() === '200'
  } catch { return false }
}

// ── Parse args ──

const args = process.argv.slice(2)

// 'test' subcommand — standalone
if (args[0] === 'test') {
  runTestCli(args.slice(1))
  return
}

// ── Background mode helpers ──

const PID_FILE = path.join(CONFIG_DIR, 'codepocket.pid')
const LOG_DIR = path.join(CONFIG_DIR, 'logs')

function getLogFile() {
  const d = new Date()
  const ts = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
  return path.join(LOG_DIR, `${ts}.log`)
}

function checkRunning() {
  try {
    const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim())
    if (pid && process.kill(pid, 0)) return pid
  } catch {}
  try { fs.unlinkSync(PID_FILE) } catch {}
  return 0
}

// 'stop' subcommand
if (args[0] === 'stop') {
  const pid = checkRunning()
  if (!pid) {
    console.log('[codepocket] Not running')
    process.exit(0)
  }
  try { process.kill(pid, 'SIGTERM') } catch {}
  try { fs.unlinkSync(PID_FILE) } catch {}
  console.log(`[codepocket] Stopped (PID ${pid})`)
  process.exit(0)
}

// 'logs' subcommand
if (args[0] === 'logs') {
  const logFile = getLogFile()
  if (!fs.existsSync(logFile)) {
    console.log('[codepocket] No log file for today')
    process.exit(1)
  }
  const lines = parseInt(args[1]) || 50
  try {
    const { execSync } = require('child_process')
    execSync(`tail -n ${lines} -f "${logFile}"`, { stdio: 'inherit' })
  } catch {}
  process.exit(0)
}

// 'status' subcommand
if (args[0] === 'status') {
  const pid = checkRunning()
  if (pid) {
    console.log(`[codepocket] Running (PID ${pid})`)
  } else {
    console.log('[codepocket] Not running')
  }
  process.exit(0)
}

// Determine mode
const CONNECT_ONLY = ['connect', 'c'].includes(args[0])
const filteredArgs = CONNECT_ONLY ? args.slice(1) : args

let hubUrl = process.env.CPKT_HUB_URL || process.env.HAPI_HUB_URL || 'http://127.0.0.1:3006'
const DEFAULT_RELAY_URL = 'wss://bkstory.cn/ws'
let relayUrl = process.env.RELAY_WS_URL || process.env.RELAY_URL || DEFAULT_RELAY_URL
let relayKey = process.env.RELAY_KEY || ''
let noHub = false

const passThroughArgs = []
for (let i = 0; i < filteredArgs.length; i++) {
  const arg = filteredArgs[i]
  if (arg.startsWith('--hub=')) hubUrl = arg.slice(6).replace(/\/+$/, '')
  else if (arg === '--hub' && filteredArgs[i + 1]) { hubUrl = filteredArgs[++i].replace(/\/+$/, '') }
  else if (arg.startsWith('--relay=')) relayUrl = arg.slice(8)
  else if (arg === '--relay' && filteredArgs[i + 1]) { relayUrl = filteredArgs[++i] }
  else if (arg.startsWith('--key=')) relayKey = arg.slice(6)
  else if (arg === '--key' && filteredArgs[i + 1]) { relayKey = filteredArgs[++i] }
  else if (arg === '--no-hub') noHub = true
  else passThroughArgs.push(arg)
}

// Load/merge config
const saved = loadConfig()
if (relayKey) {
  saveConfig({ ...saved, relayKey, relayUrl: relayUrl || saved.relayUrl || '' })
} else {
  relayKey = saved.relayKey || ''
}
if (!relayUrl) relayUrl = saved.relayUrl || ''

// ── Auto-daemonize: if not already a daemon, fork to background ──
if (!process.env.CODEPOCKET_DAEMON && relayKey) {
  const pid = checkRunning()
  if (pid) {
    console.log(`[codepocket] Already running (PID ${pid})`)
    process.exit(0)
  }
  fs.mkdirSync(LOG_DIR, { recursive: true })
  const logFile = getLogFile()
  const out = fs.openSync(logFile, 'a')
  const daemonArgs = process.argv.slice(2).filter(a => a !== '--key' && !a.startsWith('--key='))
  const child = spawn(process.argv[0], [process.argv[1], ...daemonArgs], {
    stdio: ['ignore', out, out],
    detached: true,
    env: { ...process.env, CODEPOCKET_DAEMON: '1' },
  })
  child.unref()
  fs.writeFileSync(PID_FILE, String(child.pid))
  console.log(`[codepocket] ✓ 已后台启动 (PID ${child.pid})`)
  console.log(`  日志: ${logFile}`)
  console.log(`  codepocket stop   停止`)
  console.log(`  codepocket logs   查看日志`)
  process.exit(0)
}

// ── State ──

let jwt = ''
let relayWs = null
let relayPingInterval = null
let hubBin = findHubBin()

// ── Hub ──

function startHub(hubBin) {
  return new Promise((resolve) => {
    if (noHub) { resolve(); return }
    if (isHubRunning(hubUrl)) { resolve(); return }
    if (!hubBin) { resolve(); return }

    const hub = spawn(hubBin, ['hub', 'start'], {
      stdio: ['ignore', 'pipe', 'pipe'],
      detached: false,
      env: { ...process.env, ...HOME_ENV },
    })

    let started = false
    hub.stdout.on('data', () => {}) // swallow
    hub.stderr.on('data', (data) => {
      const text = data.toString()
      if (!started && (text.includes('listening') || text.includes('ready') || text.includes('started'))) {
        started = true
        resolve()
      }
    })

    setTimeout(() => {
      if (!started && isHubRunning(hubUrl)) { started = true }
      resolve()
    }, 8000)

    hub.on('exit', () => resolve())
  })
}

// ── Auth ──

async function findHubToken() {
  const hubHome = process.env.CODEPOCKET_HOME || HOME_ENV.CODEPOCKET_HOME
  const settingsPath = path.join(hubHome, 'settings.json')
  try {
    const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'))
    if (settings.cliApiToken) return settings.cliApiToken
  } catch {}
  if (process.env.CLI_API_TOKEN) return process.env.CLI_API_TOKEN
  throw new Error('找不到 Hub token')
}

async function authenticateHub(token) {
  const res = await fetch(`${hubUrl}/api/auth`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ accessToken: token }),
  })
  if (!res.ok) throw new Error(`Hub auth failed: ${res.status}`)
  return (await res.json()).token
}

// ── SSE ──

async function startSSE() {
  const url = `${hubUrl}/api/events?token=${encodeURIComponent(jwt)}&all=true`
  try {
    const res = await fetch(url, { headers: { Accept: 'text/event-stream' } })
    if (!res.ok || !res.body) { setTimeout(startSSE, 5000); return }
    const reader = res.body.getReader()
    const decoder = new TextDecoder()
    let buffer = ''
    async function pump() {
      while (true) {
        const { done, value } = await reader.read()
        if (done) break
        buffer += decoder.decode(value, { stream: true })
        const lines = buffer.split('\n')
        buffer = lines.pop() || ''
        for (const line of lines) {
          if (line.startsWith('data:')) {
            try { sendToRelay({ type: 'sse-event', event: JSON.parse(line.slice(5).trim()) }) } catch {}
          }
        }
      }
      setTimeout(startSSE, 5000)
    }
    pump().catch(() => { setTimeout(startSSE, 5000) })
  } catch { setTimeout(startSSE, 5000) }
}

// ── Relay ──

function connectRelay() {
  let savedSession = null
  try { savedSession = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8')) } catch {}

  if (relayPingInterval) { clearInterval(relayPingInterval); relayPingInterval = null }
  if (relayWs) { try { relayWs.removeAllListeners() } catch {} try { relayWs.close() } catch {} relayWs = null }

  relayWs = new WebSocket(relayUrl)

  relayWs.on('open', () => {
    relayWs.send(JSON.stringify({
      type: 'register',
      key: relayKey,
      sessionToken: savedSession?.sessionToken || undefined,
    }))
    relayPingInterval = setInterval(() => {
      if (relayWs?.readyState === WebSocket.OPEN) relayWs.ping()
    }, 30000)
  })

  relayWs.on('message', (raw) => {
    let msg
    try { msg = JSON.parse(raw.toString()) } catch { return }
    switch (msg.type) {
      case 'registered': {
        try {
          fs.mkdirSync(path.dirname(SESSION_FILE), { recursive: true })
          fs.writeFileSync(SESSION_FILE, JSON.stringify({ sessionToken: msg.sessionToken, hubUrl, relayUrl }))
        } catch {}
        if (msg.reconnected) {
          console.log('[codepocket] ✓ 已重新连接')
        } else {
          console.log('[codepocket] ✓ 已连接中继')
        }
        break
      }
      case 'client-connected': {
        console.log('[codepocket] ✓ 小程序已连接')
        break
      }
      case 'rest-request': {
        forwardToHub(msg.requestId, msg.method, msg.path, msg.body)
        break
      }
      case 'auth-request': {
        handleAuthRequest(msg.requestId, msg.accessToken)
        break
      }
      case 'register-error': {
        console.error(`[codepocket] ✗ ${msg.error}`)
        break
      }
      case 'phone-action-response': {
        onPhoneActionResponse(msg)
        break
      }
      case 'spawn-session': {
        handleSpawnSession(msg.requestId, msg.directory)
        break
      }
      case 'list-dir': {
        handleListDir(msg.requestId, msg.path)
        break
      }
    }
  })

  relayWs.on('close', () => {
    if (relayPingInterval) { clearInterval(relayPingInterval); relayPingInterval = null }
    relayWs = null
    setTimeout(connectRelay, 3000)
  })
  relayWs.on('error', () => { relayWs?.close() })
}

function sendToRelay(msg) {
  if (relayWs?.readyState === WebSocket.OPEN) relayWs.send(JSON.stringify(msg))
}

async function doForwardToHub(requestId, method, p, body) {
  const init = {
    method,
    headers: { 'Content-Type': 'application/json', ...(jwt ? { Authorization: `Bearer ${jwt}` } : {}) },
  }
  if (body && method !== 'GET') init.body = JSON.stringify(body)
  return fetch(`${hubUrl}${p}`, init)
}

async function forwardToHub(requestId, method, p, body) {
  try {
    let res = await doForwardToHub(requestId, method, p, body)
    if (res.status === 401) {
      try {
        const token = await findHubToken()
        jwt = await authenticateHub(token)
        res = await doForwardToHub(requestId, method, p, body)
      } catch {}
    }
    const data = await res.json().catch(() => ({}))
    sendToRelay({ type: 'rest-response', requestId, status: res.status, data })
  } catch (err) {
    sendToRelay({ type: 'rest-response', requestId, status: 502, data: { error: err.message } })
  }
}

async function handleAuthRequest(requestId, accessToken) {
  try {
    const res = await fetch(`${hubUrl}/api/auth`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ accessToken }),
    })
    sendToRelay({ type: 'auth-response', requestId, ok: res.ok, data: await res.json() })
  } catch (err) {
    sendToRelay({ type: 'auth-response', requestId, ok: false, data: { error: err.message } })
  }
}

async function handleSpawnSession(requestId, directory) {
  if (!hubBin) {
    sendToRelay({ type: 'spawn-session-result', requestId, success: false, error: 'Hub binary not found' })
    return
  }
  if (!directory || typeof directory !== 'string') {
    sendToRelay({ type: 'spawn-session-result', requestId, success: false, error: 'Directory is required' })
    return
  }

  // Expand ~ to home directory
  directory = directory.replace(/^~/, os.homedir())

  console.log(`[codepocket] Spawning session in: ${directory}`)

  try {
    const child = spawn(hubBin, ['--path', directory], {
      stdio: ['ignore', 'pipe', 'pipe'],
      detached: true,
      env: { ...process.env, ...HOME_ENV },
    })
    child.unref()

    // Wait for session to be created by polling /sessions
    let sessionId = null
    for (let i = 0; i < 30; i++) {
      await new Promise(r => setTimeout(r, 1000))
      try {
        const res = await fetch(`${hubUrl}/sessions`, {
          headers: jwt ? { Authorization: `Bearer ${jwt}` } : {},
        })
        const sessions = await res.json()
        // Find the newest session with matching path
        const match = (Array.isArray(sessions) ? sessions : []).find(s =>
          s.metadata?.path === directory && s.id
        )
        if (match) {
          sessionId = match.id
          break
        }
      } catch {}
    }

    if (sessionId) {
      console.log(`[codepocket] Session created: ${sessionId}`)
      sendToRelay({ type: 'spawn-session-result', requestId, success: true, sessionId })
    } else {
      // Timeout but process started — might still be initializing
      sendToRelay({ type: 'spawn-session-result', requestId, success: true, sessionId: null, warning: 'Session initializing' })
    }
  } catch (err) {
    console.error(`[codepocket] Spawn failed: ${err.message}`)
    sendToRelay({ type: 'spawn-session-result', requestId, success: false, error: err.message })
  }
}

function handleListDir(requestId, dirPath) {
  try {
    const resolved = (dirPath || '~').replace(/^~/, os.homedir())
    const abs = path.resolve(resolved)
    const entries = fs.readdirSync(abs, { withFileTypes: true })
      .filter(d => d.isDirectory())
      .map(d => ({ name: d.name, path: path.join(abs, d.name) }))
      .sort((a, b) => a.name.localeCompare(b.name))
    sendToRelay({ type: 'list-dir-result', requestId, path: abs, entries })
  } catch (err) {
    sendToRelay({ type: 'list-dir-result', requestId, error: err.message })
  }
}

// ── Background connect: auth + MCP + SSE + relay ──

async function connectInBackground() {
  if (!relayUrl) {
    console.log('[codepocket] 未配置 Relay 地址，手机连接功能不可用')
    return
  }
  try {
    const token = await findHubToken()
    jwt = await authenticateHub(token)
  } catch {}

  try {
    const mcpPort = await startMcpServer(sendToRelay)
    injectMcpConfig(mcpPort)
  } catch {}

  startSSE()
  connectRelay()
}

// ── Main ──

async function run() {

  // 1. Key setup (interactive if first time)
  if (!relayKey) {
    console.log('[codepocket] 首次使用，请输入 Key')
    console.log('  在 CodePocket 小程序中获取 Key →')
    relayKey = await new Promise((resolve) => {
      const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
      rl.question('  Key: ', (answer) => {
        rl.close()
        resolve(answer.trim())
      })
    })
    if (!relayKey) {
      console.error('[codepocket] ✗ Key 不能为空')
      process.exit(1)
    }
  }

  // 1.5 Relay URL (default: wss://bkstory.cn/ws)
  if (!relayUrl) {
    relayUrl = DEFAULT_RELAY_URL
  }

  if (relayKey) {
    saveConfig({ ...loadConfig(), relayKey, relayUrl })
    console.log('[codepocket] ✓ 配置已保存')
  }

  // 2. Start hub (quiet, wait for ready)
  if (!noHub) {
    await startHub(hubBin)
  }

  // 3. Connect relay (auth + MCP + SSE + relay)
  connectInBackground()

  console.log('[codepocket] ✓ 已连接，等待小程序...')
}

run().catch((err) => {
  console.error('[codepocket] ✗ 启动失败:', err.message)
  process.exit(1)
})
