/**
 * KB Article Editor — Tiptap validator, SEO schemas, slug helpers, and constants.
 *
 * This file extends kb.ts (kb-module) with editor-specific types and validators.
 * Import-safe in Cloudflare Workers runtime (no DOM, no Node APIs).
 *
 * Tasks 2+3 of kb-article-editor spec.
 */
import { z } from 'zod'

// ── Constants ─────────────────────────────────────────────────────────────────

export const MAX_TREE_DEPTH = 3
export const MAX_KB_IMAGE_BYTES = 5 * 1024 * 1024 // 5 MB

/**
 * MIME types allowed for inline editor images (embedded in content JSONB).
 * SVG is intentionally excluded — XSS risk same-origin.
 * This is narrower than KB_ALLOWED_FILE_TYPES (attachments).
 */
export const KB_IMAGE_MIME_ALLOWLIST = [
  'image/jpeg',
  'image/png',
  'image/webp',
  'image/gif',
] as const

export type KbImageMimeType = (typeof KB_IMAGE_MIME_ALLOWLIST)[number]

// ── Tiptap node allowlist ─────────────────────────────────────────────────────

/**
 * Allowed Tiptap node/mark type names. Any other type is rejected by
 * validateTiptapContent to prevent unexpected node types being stored.
 */
export const ALLOWED_NODE_TYPES = new Set<string>([
  'doc',
  'paragraph',
  'heading',
  'text',
  'hardBreak',
  // marks (Tiptap emits these as node types in some versions; keep both)
  'bold',
  'italic',
  'underline',
  'strike',
  'link',
  'code',
  // lists
  'bulletList',
  'orderedList',
  'listItem',
  // block
  'blockquote',
  'codeBlock',
  // media
  'image',
  // table
  'table',
  'tableRow',
  'tableCell',
  'tableHeader',
])

// ── Tiptap JSON types ─────────────────────────────────────────────────────────

export type TiptapMark = {
  type: string
  attrs?: Record<string, unknown>
}

export type TiptapNode = {
  type: string
  attrs?: Record<string, unknown>
  marks?: TiptapMark[]
  content?: TiptapNode[]
  text?: string
}

export type TiptapDoc = {
  type: 'doc'
  content: TiptapNode[]
}

/** Alias: Tiptap v2 JSON document (content field in kb_articles). */
export type TiptapJSON = TiptapDoc

// ── Zod schemas for Tiptap content ───────────────────────────────────────────

export const tiptapMarkSchema: z.ZodType<TiptapMark> = z.object({
  type: z.string(),
  attrs: z.record(z.unknown()).optional(),
})

// Recursive schema using z.lazy
export const tiptapNodeSchema: z.ZodType<TiptapNode> = z.lazy(() =>
  z.object({
    type: z.string(),
    attrs: z.record(z.unknown()).optional(),
    marks: z.array(tiptapMarkSchema).optional(),
    content: z.array(tiptapNodeSchema).optional(),
    text: z.string().optional(),
  }),
)

export const tiptapDocSchema: z.ZodType<TiptapDoc> = z.object({
  type: z.literal('doc'),
  content: z.array(tiptapNodeSchema),
})

// ── Content validator ─────────────────────────────────────────────────────────

/**
 * Validates a Tiptap v2 JSONB document.
 *
 * 1. Validates the top-level { type: 'doc', content: TiptapNode[] } shape via Zod.
 * 2. Recursively walks every node and mark and throws if any type is not in
 *    ALLOWED_NODE_TYPES.
 * 3. Returns the typed TiptapDoc on success.
 *
 * Import-safe in CF Workers (no DOM/Node APIs).
 */
export function validateTiptapContent(content: unknown): TiptapDoc {
  // Step 1: structural shape check
  const parsed = tiptapDocSchema.parse(content)

  // Step 2: allowlist walk
  function walkNode(node: TiptapNode): void {
    if (!ALLOWED_NODE_TYPES.has(node.type)) {
      throw new Error(`Disallowed Tiptap node type: ${node.type}`)
    }
    if (node.marks) {
      for (const mark of node.marks) {
        if (!ALLOWED_NODE_TYPES.has(mark.type)) {
          throw new Error(`Disallowed Tiptap mark type: ${mark.type}`)
        }
      }
    }
    if (node.content) {
      for (const child of node.content) {
        walkNode(child)
      }
    }
  }

  for (const node of parsed.content) {
    walkNode(node)
  }

  return parsed
}

// ── SEO / metadata Zod schema ─────────────────────────────────────────────────

/**
 * Editor-only metadata fields (slug, parent, SEO).
 * Used for ArticleSettingsPanel writes.
 */
export const updateArticleMetaSchema = z.object({
  meta_title: z.string().max(200).nullable().optional(),
  meta_description: z.string().max(500).nullable().optional(),
  slug: z
    .string()
    .min(1)
    .max(200)
    .regex(/^[a-z0-9-]+$/, 'Slug must be lowercase kebab-case (a-z, 0-9, hyphens)')
    .optional(),
  parent_id: z.string().uuid().nullable().optional(),
})

export type UpdateArticleMetaInput = z.infer<typeof updateArticleMetaSchema>

/**
 * Combined editor PATCH body (kb-module updateArticleSchema fields +
 * editor metadata fields). Routes validate with this; content is further
 * validated via validateTiptapContent before write.
 */
export const editorPatchSchema = updateArticleMetaSchema.extend({
  title: z.string().min(1).max(200).optional(),
  content: z.unknown().optional(), // validated via validateTiptapContent
  position: z.number().optional(),
  status: z.enum(['DRAFT', 'PUBLISHED']).optional(),
})

export type EditorPatchBody = z.infer<typeof editorPatchSchema>

// ── Slug helpers ──────────────────────────────────────────────────────────────

/**
 * Convert an article title to a URL-safe kebab-case slug.
 * Normalizes unicode (NFKD), strips non-[a-z0-9] chars, collapses hyphens,
 * trims leading/trailing hyphens, lowercases, truncates to 200 chars.
 * Falls back to 'untitled' when the result would be empty.
 */
export function slugifyTitle(title: string): string {
  const s = title
    .toLowerCase()
    .normalize('NFKD')
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .slice(0, 200)
  return s || 'untitled'
}

/**
 * Produce a duplicate slug by appending '-copy'.
 * Used when duplicating an article.
 */
export function duplicateSlug(slug: string): string {
  return `${slug}-copy`
}

// ── Extended KbArticle type ───────────────────────────────────────────────────

/**
 * KbArticle extended with editor-specific SEO fields.
 * The base KbArticle (from kb.ts) is returned from all API routes;
 * these fields are nullable additions from the 0015 migration.
 */
export interface KbArticleWithMeta {
  id: string
  tenantId: string
  spaceId: string
  parentId: string | null
  title: string
  slug: string
  content: Record<string, unknown>
  status: 'DRAFT' | 'PUBLISHED'
  position: number
  viewCount: number
  publishedAt: string | null
  metaTitle: string | null
  metaDescription: string | null
  deletedAt: string | null
  createdBy: string
  updatedBy: string | null
  createdAt: string
  updatedAt: string
}
