/** Zod schemas for the on-disk question pack format (src/content/packs/<packKey>/...). */
import { z } from 'zod';

export const PROMPT_MAX = 160;
export const OPTION_MAX = 60;
export const EXPLANATION_MAX = 200;
export const MC_OPTION_COUNT = 4;
export const TRUE_FALSE_OPTIONS = ['True', 'False'] as const;
/** A board needs exactly 9 categories (engine CATEGORY_COUNT). */
export const PACK_CATEGORY_COUNT = 9;

const Slug = z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, 'Use lowercase letters, digits and single hyphens');

/** Lower-case, collapse whitespace: used for duplicate detection of prompts and options. */
export function normalizeText(text: string): string {
  return text.normalize('NFKC').toLowerCase().replace(/\s+/g, ' ').trim();
}

export const PackManifestSchema = z
  .object({
    key: Slug,
    name: z.string().trim().min(1).max(80),
    description: z.string().trim().max(300).default(''),
    kind: z.enum(['GENERAL', 'MUSIC']),
    categories: z
      .array(z.object({ key: Slug, label: z.string().trim().min(1).max(40) }))
      .length(PACK_CATEGORY_COUNT, `A pack needs exactly ${PACK_CATEGORY_COUNT} categories`),
  })
  .superRefine((pack, ctx) => {
    const seen = new Set<string>();
    pack.categories.forEach((c, i) => {
      if (seen.has(c.key)) ctx.addIssue({ code: 'custom', message: `Duplicate category "${c.key}"`, path: ['categories', i, 'key'] });
      seen.add(c.key);
    });
  });
export type PackManifest = z.infer<typeof PackManifestSchema>;

/** The envelope of a <categoryKey>.json file. Questions are validated one by one afterwards. */
export const QuestionFileSchema = z.object({
  pack: Slug,
  category: Slug,
  questions: z.array(z.unknown()).min(1, 'File has no questions'),
});

export const QUESTION_FIELDS = ['id', 'difficulty', 'type', 'prompt', 'options', 'answer', 'explanation', 'wildcard'] as const;

export const RawQuestionSchema = z
  .object({
    id: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*-\d{3,}$/, 'Use "<category>-NNN", e.g. "science-001"'),
    difficulty: z.enum(['STANDARD', 'DIFFICULT']),
    type: z.enum(['MULTIPLE_CHOICE', 'TRUE_FALSE']),
    prompt: z.string().trim().min(1).max(PROMPT_MAX),
    options: z.array(z.string().trim().min(1).max(OPTION_MAX)),
    answer: z.int().min(0),
    explanation: z.string().trim().min(1).max(EXPLANATION_MAX),
    wildcard: z.boolean(),
  })
  .superRefine((q, ctx) => {
    if (q.type === 'MULTIPLE_CHOICE' && q.options.length !== MC_OPTION_COUNT) {
      ctx.addIssue({ code: 'custom', path: ['options'], message: `MULTIPLE_CHOICE needs exactly ${MC_OPTION_COUNT} options` });
    }
    if (
      q.type === 'TRUE_FALSE' &&
      (q.options.length !== 2 || q.options[0] !== TRUE_FALSE_OPTIONS[0] || q.options[1] !== TRUE_FALSE_OPTIONS[1])
    ) {
      ctx.addIssue({ code: 'custom', path: ['options'], message: 'TRUE_FALSE options must be exactly ["True","False"]' });
    }
    if (q.answer >= q.options.length) {
      ctx.addIssue({ code: 'custom', path: ['answer'], message: `answer ${q.answer} is not a valid option index` });
    }
    const seen = new Set<string>();
    q.options.forEach((option, i) => {
      const key = normalizeText(option);
      if (seen.has(key)) ctx.addIssue({ code: 'custom', path: ['options', i], message: `Duplicate option "${option}"` });
      seen.add(key);
    });
  });
export type RawQuestion = z.infer<typeof RawQuestionSchema>;
