import { spawn } from 'child_process'
import { DB } from '../db.js'
import {
  AGENTRUN_CODE,
  isAgentRunReady,
  missingAgentRunConfig,
  rowToAgentRunProvider,
  sanitizeAgentRunPatch,
  secretPreview,
} from './agentrun-config.js'
import {
  isQoderCloudAgentReady,
  missingQoderCloudAgentConfig,
  rowToQoderCloudAgentRuntime,
  sanitizeQoderCloudAgentPatch,
  QODER_CLOUD_AGENT_PROTOCOL,
} from './qoder-cloud-agent.js'
import { mapCodexJsonEvent, parseJsonLine } from './runtime-events.js'

export const LOCAL_CLI_PROTOCOL = 'local-cli'
export const PURPOSES = ['create', 'evaluate', 'run', 'flowchart', 'api']

function safeJsonArray(value, fallback = []) {
  if (Array.isArray(value)) return value.map(String).filter(Boolean)
  try {
    const parsed = JSON.parse(String(value || ''))
    return Array.isArray(parsed) ? parsed.map(String).filter(Boolean) : fallback
  } catch {
    return fallback
  }
}

function safeJsonObject(value, fallback = {}) {
  if (value && typeof value === 'object' && !Array.isArray(value)) return value
  try {
    const parsed = JSON.parse(String(value || ''))
    return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : fallback
  } catch {
    return fallback
  }
}

function stringifyList(list) {
  return JSON.stringify((Array.isArray(list) ? list : []).map(String).map(s => s.trim()).filter(Boolean))
}

function stringifyObject(obj) {
  const clean = {}
  for (const [key, value] of Object.entries(obj || {})) {
    const k = String(key || '').trim()
    if (!k) continue
    clean[k] = String(value ?? '')
  }
  return JSON.stringify(clean)
}

export function isRuntimeReady(row = {}) {
  if (row.protocol === 'agentrun') return isAgentRunReady(row)
  if (row.protocol === QODER_CLOUD_AGENT_PROTOCOL) return isQoderCloudAgentReady(row)
  if (row.protocol === LOCAL_CLI_PROTOCOL) return !!row.enabled && !!String(row.local_command || '').trim()
  return !!row.enabled
}

export function missingRuntimeConfig(row = {}) {
  if (row.protocol === 'agentrun') return missingAgentRunConfig(row)
  if (row.protocol === QODER_CLOUD_AGENT_PROTOCOL) return missingQoderCloudAgentConfig(row)
  if (row.protocol === LOCAL_CLI_PROTOCOL) {
    const missing = []
    if (!String(row.local_command || '').trim()) missing.push('local_command')
    return missing
  }
  return []
}

export function rowToRuntime(row = {}) {
  if (row.protocol === 'agentrun') return rowToAgentRunProvider(row)
  if (row.protocol === QODER_CLOUD_AGENT_PROTOCOL) return rowToQoderCloudAgentRuntime(row)
  const command = String(row.local_command || '').trim()
  const args = safeJsonArray(row.local_args_json, [])
  const healthcheckArgs = safeJsonArray(row.local_healthcheck_args_json, ['--version'])
  const env = safeJsonObject(row.local_env_json, {})
  const purposeScopes = safeJsonArray(row.purpose_scopes_json, ['create', 'evaluate', 'run'])
  return {
    code: row.code,
    name: row.name,
    icon: row.icon || '⌘',
    protocol: row.protocol,
    enabled: !!row.enabled,
    isActive: !!row.is_active,
    hasKey: false,
    keyPreview: null,
    command,
    args,
    argsText: args.join(' '),
    healthcheckArgs,
    healthcheckArgsText: healthcheckArgs.join(' '),
    workingDir: row.local_working_dir || '',
    env,
    envText: Object.entries(env).map(([k, v]) => `${k}=${v}`).join('\n'),
    purposeScopes,
    timeoutMs: Number(row.timeout_ms || 300000),
    notes: row.notes || '',
    missing: missingRuntimeConfig(row),
    ready: isRuntimeReady(row),
  }
}

export function rowToAgent(row = {}) {
  return {
    id: row.id,
    name: row.name || '',
    description: row.description || '',
    prompt: row.prompt || '',
    systemSkillKind: row.system_skill_kind || '',
    skillId: row.skill_id || '',
    skillName: row.skill_name || '',
    runtimeCode: row.runtime_code || '',
    appliesTo: safeJsonArray(row.applies_to_json, []),
    enabled: !!row.enabled,
    updatedAt: row.updated_at || null,
  }
}

export async function listRuntimeRows() {
  const { results } = await DB.prepare(
    "SELECT * FROM runtime_engines ORDER BY CASE code WHEN 'agentrun' THEN 1 WHEN 'qoder-cloud-agent' THEN 2 WHEN 'local-codex' THEN 3 WHEN 'local-qoder' THEN 4 WHEN 'local-workbuddy' THEN 5 WHEN 'local-trae' THEN 6 ELSE 50 END, name"
  ).all()
  return results || []
}

export async function getRuntimeRow(code) {
  return await DB.prepare('SELECT * FROM runtime_engines WHERE code = ?').bind(code).first()
}

export async function getActiveRuntimeRow(purpose = '') {
  const scoped = String(purpose || '').trim()
  const rows = await listRuntimeRows()
  const enabled = rows.filter(row => row.enabled)
  const byScope = scoped
    ? enabled.find(row => safeJsonArray(row.purpose_scopes_json, []).includes(scoped) && row.is_active)
      || enabled.find(row => safeJsonArray(row.purpose_scopes_json, []).includes(scoped) && isRuntimeReady(row))
    : null
  return byScope || enabled.find(row => row.is_active && isRuntimeReady(row)) || null
}

export async function getActiveRuntimeAgent(purpose = '') {
  let results = []
  try {
    const res = await DB.prepare('SELECT * FROM runtime_agents WHERE enabled = 1 ORDER BY updated_at DESC').all()
    results = res.results || []
  } catch (e) {
    if (!/no such table/i.test(e?.message || '')) throw e
  }
  const scoped = String(purpose || '').trim()
  const rows = results
  return rows.find(row => safeJsonArray(row.applies_to_json, []).includes(scoped)) || null
}

export async function resolveRuntimeForPurpose(purpose = '') {
  const agent = await getActiveRuntimeAgent(purpose)
  const runtime = agent?.runtime_code ? await getRuntimeRow(agent.runtime_code) : null
  return {
    agent,
    runtime: runtime && runtime.enabled ? runtime : await getActiveRuntimeRow(purpose),
  }
}

export async function updateRuntime(code, body = {}) {
  const existing = await getRuntimeRow(code)
  if (!existing) return { error: '运行时不存在', status: 404 }

  if (existing.protocol === 'agentrun') {
    const { patch, merged } = sanitizeAgentRunPatch(body, existing)
    if (body.enabled === true && missingAgentRunConfig(merged).length > 0) {
      return { error: `启用 AgentRun 前，请补齐：${missingAgentRunConfig(merged).join(', ')}`, status: 422 }
    }
    const keys = Object.keys(patch)
    if (keys.length) {
      const sets = keys.map(k => `${k} = ?`)
      const vals = keys.map(k => patch[k])
      sets.push('updated_at = ?')
      vals.push(Math.floor(Date.now() / 1000), code)
      await DB.prepare(`UPDATE runtime_engines SET ${sets.join(', ')} WHERE code = ?`).bind(...vals).run()
    }
    return { ok: true, runtime: rowToRuntime(await getRuntimeRow(code)) }
  }

  if (existing.protocol === QODER_CLOUD_AGENT_PROTOCOL) {
    const { patch, merged } = sanitizeQoderCloudAgentPatch(body, existing)
    if (body.enabled === true && missingQoderCloudAgentConfig(merged).length > 0) {
      return { error: `启用 QODER Cloud Agent 前，请补齐：${missingQoderCloudAgentConfig(merged).join(', ')}`, status: 422 }
    }
    const keys = Object.keys(patch)
    if (keys.length) {
      const sets = keys.map(k => `${k} = ?`)
      const vals = keys.map(k => patch[k])
      sets.push('updated_at = ?')
      vals.push(Math.floor(Date.now() / 1000), code)
      await DB.prepare(`UPDATE runtime_engines SET ${sets.join(', ')} WHERE code = ?`).bind(...vals).run()
    }
    return { ok: true, runtime: rowToRuntime(await getRuntimeRow(code)) }
  }

  const sets = []
  const vals = []
  const copy = (field, column) => {
    if (typeof body[field] === 'string') {
      sets.push(`${column} = ?`)
      vals.push(body[field].trim())
    }
  }
  copy('name', 'name')
  copy('icon', 'icon')
  copy('command', 'local_command')
  copy('workingDir', 'local_working_dir')
  copy('notes', 'notes')
  if (Array.isArray(body.args)) {
    sets.push('local_args_json = ?')
    vals.push(stringifyList(body.args))
  }
  if (typeof body.argsText === 'string') {
    sets.push('local_args_json = ?')
    vals.push(stringifyList(splitArgsText(body.argsText)))
  }
  if (Array.isArray(body.healthcheckArgs)) {
    sets.push('local_healthcheck_args_json = ?')
    vals.push(stringifyList(body.healthcheckArgs))
  }
  if (typeof body.healthcheckArgsText === 'string') {
    sets.push('local_healthcheck_args_json = ?')
    vals.push(stringifyList(splitArgsText(body.healthcheckArgsText)))
  }
  if (Array.isArray(body.purposeScopes)) {
    sets.push('purpose_scopes_json = ?')
    vals.push(stringifyList(body.purposeScopes.filter(p => PURPOSES.includes(p))))
  }
  if (typeof body.envText === 'string') {
    sets.push('local_env_json = ?')
    vals.push(stringifyObject(parseEnvText(body.envText)))
  }
  if (body.env && typeof body.env === 'object') {
    sets.push('local_env_json = ?')
    vals.push(stringifyObject(body.env))
  }
  if (typeof body.timeoutMs === 'number' || typeof body.timeoutMs === 'string') {
    sets.push('timeout_ms = ?')
    vals.push(Math.max(5000, Math.min(60 * 60 * 1000, Number(body.timeoutMs) || 300000)))
  }
  if (typeof body.enabled === 'boolean') {
    sets.push('enabled = ?')
    vals.push(body.enabled ? 1 : 0)
  }
  if (!sets.length) return { ok: true, runtime: rowToRuntime(existing) }
  sets.push('updated_at = ?')
  vals.push(Math.floor(Date.now() / 1000), code)
  await DB.prepare(`UPDATE runtime_engines SET ${sets.join(', ')} WHERE code = ?`).bind(...vals).run()
  return { ok: true, runtime: rowToRuntime(await getRuntimeRow(code)) }
}

export async function setActiveRuntime(code) {
  const row = await getRuntimeRow(code)
  if (!row) return { error: '运行时不存在', status: 404 }
  if (!isRuntimeReady({ ...row, enabled: 1 })) {
    return { error: `请先补齐运行时配置：${missingRuntimeConfig(row).join(', ')}`, status: 400 }
  }
  await DB.prepare('UPDATE runtime_engines SET is_active = 0').run()
  await DB.prepare('UPDATE runtime_engines SET is_active = 1, enabled = 1, updated_at = ? WHERE code = ?')
    .bind(Math.floor(Date.now() / 1000), code).run()
  return { ok: true }
}

export async function listAgents() {
  try {
    const { results } = await DB.prepare(`
      SELECT a.*, s.name AS skill_name
      FROM runtime_agents a
      LEFT JOIN skills s ON s.id = a.skill_id
      ORDER BY CASE a.id WHEN 'agent-skill-creator' THEN 1 WHEN 'agent-skill-evaluator' THEN 2 WHEN 'agent-skill-runner' THEN 3 ELSE 50 END, a.name
    `).all()
    return (results || []).map(rowToAgent)
  } catch (e) {
    if (/no such table: skills/i.test(e?.message || '')) {
      const { results } = await DB.prepare("SELECT * FROM runtime_agents ORDER BY CASE id WHEN 'agent-skill-creator' THEN 1 WHEN 'agent-skill-evaluator' THEN 2 WHEN 'agent-skill-runner' THEN 3 ELSE 50 END, name").all()
      return (results || []).map(rowToAgent)
    }
    if (/no such table/i.test(e?.message || '')) return []
    throw e
  }
}

export async function saveAgent(id, body = {}) {
  const agentId = String(id || '').replace(/[^a-z0-9_-]/gi, '').slice(0, 80) || `agent-${Date.now().toString(36)}`
  const existing = await DB.prepare('SELECT * FROM runtime_agents WHERE id = ?').bind(agentId).first()
  const pickString = (camel, snake, fallback = '') => {
    if (typeof body[camel] === 'string') return body[camel].trim()
    if (snake && typeof body[snake] === 'string') return body[snake].trim()
    return String(fallback || '').trim()
  }
  const agent = {
    id: agentId,
    name: pickString('name', null, existing?.name || '运行智能体') || '运行智能体',
    description: pickString('description', null, existing?.description || ''),
    prompt: pickString('prompt', null, existing?.prompt || ''),
    system_skill_kind: pickString('systemSkillKind', 'system_skill_kind', existing?.system_skill_kind || '') || null,
    skill_id: pickString('skillId', 'skill_id', existing?.skill_id || '') || null,
    runtime_code: pickString('runtimeCode', 'runtime_code', existing?.runtime_code || '') || null,
    applies_to_json: stringifyList(Array.isArray(body.appliesTo) ? body.appliesTo.filter(p => PURPOSES.includes(p)) : safeJsonArray(existing?.applies_to_json, [])),
    enabled: typeof body.enabled === 'boolean' ? (body.enabled ? 1 : 0) : (existing?.enabled ?? 1),
    updated_at: Math.floor(Date.now() / 1000),
  }
  await DB.prepare(`
    INSERT INTO runtime_agents (id, name, description, prompt, system_skill_kind, skill_id, runtime_code, applies_to_json, enabled, updated_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    ON CONFLICT(id) DO UPDATE SET
      name = excluded.name,
      description = excluded.description,
      prompt = excluded.prompt,
      system_skill_kind = excluded.system_skill_kind,
      skill_id = excluded.skill_id,
      runtime_code = excluded.runtime_code,
      applies_to_json = excluded.applies_to_json,
      enabled = excluded.enabled,
      updated_at = excluded.updated_at
  `).bind(
    agent.id,
    agent.name,
    agent.description,
    agent.prompt,
    agent.system_skill_kind,
    agent.skill_id,
    agent.runtime_code,
    agent.applies_to_json,
    agent.enabled,
    agent.updated_at,
  ).run()
  try {
    return rowToAgent(await DB.prepare(`
      SELECT a.*, s.name AS skill_name
      FROM runtime_agents a
      LEFT JOIN skills s ON s.id = a.skill_id
      WHERE a.id = ?
    `).bind(agentId).first())
  } catch (e) {
    if (!/no such table: skills/i.test(e?.message || '')) throw e
    return rowToAgent(await DB.prepare('SELECT * FROM runtime_agents WHERE id = ?').bind(agentId).first())
  }
}

export function splitArgsText(text = '') {
  const args = []
  const re = /"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|(\S+)/g
  let match
  while ((match = re.exec(String(text || '')))) {
    args.push((match[1] || match[2] || match[3] || '').replace(/\\(["'])/g, '$1'))
  }
  return args
}

export function parseEnvText(text = '') {
  const env = {}
  for (const line of String(text || '').split(/\r?\n/)) {
    const trimmed = line.trim()
    if (!trimmed || trimmed.startsWith('#')) continue
    const idx = trimmed.indexOf('=')
    if (idx <= 0) continue
    env[trimmed.slice(0, idx).trim()] = trimmed.slice(idx + 1)
  }
  return env
}

function renderArgTemplate(args = [], { workdir, prompt } = {}) {
  return args.map(arg => String(arg)
    .replace(/\{workdir\}/g, workdir || '')
    .replace(/\{prompt\}/g, prompt || ''))
}

export function buildLocalRuntimePrompt({ systemPrompt, chatMessages, mode, agentPrompt }) {
  const messages = (chatMessages || []).map(m => `${m.role === 'assistant' ? 'Assistant' : 'User'}: ${m.content}`).join('\n\n')
  return [
    '你正在作为 AIPROS 平台的本地 AI 运行时执行任务。',
    `运行场景：${mode || 'run'}`,
    '',
    agentPrompt ? `## 智能体提示词\n${agentPrompt}` : '',
    systemPrompt ? `## 系统/Skill 提示词\n${systemPrompt}` : '',
    '## 对话输入',
    messages,
    '',
    '根据对话直接工作。信息不足时可以先追问；信息足够时继续执行。可按需生成文件到当前工作目录。',
  ].filter(Boolean).join('\n')
}

export function prepareLocalRuntimeArgs({ runtimeRow = {}, args = [] } = {}) {
  const command = String(runtimeRow.local_command || '')
  const executable = command.split(/[\\/]/).pop()?.toLowerCase() || ''
  const isCodex = runtimeRow.code === 'local-codex' || executable === 'codex' || executable === 'codex.exe'
  const next = [...args]
  if (!isCodex || next.includes('--json')) return next
  const execIndex = next.indexOf('exec')
  if (execIndex >= 0) next.splice(execIndex + 1, 0, '--json')
  return next
}

export function normalizeLocalRuntimeDiagnostic(value = '') {
  const ignored = [
    /codex_core_skills::loader:/i,
    /codex_core::session::session: failed to load skill/i,
    /skill descriptions were shortened to fit the .*skills context budget/i,
  ]
  return String(value || '')
    .split(/\r?\n/)
    .filter(line => line.trim() && !ignored.some(pattern => pattern.test(line)))
    .join('\n')
    .trim()
}

function createAsyncEventQueue() {
  const values = []
  const waiters = []
  let ended = false
  let endError = null
  return {
    push(value) {
      if (ended) return
      const waiter = waiters.shift()
      if (waiter) waiter.resolve({ value, done: false })
      else values.push(value)
    },
    end(error = null) {
      if (ended) return
      ended = true
      endError = error
      while (waiters.length) {
        const waiter = waiters.shift()
        if (endError) waiter.reject(endError)
        else waiter.resolve({ value: undefined, done: true })
      }
    },
    next() {
      if (values.length) return Promise.resolve({ value: values.shift(), done: false })
      if (ended) return endError ? Promise.reject(endError) : Promise.resolve({ value: undefined, done: true })
      return new Promise((resolve, reject) => waiters.push({ resolve, reject }))
    },
  }
}

export async function* streamLocalRuntime({ runtimeRow, prompt, workdir, signal, timeoutMs }) {
  const command = String(runtimeRow.local_command || '').trim()
  if (!command) throw new Error('本地运行时缺少命令')
  const renderedArgs = renderArgTemplate(safeJsonArray(runtimeRow.local_args_json, []), { workdir, prompt })
  const args = prepareLocalRuntimeArgs({ runtimeRow, args: renderedArgs })
  const env = { ...process.env, ...safeJsonObject(runtimeRow.local_env_json, {}) }
  const cwd = runtimeRow.local_working_dir || workdir || process.cwd()
  const timeout = Math.max(5000, Math.min(60 * 60 * 1000, Number(timeoutMs || runtimeRow.timeout_ms || 300000)))
  const jsonMode = args.includes('--json')
  const queue = createAsyncEventQueue()
  const child = spawn(command, args, { cwd, env, stdio: ['pipe', 'pipe', 'pipe'], shell: false })
  let jsonBuffer = ''
  let closed = false

  const emitJsonLine = (line) => {
    const parsed = parseJsonLine(line)
    if (!parsed) {
      const diagnostic = String(line || '').trim()
      if (diagnostic) queue.push({ t: 'warning', v: diagnostic })
      return
    }
    for (const event of mapCodexJsonEvent(parsed)) queue.push(event)
  }

  child.stdout.on('data', chunk => {
    const text = chunk.toString()
    if (!jsonMode) {
      if (text) queue.push({ t: 'delta', v: text })
      return
    }
    jsonBuffer += text
    let newline
    while ((newline = jsonBuffer.indexOf('\n')) >= 0) {
      const line = jsonBuffer.slice(0, newline).replace(/\r$/, '')
      jsonBuffer = jsonBuffer.slice(newline + 1)
      emitJsonLine(line)
    }
  })
  child.stderr.on('data', chunk => {
    const text = normalizeLocalRuntimeDiagnostic(chunk.toString())
    if (text) queue.push({ t: 'warning', v: text })
  })

  const timer = setTimeout(() => {
    try { child.kill('SIGTERM') } catch {}
    queue.end(new Error(`本地运行时超时：${timeout}ms`))
  }, timeout)
  timer.unref?.()

  const abort = () => {
    try { child.kill('SIGTERM') } catch {}
    queue.end(new Error('本地运行时已取消'))
  }
  if (signal) {
    if (signal.aborted) abort()
    else signal.addEventListener('abort', abort, { once: true })
  }

  child.on('error', error => {
    clearTimeout(timer)
    queue.end(error)
  })
  child.on('close', code => {
    closed = true
    clearTimeout(timer)
    if (jsonMode && jsonBuffer.trim()) emitJsonLine(jsonBuffer)
    jsonBuffer = ''
    if (code === 0) queue.end()
    else queue.end(new Error(`本地运行时退出码 ${code}`))
  })
  child.stdin.end(prompt)

  try {
    while (true) {
      const item = await queue.next()
      if (item.done) break
      yield item.value
    }
  } finally {
    clearTimeout(timer)
    if (signal) signal.removeEventListener?.('abort', abort)
    if (!closed) {
      try { child.kill('SIGTERM') } catch {}
    }
  }
}

export async function runLocalRuntime(args) {
  let stdout = ''
  let stderr = ''
  for await (const event of streamLocalRuntime(args)) {
    if (event?.t === 'delta') stdout += event.v || ''
    if (event?.t === 'warning') stderr += event.v || ''
    if (event?.t === 'error') throw new Error(event.v || '本地运行时失败')
  }
  return { stdout, stderr, code: 0 }
}

export async function testLocalRuntime(row) {
  const command = String(row.local_command || '').trim()
  if (!command) return { ok: false, message: '未填写本地命令' }
  const args = safeJsonArray(row.local_healthcheck_args_json, ['--version'])
  const t0 = Date.now()
  try {
    const result = await new Promise((resolve, reject) => {
      const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], shell: false })
      let stdout = ''
      let stderr = ''
      const timer = setTimeout(() => {
        try { child.kill('SIGTERM') } catch {}
        reject(new Error('健康检查超时'))
      }, 15000)
      timer.unref?.()
      child.stdout.on('data', chunk => { stdout += chunk.toString() })
      child.stderr.on('data', chunk => { stderr += chunk.toString() })
      child.on('error', reject)
      child.on('close', code => {
        clearTimeout(timer)
        code === 0 ? resolve({ stdout, stderr }) : reject(new Error(stderr || stdout || `退出码 ${code}`))
      })
    })
    return {
      ok: true,
      model: row.name,
      latencyMs: Date.now() - t0,
      content: String(result.stdout || result.stderr || '命令可执行').trim().slice(0, 120),
    }
  } catch (e) {
    return { ok: false, message: e?.message || String(e) }
  }
}

export { secretPreview }
