import { Hono } from 'hono'
import { authJwt, type JwtContext } from '../middleware/authJwt.js'
import { supabaseAdmin } from '../lib/db.js'
import { aes256Decrypt } from '../lib/crypto.js'
import { calculateCost } from '../lib/cost.js'
import { interpolate, inferProvider, buildOpenAIBody } from '../lib/playground-runner.js'
import { startInternalTrace } from '../lib/internal-tracing.js'
import { ApiError } from '../lib/errors.js'

export const promptsPlaygroundRouter = new Hono<JwtContext>()

promptsPlaygroundRouter.use('*', authJwt)

// Simple in-memory rate limit: 20 req per user per 60s
const rateLimitMap = new Map<string, { count: number; resetAt: number }>()
const RATE_LIMIT = 20
const RATE_WINDOW_MS = 60_000

function checkRateLimit(userId: string): boolean {
  const now = Date.now()
  const entry = rateLimitMap.get(userId)
  if (!entry || now > entry.resetAt) {
    rateLimitMap.set(userId, { count: 1, resetAt: now + RATE_WINDOW_MS })
    return true
  }
  if (entry.count >= RATE_LIMIT) return false
  entry.count++
  return true
}

promptsPlaygroundRouter.post('/run', async (c) => {
  const orgId = c.get('orgId')
  const userId = c.get('userId')
  if (!orgId) throw new ApiError('NOT_FOUND', 'Organization not found')

  if (userId && !checkRateLimit(userId)) {
    throw new ApiError('RATE_LIMIT', 'Rate limit exceeded. Try again in a minute.')
  }

  let body: {
    promptVersionId?: unknown
    variables?: unknown
    model?: unknown
    temperature?: unknown
    maxTokens?: unknown
    providerKeyId?: unknown
  }
  try {
    body = (await c.req.json()) as typeof body
  } catch {
    throw new ApiError('INVALID_JSON_BODY', 'Invalid JSON body')
  }

  const promptVersionId = typeof body.promptVersionId === 'string' ? body.promptVersionId.trim() : ''
  if (!promptVersionId) throw new ApiError('VALIDATION_FAILED', 'promptVersionId is required')

  const providerKeyId = typeof body.providerKeyId === 'string' ? body.providerKeyId.trim() : ''
  if (!providerKeyId) throw new ApiError('VALIDATION_FAILED', 'providerKeyId is required')

  const model = typeof body.model === 'string' ? body.model.trim() : ''
  if (!model) throw new ApiError('VALIDATION_FAILED', 'model is required')

  const temperature = typeof body.temperature === 'number'
    ? Math.min(2, Math.max(0, body.temperature))
    : 0.7
  const maxTokens = typeof body.maxTokens === 'number'
    ? Math.min(8192, Math.max(1, Math.round(body.maxTokens)))
    : 1024
  const variables = (body.variables && typeof body.variables === 'object' && !Array.isArray(body.variables))
    ? (body.variables as Record<string, string>)
    : {}

  // Fetch prompt version (scoped to org)
  const { data: pv, error: pvErr } = await supabaseAdmin
    .from('prompt_versions')
    .select('id, content')
    .eq('id', promptVersionId)
    .eq('organization_id', orgId)
    .maybeSingle()

  if (pvErr || !pv) throw new ApiError('NOT_FOUND', 'Prompt version not found')

  // Fetch the specified provider key (scoped to org, must be active)
  const { data: pkRow, error: pkErr } = await supabaseAdmin
    .from('provider_keys')
    .select('id, provider, encrypted_key')
    .eq('id', providerKeyId)
    .eq('organization_id', orgId)
    .eq('is_active', true)
    .maybeSingle()

  if (pkErr || !pkRow) {
    throw new ApiError('BAD_REQUEST', 'Provider key not found or inactive')
  }

  // Validate key provider matches model
  const modelProvider = inferProvider(model)
  if (pkRow.provider !== modelProvider) {
    return c.json({
      error: `Model "${model}" requires a ${modelProvider} key, but selected key is ${pkRow.provider}.`,
    }, 400)
  }

  const plaintext = await aes256Decrypt(pkRow.encrypted_key as string)
  if (!plaintext) {
    throw new ApiError('INTERNAL_ERROR', 'Failed to decrypt provider key. Check your ENCRYPTION_KEY.')
  }

  const { result: interpolated, missingVars } = interpolate(pv.content, variables)

  // 4B.2b — dogfood ourselves. Each playground run posts a
  // `playground_call` trace + a single `llm` span to the spanlens-team
  // workspace so we can audit our own model/temperature/maxTokens
  // choices alongside customer traffic.
  const internalTrace = startInternalTrace('playground_call', {
    organization_id: orgId,
    prompt_version_id: promptVersionId,
    model,
    provider: modelProvider,
    temperature,
    max_tokens: maxTokens,
  })
  const span = internalTrace.startSpan('llm', {
    spanType: 'llm',
    metadata: { model, provider: modelProvider },
  })

  const startMs = Date.now()

  if (modelProvider === 'openai') {
    const upstreamRes = await fetch('https://api.openai.com/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${plaintext}`,
      },
      body: JSON.stringify(buildOpenAIBody(
        model,
        [{ role: 'user', content: interpolated }],
        { temperature, maxTokens },
      )),
    })

    const latencyMs = Date.now() - startMs

    if (!upstreamRes.ok) {
      const text = await upstreamRes.text().catch(() => '')
      span.end({ status: 'error', errorMessage: `OpenAI ${upstreamRes.status}` })
      internalTrace.end({ status: 'error', errorMessage: `OpenAI ${upstreamRes.status}` })
      throw new ApiError('UPSTREAM_FAILED', `OpenAI error: ${text}`, { upstreamStatus: upstreamRes.status })
    }

    const json = await upstreamRes.json() as {
      choices: Array<{ message: { content: string } }>
      usage: { prompt_tokens: number; completion_tokens: number }
      model: string
    }

    const responseText = json.choices?.[0]?.message?.content ?? ''
    const promptTokens = json.usage?.prompt_tokens ?? 0
    const completionTokens = json.usage?.completion_tokens ?? 0
    const resolvedModel = json.model ?? model
    const cost = calculateCost('openai', resolvedModel, { promptTokens, completionTokens })

    span.end({
      status: 'completed',
      ...(cost?.totalCost != null ? { costUsd: cost.totalCost } : {}),
      promptTokens,
      completionTokens,
      totalTokens: promptTokens + completionTokens,
      metadata: { resolved_model: resolvedModel, latency_ms: latencyMs },
    })
    internalTrace.end({
      status: 'completed',
      metadata: { latency_ms: latencyMs, cost_usd: cost?.totalCost ?? null },
    })

    return c.json({
      success: true,
      data: {
        responseText,
        model: resolvedModel,
        promptTokens,
        completionTokens,
        totalTokens: promptTokens + completionTokens,
        costUsd: cost?.totalCost ?? null,
        latencyMs,
        missingVars,
      },
    })
  }

  // Anthropic
  const upstreamRes = await fetch('https://api.anthropic.com/v1/messages', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': plaintext,
      'anthropic-version': '2023-06-01',
    },
    body: JSON.stringify({
      model,
      max_tokens: maxTokens,
      messages: [{ role: 'user', content: interpolated }],
      temperature,
    }),
  })

  const latencyMs = Date.now() - startMs

  if (!upstreamRes.ok) {
    const text = await upstreamRes.text().catch(() => '')
    span.end({ status: 'error', errorMessage: `Anthropic ${upstreamRes.status}` })
    internalTrace.end({ status: 'error', errorMessage: `Anthropic ${upstreamRes.status}` })
    throw new ApiError('UPSTREAM_FAILED', `Anthropic error: ${text}`, { upstreamStatus: upstreamRes.status })
  }

  const json = await upstreamRes.json() as {
    content: Array<{ type: string; text: string }>
    usage: { input_tokens: number; output_tokens: number }
    model: string
  }

  const responseText = json.content?.find((b) => b.type === 'text')?.text ?? ''
  const promptTokens = json.usage?.input_tokens ?? 0
  const completionTokens = json.usage?.output_tokens ?? 0
  const resolvedModel = json.model ?? model
  const cost = calculateCost('anthropic', resolvedModel, { promptTokens, completionTokens })

  span.end({
    status: 'completed',
    ...(cost?.totalCost != null ? { costUsd: cost.totalCost } : {}),
    promptTokens,
    completionTokens,
    totalTokens: promptTokens + completionTokens,
    metadata: { resolved_model: resolvedModel, latency_ms: latencyMs },
  })
  internalTrace.end({
    status: 'completed',
    metadata: { latency_ms: latencyMs, cost_usd: cost?.totalCost ?? null },
  })

  return c.json({
    success: true,
    data: {
      responseText,
      model: resolvedModel,
      promptTokens,
      completionTokens,
      totalTokens: promptTokens + completionTokens,
      costUsd: cost?.totalCost ?? null,
      latencyMs,
      missingVars,
    },
  })
})
