import * as db from './db';
import { callLLM, cachedSystem } from './llm';
import { EMIT_QUIZ_QUESTIONS_TOOL } from './llmTools';
import { getCurrentWeekContent } from './curriculumService';
import { shuffle, sample } from './random';

const QUIZ_BATCH_TIMEOUT_MS = 25_000;

const QUIZ_SYSTEM = `You are a quiz generator for Respellion, an internal IT company learning platform.
You generate multiple-choice questions to test employee knowledge on specific topics.
Always write in clear, professional English.

Emit questions through the emit_quiz_questions tool. Each question has exactly four options; correctIndex is 0-based. Tag every question with difficulty ('easy', 'medium' or 'hard'). For a typical 5-question batch, mix difficulty roughly 2 easy / 2 medium / 1 hard; scale the ratio proportionally for larger batches.

Distribute correctIndex roughly evenly across 0, 1, 2, and 3. Do not place the correct answer at the same position more than 4 out of 10 times.

Never use filler options such as "all of the above", "none of the above", or "both A and B". Every explanation must be a substantive sentence (≥ 20 characters) describing why the correct answer is correct.`;

const BANNED_OPTION_PATTERNS = [
  /all of the above/i,
  /none of the above/i,
  /both a and b/i,
  /both b and c/i,
  /both c and d/i,
  /both a and c/i,
  /both b and d/i,
  /both a and d/i,
];

function normalizeQuestionText(text) {
  return String(text || '')
    .toLowerCase()
    .replace(/[\p{P}\p{S}]/gu, ' ')
    .replace(/\s+/g, ' ')
    .trim();
}

function dominantCorrectIndex(questions) {
  if (!questions.length) return null;
  const counts = [0, 0, 0, 0];
  for (const q of questions) counts[q.correctIndex] = (counts[q.correctIndex] || 0) + 1;
  const max = Math.max(...counts);
  return max / questions.length > 0.8 ? { index: counts.indexOf(max), ratio: max / questions.length } : null;
}

function validateBatchQuality(questions) {
  for (const q of questions) {
    const distinct = new Set(q.options.map((o) => o.trim().toLowerCase()));
    if (distinct.size < 4) {
      return `Question "${q.question}" has duplicate options.`;
    }
    for (const opt of q.options) {
      if (BANNED_OPTION_PATTERNS.some((re) => re.test(opt))) {
        return `Question "${q.question}" uses a banned filler option ("${opt}").`;
      }
    }
    if (!q.explanation || q.explanation.trim().length < 20) {
      return `Question "${q.question}" has an explanation that is too short.`;
    }
  }
  return null;
}

async function callQuizModel(topic, count) {
  const prompt = `Generate exactly ${count} multiple-choice quiz questions for this knowledge topic and emit them via the emit_quiz_questions tool:

Topic: ${topic.label}
Type: ${topic.type}
Description: ${topic.description}

Options must be prefixed "A) ", "B) ", "C) ", "D) ". Make questions specific and practical, not trivial. Ensure correct answers are evenly distributed.`;

  const result = await callLLM({
    task: 'quiz.generate',
    tier: 'standard',
    system: cachedSystem(QUIZ_SYSTEM),
    user: prompt,
    tools: [EMIT_QUIZ_QUESTIONS_TOOL],
    toolChoice: { type: 'tool', name: EMIT_QUIZ_QUESTIONS_TOOL.name },
    maxTokens: 4096,
  });

  const emitted = result.toolUses[0]?.input;
  if (!emitted) {
    throw new Error(`LLM did not emit tool output for ${topic.label}`);
  }

  // Handle different shapes the LLM might return:
  // 1. { questions: [...] }  — expected
  // 2. [...]                 — model returned array directly
  // 3. { questions: { ... } } — model wrapped single question in object
  let questions;
  if (Array.isArray(emitted.questions)) {
    questions = emitted.questions;
  } else if (Array.isArray(emitted)) {
    questions = emitted;
  } else if (emitted.questions && typeof emitted.questions === 'object') {
    // Single question wrapped as object instead of array
    questions = [emitted.questions];
  } else {
    throw new Error(`Unexpected quiz output shape for ${topic.label}`);
  }

  if (!questions.length) {
    throw new Error(`No questions generated for ${topic.label}`);
  }

  // Ensure each question has required fields (fill in defaults for missing ones)
  return questions.map((q, i) => ({
    id: q.id || `gen-${i}-${Math.random().toString(36).slice(2, 8)}`,
    question: q.question || '',
    topicLabel: q.topicLabel || topic.label,
    options: Array.isArray(q.options) ? q.options.slice(0, 4) : [],
    correctIndex: typeof q.correctIndex === 'number' ? q.correctIndex : 0,
    explanation: q.explanation || 'No explanation provided.',
    difficulty: q.difficulty || 'medium',
  })).filter(q => q.question && q.options.length === 4);
}

async function selectTestTopics(userId, isoWeekNumber) {
  const allTopics = await db.getTopics();
  const topics = allTopics.filter(t => t.type !== 'fact' && t.learning_relevance !== 'exclude');
  if (!topics || topics.length === 0) return { primaryTopic: null, reviewTopics: [], isReviewWeek: false };

  try {
    const weekContent = await getCurrentWeekContent(isoWeekNumber);

    if (weekContent && weekContent.topics && weekContent.topics.length > 0) {
      const primaryTopic = weekContent.topics[0];
      if (!primaryTopic?.id) {
        console.warn('[Test] First curriculum topic has no id — falling back to hash');
      } else {
        const others = topics.filter(t => t.id !== primaryTopic.id);
        const reviewTopics = sample(others, Math.min(5, others.length));
        return { primaryTopic, reviewTopics, isReviewWeek: false };
      }
    }
  } catch (e) {
    console.warn('[Test] Curriculum lookup failed, falling back to hash:', e.message);
  }

  // Fallback hash-based assignment
  const str = `${userId}:${isoWeekNumber}`;
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    hash = (hash << 5) - hash + str.charCodeAt(i);
    hash |= 0;
  }
  const primaryIndex = Math.abs(hash) % topics.length;
  const primaryTopic = topics[primaryIndex];

  const others = topics.filter((_, i) => i !== primaryIndex);
  const reviewTopics = sample(others, Math.min(5, others.length));

  return { primaryTopic, reviewTopics, isReviewWeek: false };
}

export async function getCachedQuiz(userId, weekNumber) {
  return db.getCachedQuiz(userId, weekNumber);
}

export async function forceGenerateTopicQuestions(topic, count = 5) {
  const existingBank = await db.getQuizBank(topic.id);
  const existingKeys = new Set(existingBank.map((q) => normalizeQuestionText(q.question)));

  let lastQualityError = null;
  let candidates = null;

  for (let attempt = 0; attempt < 3; attempt++) {
    let questions;
    try {
      questions = await callQuizModel(topic, count);
    } catch (err) {
      console.warn(`[quiz] LLM failed (attempt ${attempt + 1}):`, err.message);
      lastQualityError = err.message;
      continue;
    }

    const qualityError = validateBatchQuality(questions);
    if (qualityError) {
      lastQualityError = qualityError;
      console.warn(`[quiz] batch rejected (attempt ${attempt + 1}): ${qualityError}`);
      continue;
    }

    const dominant = dominantCorrectIndex(questions);
    if (dominant && attempt < 2) {
      console.warn(`[quiz] correctIndex dominated by ${dominant.index} (${Math.round(dominant.ratio * 100)}%) — re-rolling`);
      continue;
    }

    candidates = questions;
    break;
  }

  if (!candidates) {
    throw new Error(`Quality gate rejected the generated batch for ${topic.label}: ${lastQualityError || 'unbalanced answer distribution'}. Click "Generate" to try again.`);
  }

  const accepted = [];
  for (const q of candidates) {
    const key = normalizeQuestionText(q.question);
    if (existingKeys.has(key)) {
      console.debug('[quiz] dropped duplicate:', q.question);
      continue;
    }
    existingKeys.add(key);
    accepted.push({
      ...q,
      id: `${topic.id}-${Math.random().toString(36).slice(2, 11)}`,
    });
  }

  if (!accepted.length) {
    throw new Error(`All generated questions for ${topic.label} were duplicates of existing ones.`);
  }

  const merged = [...existingBank, ...accepted];
  await db.setQuizBank(topic.id, merged);
  return accepted;
}

// Single-call batch generator used by the weekly quiz path. Generates the
// entire 5-question test in one LLM round-trip instead of looping topic by
// topic — the persisted quiz_banks collection is deprecated so caching can't
// amortise multiple calls, and 6 sequential calls blew through the <30s budget.
async function callQuizBatchModel(primaryTopic, reviewTopics, count) {
  const reviewBlock = reviewTopics.length
    ? `\n\nReview topics — draw ${Math.max(0, count - 3)} questions from these to broaden the test:\n${reviewTopics
        .map((t, i) => `${i + 1}. ${t.label} (${t.type}) — ${t.description}`)
        .join('\n')}`
    : '';

  const prompt = `Generate exactly ${count} multiple-choice quiz questions for this week's test and emit them via the emit_quiz_questions tool.

Primary topic — focus 3 questions here:
- Label: ${primaryTopic.label}
- Type: ${primaryTopic.type}
- Description: ${primaryTopic.description}${reviewBlock}

Rules:
- Set "topicLabel" to the label of the topic the question covers.
- Prefix the four options with "A) ", "B) ", "C) ", "D) ".
- Make questions specific and practical, not trivial recall.
- Distribute correctIndex roughly evenly across 0, 1, 2, 3 within the batch.`;

  const result = await callLLM({
    task: 'quiz.batch_generate',
    tier: 'fast',
    system: cachedSystem(QUIZ_SYSTEM),
    user: prompt,
    tools: [EMIT_QUIZ_QUESTIONS_TOOL],
    toolChoice: { type: 'tool', name: EMIT_QUIZ_QUESTIONS_TOOL.name },
    maxTokens: 4096,
    timeoutMs: QUIZ_BATCH_TIMEOUT_MS,
  });

  const emitted = result.toolUses[0]?.input;
  if (!emitted) throw new Error('LLM did not emit tool output.');

  let questions;
  if (Array.isArray(emitted.questions)) questions = emitted.questions;
  else if (Array.isArray(emitted)) questions = emitted;
  else if (emitted.questions && typeof emitted.questions === 'object') questions = [emitted.questions];
  else throw new Error('Unexpected quiz output shape.');

  if (!questions.length) throw new Error('LLM emitted zero questions.');

  return questions.map((q, i) => ({
    id: `${primaryTopic.id}-${Date.now().toString(36)}-${i}`,
    question: q.question || '',
    topicLabel: q.topicLabel || primaryTopic.label,
    options: Array.isArray(q.options) ? q.options.slice(0, 4) : [],
    correctIndex: typeof q.correctIndex === 'number' ? q.correctIndex : 0,
    explanation: q.explanation || 'No explanation provided.',
    difficulty: q.difficulty || 'medium',
  })).filter((q) => q.question && q.options.length === 4);
}

export async function getTopicQuestionBank(topicId) {
  return db.getQuizBank(topicId);
}

export async function deleteQuestion(topicId, questionId) {
  return db.deleteQuestionFromBank(topicId, questionId);
}

export async function generateWeeklyQuiz(userId, weekNumber, force = false) {
  if (!force) {
    const cached = await db.getCachedQuiz(userId, weekNumber);
    if (cached?.questions?.length > 0) return cached;
  }

  const { primaryTopic, reviewTopics } = await selectTestTopics(userId, weekNumber);
  if (!primaryTopic) throw new Error('No topics available to generate a quiz.');

  // Cap review-topic context to keep the prompt focused and the call fast.
  const reviewContext = (reviewTopics || []).slice(0, 3);

  let accepted = [];
  let lastError = null;

  for (let attempt = 0; attempt < 2; attempt++) {
    let candidates;
    try {
      candidates = await callQuizBatchModel(primaryTopic, reviewContext, 5);
    } catch (err) {
      lastError = err.message;
      console.warn(`[quiz] batch attempt ${attempt + 1} failed:`, err.message);
      continue;
    }

    const qualityError = validateBatchQuality(candidates);
    if (qualityError) {
      lastError = qualityError;
      console.warn(`[quiz] batch quality rejected (attempt ${attempt + 1}): ${qualityError}`);
      continue;
    }

    accepted = candidates;
    break;
  }

  if (accepted.length < 5) {
    throw new Error(
      `Could not generate a 5-question test for ${primaryTopic.label}${lastError ? `: ${lastError}` : ''}. Please try again.`,
    );
  }

  const shuffled = shuffle(accepted).slice(0, 5);

  const quiz = {
    questions: shuffled,
    meta: {
      userId,
      weekNumber,
      generatedAt: new Date().toISOString(),
      primaryTopic: primaryTopic.label,
    },
  };

  await db.setCachedQuiz(userId, weekNumber, quiz);
  return quiz;
}

export async function saveTestResult(userId, weekNumber, result) {
  await db.saveQuizResult(userId, weekNumber, result);

  const pointsEarned = result.score * 2;
  const members = await db.getTeamMembers();
  const member = members.find(m => m.id === userId);
  await db.upsertLeaderboardEntry(userId, member?.name || 'Unknown', pointsEarned, 1);

  return { pointsEarned };
}

export async function getTestResult(userId, weekNumber) {
  return db.getQuizResult(userId, weekNumber);
}

// ── On-demand topic tests ───────────────────────────────────────────────────
// A medewerker can take a 5-question test on any eligible topic at any time.
// Questions come from the shared, admin-curated question_bank collection — the
// LLM is NOT called from the user path. Points feed the same leaderboard as the
// weekly test, but are capped at WEEKLY_POINT_CAP_PER_TOPIC per ISO week per
// (user, topic) so the shared bank can't be farmed.

export const ON_DEMAND_TEST_SIZE = 5;
export const WEEKLY_POINT_CAP_PER_TOPIC = 10;

export async function getEligibleTopicsForOnDemand() {
  // Topics a learner may pick from: not a fact, not excluded by relevance,
  // and with at least ON_DEMAND_TEST_SIZE questions in the shared bank.
  const allTopics = await db.getTopics();
  const candidates = allTopics.filter(
    t => t.type !== 'fact' && t.learning_relevance !== 'exclude',
  );

  const out = [];
  for (const t of candidates) {
    const bank = await db.getQuestionBank(t.id);
    if (bank.length >= ON_DEMAND_TEST_SIZE) {
      out.push({ ...t, bankSize: bank.length });
    }
  }
  return out;
}

export async function startOnDemandTest(userId, topicId) {
  const allTopics = await db.getTopics();
  const topic = allTopics.find(t => t.id === topicId);
  if (!topic) throw new Error('Unknown topic.');

  const bank = await db.getQuestionBank(topicId);
  if (bank.length < ON_DEMAND_TEST_SIZE) {
    throw new Error(`Not enough questions in this topic's bank yet (have ${bank.length}, need ${ON_DEMAND_TEST_SIZE}). Ask an admin to expand it.`);
  }

  const seen = await db.getUserSeenQuestionIds(userId, topicId);
  const unseen = bank.filter(q => !seen.has(q.id));
  const seenList = bank.filter(q => seen.has(q.id));

  // Prefer unseen; fall back to seen if the bank is thin. Repeated questions
  // are marked isUnseen=false so finishOnDemandTest can deny them points.
  const picked = [
    ...shuffle(unseen).slice(0, ON_DEMAND_TEST_SIZE).map(q => ({ ...q, isUnseen: true })),
  ];
  if (picked.length < ON_DEMAND_TEST_SIZE) {
    const fill = shuffle(seenList).slice(0, ON_DEMAND_TEST_SIZE - picked.length)
      .map(q => ({ ...q, isUnseen: false }));
    picked.push(...fill);
  }

  return {
    questions: shuffle(picked),
    meta: {
      userId,
      topicId,
      topicLabel: topic.label,
      startedAt: new Date().toISOString(),
      bankSize: bank.length,
      unseenAvailable: unseen.length,
    },
  };
}

export async function finishOnDemandTest(userId, topicId, topicLabel, quiz, answers, timeUsed) {
  const questions = quiz.questions || [];

  let score = 0;
  let unseenCorrect = 0;
  const questionIds = [];
  const breakdown = questions.map((q) => {
    questionIds.push(q.id);
    const selected = answers[q.id];
    const correct = selected === q.correctIndex;
    if (correct) {
      score++;
      if (q.isUnseen) unseenCorrect++;
    }
    return {
      questionId: q.id,
      question: q.question,
      topicLabel: q.topicLabel || topicLabel,
      selected,
      correctIndex: q.correctIndex,
      correct,
      isUnseen: !!q.isUnseen,
      explanation: q.explanation,
      options: q.options,
    };
  });

  const total = questions.length;
  const percentage = total ? Math.round((score / total) * 100) : 0;

  // Points: 2 per correctly-answered UNSEEN question, capped weekly per topic.
  const rawPoints = unseenCorrect * 2;
  const alreadyEarned = await db.getOnDemandPointsThisWeek(userId, topicId);
  const remainingBudget = Math.max(0, WEEKLY_POINT_CAP_PER_TOPIC - alreadyEarned);
  const pointsEarned = Math.min(rawPoints, remainingBudget);
  const capped = rawPoints > pointsEarned;

  const completedAt = new Date().toISOString();
  const isoWeek = db.isoWeekKey();

  await db.saveOnDemandAttempt({
    userId,
    topicId,
    topicLabel,
    questionIds,
    score,
    total,
    percentage,
    timeUsed,
    unseenCorrect,
    pointsEarned,
    capped,
    isoWeek,
    completedAt,
    breakdown,
  });

  if (pointsEarned > 0) {
    const members = await db.getTeamMembers();
    const member = members.find(m => m.id === userId);
    await db.upsertLeaderboardEntry(userId, member?.name || 'Unknown', pointsEarned, 1);
  }

  return {
    score,
    total,
    percentage,
    timeUsed,
    completedAt,
    breakdown,
    unseenCorrect,
    pointsEarned,
    capped,
    rawPoints,
  };
}
