import { Hono } from 'hono'
import { DB } from '../db.js'
import path from 'path'
import fs from 'fs'
import { fileURLToPath } from 'url'
import {
  isAgentRunReady,
  missingAgentRunConfig,
} from '../runtime/agentrun-config.js'
import {
  buildLocalRuntimePrompt,
  missingRuntimeConfig,
  resolveRuntimeForPurpose,
  rowToRuntime,
  runLocalRuntime,
} from '../runtime/runtime-registry.js'
import {
  buildAgentRunChatCompletionsRequest,
  extractOpenAIChunkText,
  isAgentRunIntegrationUrl,
} from '../runtime/agentrun-openai.js'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const UPLOADS_ROOT = process.env.UPLOADS_DIR || path.join(__dirname, '../../uploads')
const OUTPUTS_DIR = path.resolve(process.env.OUTPUTS_DIR || './outputs')

const app = new Hono()

function newSessionId() {
  return globalThis.crypto?.randomUUID?.() || (Date.now().toString(36) + Math.random().toString(36).slice(2, 10))
}

function resolveSessionDir(sessionId) {
  const safeId = String(sessionId || '').replace(/[^a-z0-9_-]/gi, '')
  return safeId ? path.resolve(OUTPUTS_DIR, `invoke-${safeId}`) : null
}

function isInside(parentDir, fp) {
  const resolvedParent = path.resolve(parentDir)
  const resolvedChild = path.resolve(fp)
  return resolvedChild === resolvedParent || resolvedChild.startsWith(resolvedParent + path.sep)
}

async function pickActiveAgentRunRow() {
  return await DB.prepare(
    "SELECT * FROM runtime_engines WHERE code = 'agentrun' AND enabled = 1 AND is_active = 1 LIMIT 1"
  ).first() || null
}

function stringifyInput(body) {
  if (typeof body?.input === 'string') return body.input
  if (body?.input !== undefined) {
    try { return JSON.stringify(body.input, null, 2) } catch { return String(body.input) }
  }
  try { return JSON.stringify(body || {}, null, 2) } catch { return String(body || '') }
}

async function loadSkillMarkdown(skill, sessionDir) {
  let content = `# ${skill.name || '未命名 Skill'}\n\n${skill.description || '（无描述）'}`
  const key = String(skill.package_key || '').trim()
  if (!key || !sessionDir) return content

  try {
    const zipPath = path.resolve(UPLOADS_ROOT, path.normalize(key))
    const uploadsResolved = path.resolve(UPLOADS_ROOT)
    if (!zipPath.startsWith(uploadsResolved + path.sep)) return content

    const zipBuf = await fs.promises.readFile(zipPath)
    const JSZip = (await import('jszip')).default
    const zip = await JSZip.loadAsync(zipBuf)

    await fs.promises.mkdir(sessionDir, { recursive: true })
    const mdEntries = []
    for (const [relativePath, zipEntry] of Object.entries(zip.files)) {
      if (zipEntry.dir || relativePath.startsWith('__MACOSX/')) continue
      const safePath = relativePath.replace(/\\/g, '/').replace(/^\/+/, '')
      if (!safePath || safePath.includes('..') || safePath.split('/').some(seg => !seg || seg.startsWith('.'))) continue
      const fp = path.join(sessionDir, safePath)
      if (!isInside(sessionDir, fp)) continue
      await fs.promises.mkdir(path.dirname(fp), { recursive: true })
      const fileBuf = await zipEntry.async('nodebuffer')
      await fs.promises.writeFile(fp, fileBuf)
      if (/\.(md|markdown)$/i.test(safePath)) mdEntries.push({ path: safePath, entry: zipEntry })
    }

    if (mdEntries.length) {
      const score = (p) => {
        const base = p.split('/').pop().toLowerCase()
        if (base === 'skill.md') return 0
        if (base === 'skill.markdown') return 1
        if (base === 'readme.md') return 2
        return 3
      }
      mdEntries.sort((a, b) => score(a.path) - score(b.path) || a.path.length - b.path.length)
      content = await mdEntries[0].entry.async('string')
    }
  } catch (e) {
    console.warn('[invoke] package load failed:', e?.message || e)
  }
  return content
}

async function listSessionFiles(sessionDir) {
  const out = []
  try {
    async function walk(dir, prefix = '') {
      const entries = await fs.promises.readdir(dir, { withFileTypes: true })
      for (const e of entries) {
        const rel = prefix ? `${prefix}/${e.name}` : e.name
        if (e.isDirectory()) await walk(path.join(dir, e.name), rel)
        else if (e.isFile()) out.push(rel)
      }
    }
    await walk(sessionDir)
  } catch {}
  return out
}

async function runAgentRunInvoke({ skill, body, agentRunRow }) {
  if (!isAgentRunReady(agentRunRow)) {
    throw new Error(`AgentRun 配置未就绪：${missingAgentRunConfig(agentRunRow).join(', ')}`)
  }

  const sessionId = newSessionId()
  const sessionDir = resolveSessionDir(sessionId)
  await fs.promises.mkdir(sessionDir, { recursive: true })
  const systemPrompt = await loadSkillMarkdown(skill, sessionDir)
  const userInput = stringifyInput(body)

  if (isAgentRunIntegrationUrl(agentRunRow.endpoint_public_url)) {
    const { url, options } = buildAgentRunChatCompletionsRequest({
      endpointPublicUrl: agentRunRow.endpoint_public_url,
      invocationKey: agentRunRow.api_key,
      sessionId,
      messages: [
        { role: 'system', content: systemPrompt },
        { role: 'user', content: `请严格按照当前 Skill 执行下面的用户输入，并只输出最终可交付结果。\n\n## 用户输入\n${userInput}` },
      ],
      stream: false,
    })
    const resp = await fetch(url, options)
    const data = await resp.json().catch(async () => ({ raw: await resp.text().catch(() => '') }))
    if (!resp.ok) {
      throw new Error(data?.error?.message || data?.message || data?.raw || `AgentRun HTTP ${resp.status}`)
    }
    return {
      output: extractOpenAIChunkText(data).trim(),
      session_id: sessionId,
      files: await listSessionFiles(sessionDir),
    }
  }

  const { AgentRuntimeClient, Config } = await import('@agentrun/sdk')
  const config = new Config({
    accessKeyId: agentRunRow.access_key_id,
    accessKeySecret: agentRunRow.access_key_secret,
    securityToken: agentRunRow.security_token || undefined,
    accountId: agentRunRow.account_id,
    regionId: agentRunRow.region_id || 'cn-hangzhou',
    readTimeout: 300000,
    timeout: 300000,
  })
  const client = new AgentRuntimeClient(config)
  const resp = await client.invokeOpenai({
    agentRuntimeName: agentRunRow.runtime_name,
    agentRuntimeEndpointName: agentRunRow.endpoint_name || 'default',
    messages: [
      { role: 'system', content: systemPrompt },
      { role: 'user', content: `请严格按照当前 Skill 执行下面的用户输入，并只输出最终可交付结果。\n\n## 用户输入\n${userInput}` },
    ],
    stream: false,
  })
  const content = resp?.choices?.[0]?.message?.content || ''
  return {
    output: String(content).trim(),
    session_id: sessionId,
    files: await listSessionFiles(sessionDir),
  }
}

async function runLocalInvoke({ skill, body, runtimeRow, agent }) {
  const sessionId = newSessionId()
  const sessionDir = resolveSessionDir(sessionId)
  await fs.promises.mkdir(sessionDir, { recursive: true })
  const systemPrompt = await loadSkillMarkdown(skill, sessionDir)
  const userInput = stringifyInput(body)
  const prompt = buildLocalRuntimePrompt({
    systemPrompt,
    agentPrompt: agent?.prompt || '',
    mode: 'api',
    chatMessages: [{ role: 'user', content: `请严格按照当前 Skill 执行下面的用户输入，并只输出最终可交付结果。\n\n## 用户输入\n${userInput}` }],
  })
  const result = await runLocalRuntime({
    runtimeRow,
    prompt,
    workdir: sessionDir,
    timeoutMs: runtimeRow.timeout_ms,
  })
  return {
    output: String(result.stdout || '').trim(),
    session_id: sessionId,
    files: await listSessionFiles(sessionDir),
  }
}

async function logInvocation({ skill, apiKey, c, body, output, status, latency, errorMessage = null }) {
  const callerIp = c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown'
  try {
    await DB.prepare(`
      INSERT INTO skill_invocations (skill_id, skill_name, api_key_id, caller_ip, request_input, response_output, response_status, latency_ms, error_message)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).bind(
      skill.id, skill.name, apiKey.id, callerIp,
      JSON.stringify(body), JSON.stringify(output), status, latency, errorMessage
    ).run()
  } catch (e) {
    console.error('[invocation-log-failed]', e.message)
  }
}

// POST /v1/skills/:skillId/invoke
app.post('/:skillId/invoke', async (c) => {
  const skillId = c.req.param('skillId')
  const apiKey = c.get('apiKey')
  const usageToday = c.get('apiUsageToday') || 0

  if (apiKey.skill_id !== skillId) {
    return c.json({ error: 'forbidden', message: '该 API Key 无权访问此 Skill' }, 403)
  }

  const skill = await DB.prepare(
    'SELECT id, name, description, status, node_path, package_key FROM skills WHERE id = ?'
  ).bind(skillId).first()

  if (!skill) return c.json({ error: 'not_found', message: 'Skill 不存在' }, 404)
  if (skill.status !== 'published') {
    return c.json({ error: 'unavailable', message: 'Skill 未发布，无法调用' }, 403)
  }

  const startTime = Date.now()
  const body = await c.req.json().catch(() => ({}))
  const { agent, runtime } = await resolveRuntimeForPurpose('run')
  if (!runtime) {
    const latency = Date.now() - startTime
    const error = { error: 'engine_not_configured', message: '平台未启用运行时，Skill API 调用无法执行' }
    await logInvocation({ skill, apiKey, c, body, output: error, status: 'error', latency, errorMessage: error.message })
    return c.json(error, 503)
  }

  try {
    const missing = missingRuntimeConfig(runtime)
    if (missing.length) {
      const latency = Date.now() - startTime
      const error = { error: 'engine_not_configured', message: `运行时配置未就绪：${missing.join(', ')}` }
      await logInvocation({ skill, apiKey, c, body, output: error, status: 'error', latency, errorMessage: error.message })
      return c.json(error, 503)
    }

    const result = runtime.protocol === 'local-cli'
      ? await runLocalInvoke({ skill, body, runtimeRow: runtime, agent })
      : await runAgentRunInvoke({ skill, body, agentRunRow: runtime })
    const latency = Date.now() - startTime
    const output = {
      skill_id: skill.id,
      skill_name: skill.name,
      node_path: skill.node_path,
      provider: runtime.protocol === 'local-cli'
        ? { code: runtime.code, model: runtime.local_command, protocol: runtime.protocol }
        : { code: 'agentrun', model: runtime.endpoint_name || 'default', protocol: runtime.protocol },
      runtime: rowToRuntime(runtime),
      ...result,
    }
    await logInvocation({ skill, apiKey, c, body, output, status: 'success', latency })

    return c.json({
      status: 'success',
      data: output,
      usage: {
        calls_today: usageToday + 1,
        daily_limit: apiKey.daily_limit,
        calls_remaining: apiKey.daily_limit - usageToday - 1,
        latency_ms: latency,
      },
    })
  } catch (e) {
    const latency = Date.now() - startTime
    const message = e?.message || String(e)
    const error = { error: 'skill_invoke_failed', message }
    await logInvocation({ skill, apiKey, c, body, output: error, status: 'error', latency, errorMessage: message })
    return c.json(error, /auth|token|login|credential|accesskey|signature/i.test(message) ? 503 : 502)
  }
})

export default app
