/**
 * KB Vectorize indexing — embed article content for semantic search and AI RAG.
 *
 * Namespace: tenant:{tenantId}  (shared with ai-assistant — same namespace,
 * metadata filter source='kb' scopes KB-specific searches without isolation cost).
 *
 * Vector ID: 'kb:' + article.id
 *
 * Called from article create/update routes via executionCtx.waitUntil (non-blocking).
 */
import type { Env } from '@zync/types'
import type { KbArticle } from '@zync/types'

// ── Text extraction ───────────────────────────────────────────────────────────

/**
 * Flatten a Tiptap v2 JSON document to plain text.
 * Recursively visits all nodes and collects `text` leaf node values.
 * Headings are included (they're just paragraph-ish nodes with text children).
 */
export function extractKbPlainText(content: unknown): string {
  if (!content || typeof content !== 'object') return ''

  const parts: string[] = []

  function visit(node: unknown): void {
    if (!node || typeof node !== 'object') return
    const n = node as Record<string, unknown>

    // Leaf text node
    if (typeof n['text'] === 'string') {
      parts.push(n['text'])
      return
    }

    // Visit children recursively
    if (Array.isArray(n['content'])) {
      for (const child of n['content']) {
        visit(child)
      }
      parts.push('\n')
    }
  }

  visit(content)
  return parts.join('').replace(/\n{3,}/g, '\n\n').trim()
}

// ── Upsert vector ─────────────────────────────────────────────────────────────

/**
 * Embed an article's text content and upsert into the tenant's Vectorize namespace.
 * Should be called via c.executionCtx.waitUntil() to avoid blocking responses.
 */
export async function upsertKbVector(
  env: Env,
  args: { tenantId: string; article: KbArticle },
): Promise<void> {
  const { tenantId, article } = args

  const text = extractKbPlainText(article.content)
  if (!text.trim()) return // Nothing to embed

  // Embed via Workers AI
  const embeddingResult = await env.AI.run('@cf/baai/bge-small-en-v1.5' as Parameters<typeof env.AI.run>[0], {
    text: [text.slice(0, 4096)], // Limit to 4096 chars for embedding model
  } as Parameters<typeof env.AI.run>[1])

  const values =
    (embeddingResult as { data?: number[][] }).data?.[0] ??
    (embeddingResult as { result?: { data?: number[][] } }).result?.data?.[0]

  if (!values || !Array.isArray(values)) return

  await env.VECTORIZE.upsert([
    {
      id: `kb:${article.id}`,
      namespace: `tenant:${tenantId}`,
      values: values as number[],
      metadata: {
        source: 'kb',
        articleId: article.id,
        spaceId: article.spaceId,
        tenantId,
      },
    },
  ])
}

// ── Delete vector ─────────────────────────────────────────────────────────────

/**
 * Remove an article's vector from Vectorize.
 * Called on article delete or space delete (iterate article IDs first).
 */
export async function deleteKbVector(
  env: Env,
  tenantId: string,
  articleId: string,
): Promise<void> {
  // Vectorize deleteByIds — namespace is baked into the vector ID prefix
  await env.VECTORIZE.deleteByIds([`kb:${articleId}`])
}
