/**
 * @zync/ai — public entry point — system-ai.
 * callAI() is the single entry point for all AI calls across Zync modules.
 */
import { getGlobalConfig, getModelPricing } from '@zync/db/queries'
import type { Db } from '@zync/db/queries'
import type { AIMessage, AIRequest, AIResponse } from './adapter'
import type { AIUseCase } from './use-cases'
import type { ModelConfig, AISettings } from './executor'
import { executeWithFallback } from './executor'
import { assembleSystemPrompt } from './prompt'
import { withCreditAccounting, type AICtx } from './accounting'

// ── Re-exports of public surface ─────────────────────────────────────────────

export { executeWithFallback, withTimeout } from './executor'
export type { ModelConfig, AISettings } from './executor'

export { AIProviderError, ExtraSpendLimitError, QuotaExceededError } from './errors'

export type { AIMessage, AIContentBlock, AIRequest, AIResponse, AIAdapter } from './adapter'
export type { AIUseCase } from './use-cases'
export { AI_USE_CASES, AI_USE_CASE_LABELS } from './use-cases'

export { getAdapter } from './adapters/index'
export type { AIEnv } from './adapters/index'

// ── ai-assistant: RAG + chat re-exports ─────────────────────────────────────
export { embedText, retrieveContext, enqueueIndexUpsert, enqueueIndexDelete, buildCustomerIndexText, buildInvoiceIndexText, buildTaskIndexText, buildProjectIndexText, buildExpenseIndexText, buildKbArticleIndexText, vectorIdFor } from './rag/index'
export type { IndexableEntity, IndexableEntityType, AiIndexUpdateJob } from './rag/index'
export { streamAssistantTurn, runAssistantTurn, buildAssistantSystemPrompt } from './chat/index'
export type { StreamAssistantArgs, StreamAssistantResult, RunAssistantArgs } from './chat/index'

export { assembleSystemPrompt } from './prompt'
export { calculateCost, usdToTokens } from './pricing'
export { getQuotaStatus, isExtraUsageAllowed, checkExtraSpendLimit } from './accounting'
export type { QuotaStatus, AICtx } from './accounting'

// ── callAI ────────────────────────────────────────────────────────────────────

export interface CallAIOptions {
  tenantId: string
  userId?: string
  useCase: AIUseCase
  messages: AIMessage[]
  maxTokens?: number
  entityType?: string
  entityId?: string
  /** Tenant's subscription tier (from session). Required for quota checks. */
  tier: string
}

/**
 * Primary AI call function. Assembles system prompt, resolves model chain,
 * executes with fallback and credit accounting.
 *
 * For use cases requiring vision (expense_ocr, invoice_extract), models without
 * is_vision_capable are filtered from the fallback chain when image blocks are present.
 */
export async function callAI(ctx: AICtx, opts: CallAIOptions): Promise<AIResponse> {
  const db = ctx.db

  // Load global config and build AISettings
  const config = await getGlobalConfig(db)

  // Check if the request has image content blocks (vision required)
  const hasImages = opts.messages.some((m) => {
    if (typeof m.content === 'string') return false
    return m.content.some((b) => b.type === 'image')
  })

  const visionRequired =
    hasImages && (opts.useCase === 'expense_ocr' || opts.useCase === 'invoice_extract')

  // Filter model chain for vision capability if needed
  let mainModel = config.mainModel
  let backupModels = config.backupModels

  if (visionRequired) {
    // Verify main model supports vision
    const mainPricing = await getModelPricing(db, mainModel.model)
    if (!mainPricing?.isVisionCapable) {
      // Find first vision-capable model from backups
      const visionBackups: ModelConfig[] = []
      let newMain: ModelConfig | null = null

      for (const m of backupModels) {
        const pricing = await getModelPricing(db, m.model)
        if (pricing?.isVisionCapable) {
          if (!newMain) {
            newMain = m
          } else {
            visionBackups.push(m)
          }
        }
      }

      if (!newMain) {
        throw new Error('No vision-capable model available for this use case')
      }
      mainModel = newMain
      backupModels = visionBackups
    } else {
      // Filter backups to vision-capable only
      const visionBackups: ModelConfig[] = []
      for (const m of backupModels) {
        const pricing = await getModelPricing(db, m.model)
        if (pricing?.isVisionCapable) visionBackups.push(m)
      }
      backupModels = visionBackups
    }
  }

  const settings: AISettings = { mainModel, backupModels }

  // Assemble system prompt
  const systemPrompt = await assembleSystemPrompt(db, opts.tenantId, opts.useCase)

  // Prepend system message
  const messagesWithSystem: AIMessage[] = systemPrompt
    ? [{ role: 'system' as const, content: systemPrompt }, ...opts.messages]
    : [...opts.messages]

  const request: AIRequest = {
    messages: messagesWithSystem,
    model: mainModel.model, // executor will override per-model
    ...(opts.maxTokens !== undefined ? { maxTokens: opts.maxTokens } : {}),
  }

  // Execute with fallback, wrapped in credit accounting
  return withCreditAccounting(
    ctx,
    opts.tenantId,
    opts.useCase,
    opts.tier,
    () => executeWithFallback(request, settings, ctx.env),
    {
      userId: opts.userId,
      entityType: opts.entityType,
      entityId: opts.entityId,
    },
  )
}
