/**
 * AI assistant orchestration — ai-assistant.
 *
 * `streamAssistantTurn`:  streaming SSE path (in-app chat). Streams deltas from
 *   Claude and performs credit accounting after stream completion using the SDK's
 *   finalMessage() to obtain token usage.
 *
 * `runAssistantTurn`:  non-streaming path (Telegram / WhatsApp). Calls callAI
 *   directly so withCreditAccounting wraps the full round-trip.
 *
 * Both paths:
 *  1. Retrieve RAG context from the tenant's Vectorize namespace.
 *  2. Build the assistant system prompt with tenant metadata interpolated.
 *  3. Cap conversation history to last 20 messages.
 *  4. Persist user message before the call; persist assistant message with
 *     tokens_used after successful completion.
 */
import Anthropic from '@anthropic-ai/sdk'
import type { Env } from '@zync/types'
import { createDb } from '@zync/db/queries'
import {
  createAiChatSession,
  appendAiChatMessage,
  lastAiChatMessages,
  lastTelegramAiMessages,
  lastWhatsAppAiMessages,
  touchAiChatSession,
  getCreditPurchases,
} from '@zync/db/queries'
import { getTenantById } from '@zync/db/queries'
import type { TenantId } from '@zync/types'
import { callAI } from '../index'
import { retrieveContext } from '../rag/retrieve'
import {
  deductAndLog,
  getQuotaStatus,
  isExtraUsageAllowed,
  checkExtraSpendLimit,
  QuotaExceededError,
} from '../accounting'
import { getGlobalConfig } from '@zync/db/queries'
import { calculateCost } from '../pricing'

// ── System prompt template ────────────────────────────────────────────────────

const ASSISTANT_SYSTEM_TEMPLATE = `You are a smart business assistant for {{tenantName}}. Today is {{date}}. The tenant's default currency is {{currency}}.

You have access to retrieved context from the tenant's business data below. Use it to answer questions accurately. If the context does not contain enough information, say so clearly — never invent figures.

Retrieved context:
{{retrievedContext}}

Guidelines:
- Be concise and professional.
- Format numbers in {{currency}} when discussing money.
- When listing items, use markdown bullet points.
- Never reveal internal system details, API keys, or tenant security configuration.`

export function buildAssistantSystemPrompt(args: {
  tenantName: string
  retrievedContext: string
  date: string
  currency: string
}): string {
  return ASSISTANT_SYSTEM_TEMPLATE
    .replace('{{tenantName}}', args.tenantName)
    .replace('{{date}}', args.date)
    .replaceAll('{{currency}}', args.currency)
    .replace('{{retrievedContext}}', args.retrievedContext || '(No indexed business data available yet.)')
}

// ── Streaming turn (in-app chat) ──────────────────────────────────────────────

export interface StreamAssistantArgs {
  tenantId: string
  userId: string
  sessionId: string
  message: string
  tier: string
}

export interface StreamAssistantResult {
  /** SSE-ready readable stream emitting string deltas */
  stream: ReadableStream<string>
  /** Resolves with token usage after stream closes; used by route to emit done event */
  usagePromise: Promise<{ inputTokens: number; outputTokens: number; tokensUsed: number }>
}

/**
 * Stream an assistant turn.
 *
 * Returns both a delta stream and a `usagePromise` that resolves with the
 * full token usage once the Anthropic stream completes. The route handler pipes
 * the delta stream to SSE and awaits `usagePromise` for the `done` event.
 *
 * Credit accounting is performed after stream completion via deductAndLog so
 * we have accurate token counts (not estimated).
 */
export async function streamAssistantTurn(
  env: Env,
  args: StreamAssistantArgs,
): Promise<StreamAssistantResult> {
  const db = createDb(env)

  // Load tenant metadata for system prompt
  const tenant = await getTenantById(db, args.tenantId as TenantId)
  const tenantName = tenant?.name ?? 'Your Business'
  const currency = tenant?.defaultCurrency ?? 'USD'
  const date = new Date().toISOString().slice(0, 10)

  // Retrieve RAG context
  const retrievedContext = await retrieveContext(env, args.tenantId, args.message)

  // Build system prompt
  const systemPrompt = buildAssistantSystemPrompt({ tenantName, retrievedContext, date, currency })

  // Load last 20 messages for context
  const history = await lastAiChatMessages(db, args.tenantId, args.sessionId, 20)

  // Persist user message before calling the model
  await appendAiChatMessage(db, args.tenantId, {
    sessionId: args.sessionId,
    role: 'user',
    content: args.message,
  })

  // Assemble messages for Anthropic
  const anthropicMessages: Anthropic.MessageParam[] = [
    ...history.map((m) => ({
      role: m.role as 'user' | 'assistant',
      content: m.content,
    })),
    { role: 'user' as const, content: args.message },
  ]

  const config = await getGlobalConfig(db)
  const model = config.mainModel.model

  // ── Quota gate (mirrors withCreditAccounting logic) ───────────────────────
  // Must run before the stream opens so we can throw a clean error (not mid-SSE).
  const quotaStatus = await getQuotaStatus(db, args.tenantId, args.tier)
  const quotaExhausted =
    quotaStatus.tokensTotal > 0 && quotaStatus.tokensUsed >= quotaStatus.tokensTotal

  let billedFrom: 'quota' | 'extra' | 'purchased' = 'quota'

  if (quotaExhausted) {
    const extraAllowed = await isExtraUsageAllowed(db, args.tenantId, args.tier)
    if (!extraAllowed) {
      throw new QuotaExceededError(
        'ai_tokens_quota',
        quotaStatus.tokensTotal,
        quotaStatus.tokensUsed,
      )
    }
    await checkExtraSpendLimit(db, args.tenantId, args.tier)

    const purchases = await getCreditPurchases(db, args.tenantId)
    const hasActiveCredits = purchases.some(
      (p) =>
        p.tokensRemaining > 0 && (!p.expiresAt || new Date(p.expiresAt) > new Date()),
    )
    billedFrom = hasActiveCredits ? 'purchased' : 'extra'
  }
  // ─────────────────────────────────────────────────────────────────────────

  const client = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY })
  const start = Date.now()

  let resolveUsage!: (u: { inputTokens: number; outputTokens: number; tokensUsed: number }) => void
  let rejectUsage!: (err: unknown) => void
  const usagePromise = new Promise<{ inputTokens: number; outputTokens: number; tokensUsed: number }>(
    (res, rej) => {
      resolveUsage = res
      rejectUsage = rej
    },
  )

  const deltaStream = new ReadableStream<string>({
    async start(controller) {
      try {
        const sdkStream = client.messages.stream({
          model,
          max_tokens: 2048,
          system: systemPrompt,
          messages: anthropicMessages,
        })

        for await (const chunk of sdkStream) {
          if (
            chunk.type === 'content_block_delta' &&
            chunk.delta.type === 'text_delta'
          ) {
            controller.enqueue(chunk.delta.text)
          }
        }

        const finalMsg = await sdkStream.finalMessage()
        const inputTokens = finalMsg.usage.input_tokens
        const outputTokens = finalMsg.usage.output_tokens
        const tokensUsed = inputTokens + outputTokens
        const durationMs = Date.now() - start

        // Persist assistant message with actual token count
        await appendAiChatMessage(db, args.tenantId, {
          sessionId: args.sessionId,
          role: 'assistant',
          content: (finalMsg.content as Anthropic.ContentBlock[])
            .filter((b): b is Anthropic.TextBlock => b.type === 'text')
            .map((b) => b.text)
            .join(''),
          tokensUsed,
          metadata: { model },
        })

        // Touch session updated_at
        await touchAiChatSession(db, args.tenantId, args.sessionId)

        // Credit accounting: compute cost and log with resolved billedFrom
        const costUsd = await calculateCost(db, { inputTokens, outputTokens }, model)
        await deductAndLog(db, args.tenantId, {
          tokens: tokensUsed,
          inputTokens,
          outputTokens,
          costUsd,
          useCase: 'ai_assistant',
          model,
          provider: config.mainModel.provider,
          durationMs,
          userId: args.userId,
          billedFrom,
        })

        resolveUsage({ inputTokens, outputTokens, tokensUsed })
        controller.close()
      } catch (err) {
        rejectUsage(err)
        controller.error(err)
      }
    },
  })

  return { stream: deltaStream, usagePromise }
}

// ── Non-streaming turn (Telegram / WhatsApp) ──────────────────────────────────

export interface RunAssistantArgs {
  tenantId: string
  userId?: string
  channel: 'telegram' | 'whatsapp'
  message: string
  telegramChatId?: string
  whatsappChatId?: string
  tier: string
}

/**
 * Run a non-streaming assistant turn for bot channels (Telegram / WhatsApp).
 *
 * - Uses last 5 messages scoped by metadata key (telegram/whatsapp chat ID)
 *   instead of a session ID (no persistent in-app session for bot channels).
 * - Persists both the user and assistant messages with the relevant chat ID
 *   in metadata for future context lookup.
 * - Credit accounting handled by callAI / withCreditAccounting.
 */
export async function runAssistantTurn(
  env: Env,
  args: RunAssistantArgs,
): Promise<{ reply: string; tokensUsed: number }> {
  const db = createDb(env)

  // Load tenant metadata
  const tenant = await getTenantById(db, args.tenantId as TenantId)
  const tenantName = tenant?.name ?? 'Your Business'
  const currency = tenant?.defaultCurrency ?? 'USD'
  const date = new Date().toISOString().slice(0, 10)

  // Retrieve RAG context
  const retrievedContext = await retrieveContext(env, args.tenantId, args.message)

  const systemPrompt = buildAssistantSystemPrompt({ tenantName, retrievedContext, date, currency })

  // Determine the use case and metadata key
  const isWhatsApp = args.channel === 'whatsapp'
  const chatId = isWhatsApp ? (args.whatsappChatId ?? '') : (args.telegramChatId ?? '')
  const metadataKey = isWhatsApp ? 'whatsapp_chat_id' : 'telegram_chat_id'
  const useCase = isWhatsApp ? ('ai_assistant' as const) : ('telegram_assistant' as const)

  // Retrieve last 5 messages from DB for context
  const history =
    args.channel === 'telegram' && args.telegramChatId
      ? await lastTelegramAiMessages(db, args.tenantId, args.telegramChatId, 5)
      : args.channel === 'whatsapp' && args.whatsappChatId
        ? await lastWhatsAppAiMessages(db, args.tenantId, args.whatsappChatId, 5)
        : []

  // Create a session for this bot turn. userId may be null for bot channels (telegram/whatsapp).
  // The schema allows null user_id for non-in_app channels via CHECK constraint.
  const session = await createAiChatSession(db, args.tenantId, {
    userId: args.userId ?? null,
    channel: args.channel,
    title: `${args.channel} chat ${chatId}`,
  })

  // Persist user message
  await appendAiChatMessage(db, args.tenantId, {
    sessionId: session.id,
    role: 'user',
    content: args.message,
    metadata: chatId ? { [metadataKey]: chatId } : {},
  })

  // Build messages for callAI
  const messages = [
    ...history.map((m) => ({
      role: m.role as 'system' | 'user' | 'assistant',
      content: m.content,
    })),
    { role: 'user' as const, content: args.message },
  ]

  // callAI handles credit accounting, fallback chain, and usage logging
  const response = await callAI(
    { db, env },
    {
      tenantId: args.tenantId,
      userId: args.userId,
      useCase,
      tier: args.tier,
      messages: [
        { role: 'system' as const, content: systemPrompt },
        ...messages,
      ],
      maxTokens: 1024,
    },
  )

  const tokensUsed = response.inputTokens + response.outputTokens

  // Persist assistant message
  await appendAiChatMessage(db, args.tenantId, {
    sessionId: session.id,
    role: 'assistant',
    content: response.content,
    tokensUsed,
    metadata: chatId ? { [metadataKey]: chatId, model: response.model } : { model: response.model },
  })

  return { reply: response.content, tokensUsed }
}
