// lib/canvas/map.js — turn one assignment into a neutral "Canvas IR" that both the
// QTI .zip renderer (qti.js) and the future REST push (api.js) consume. This is where
// every app question type is mapped to a Canvas question type, exactly once.
//
// Mapping decisions (see project memory canvas-export-feature):
//   multiple_choice -> multiple_choice_question
//   true_false      -> true_false_question
//   essay           -> essay_question           (manual; rubric -> neutral feedback)
//   short_answer    -> essay_question           (open-ended; sample/key points -> feedback)
//   fill_blank      -> fill_in_multiple_blanks_question  (______ becomes [blankN] tokens)
//   matching        -> matching_question
//   discussion      -> skipped (no Canvas quiz equivalent)
//   caseStudy       -> text_only_question (intro passage at the top)

const plain = (s) => String(s ?? "").trim();

// Escape text so that, after Canvas decodes the XML layer, the result is valid HTML.
const escHtmlText = (s) =>
  String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");

// Plain text -> HTML with paragraphs and line breaks (content escaped, tags raw).
function paragraphsToHtml(text) {
  const t = plain(text);
  if (!t) return "";
  return t
    .split(/\n{2,}/)
    .map((para) => "<p>" + para.split(/\n/).map(escHtmlText).join("<br>") + "</p>")
    .join("");
}

function shortAnswerGuide(q) {
  const out = [];
  if (plain(q.sampleAnswer)) out.push("Sample answer: " + plain(q.sampleAnswer));
  const kp = (q.keyPoints || []).map(plain).filter(Boolean);
  if (kp.length) out.push("Must include: " + kp.join("; "));
  return out.join("\n");
}

function essayGuide(q) {
  const out = [];
  if (plain(q.sampleResponse)) out.push("Sample response: " + plain(q.sampleResponse));
  const rubric = (q.rubric || []).filter((r) => plain(r?.criterion));
  if (rubric.length) {
    out.push("Rubric:");
    for (const r of rubric) {
      out.push(`• ${plain(r.criterion)} (${Number(r.points) || 0} pts)` +
        (plain(r.description) ? " — " + plain(r.description) : ""));
    }
  }
  return out.join("\n");
}

// Convert the app's ______ blanks into Canvas named tokens [blank1], [blank2], ...
function buildBlanks(q, nextId) {
  let n = 0;
  const text = String(q.question || "").replace(/_{2,}/g, () => `[blank${++n}]`);
  if (n === 0) return null;
  const answers = q.answers || [];
  const list = [];
  for (let i = 0; i < n; i++) {
    const ans = plain(answers[i]);
    list.push({
      name: `blank${i + 1}`,
      respIdent: `response_blank${i + 1}`,
      answers: [{ ident: nextId(), textPlain: ans || "" }],
    });
  }
  return { list, promptHtml: paragraphsToHtml(text) };
}

function buildMatching(q, nextId) {
  const pairs = (q.pairs || []).filter((p) => plain(p?.left) && plain(p?.right));
  if (pairs.length < 2) return null;
  const rights = [];
  const identByText = new Map();
  for (const p of pairs) {
    const text = plain(p.right);
    if (!identByText.has(text)) {
      const ident = nextId();
      identByText.set(text, ident);
      rights.push({ ident, textPlain: text });
    }
  }
  const lefts = pairs.map((p, i) => ({
    respIdent: `question_${i + 1}`,
    textPlain: plain(p.left),
    correctRightIdent: identByText.get(plain(p.right)),
  }));
  return { lefts, rights };
}

export function mapAssignmentToCanvas(assignment, opts = {}) {
  const includeExplanations = opts.includeExplanations !== false;
  let counter = 1000;
  const nextId = () => String(++counter);

  const items = [];
  const skipped = [];

  const caseStudy = plain(assignment?.caseStudy);
  if (caseStudy) {
    items.push({
      ident: "intro_passage",
      title: "Read the following",
      canvasType: "text_only_question",
      points: 0,
      promptHtml: paragraphsToHtml(caseStudy),
    });
  }

  (assignment?.questions || []).forEach((q, i) => {
    const ident = "q_" + (i + 1);
    const points = Number(q.points) || 0;
    const neutral = includeExplanations ? plain(q.explanation) : "";
    const neutralHtml = (extra) => {
      const merged = [neutral, extra].filter(Boolean).join("\n\n");
      return merged ? paragraphsToHtml(merged) : "";
    };
    const title = `Question ${i + 1}`;

    switch (q.type) {
      case "multiple_choice": {
        const choices = (q.options || []).map((opt, j) => ({
          ident: nextId(), html: escHtmlText(plain(opt)), correct: j === q.correctIndex,
        }));
        if (choices.length < 2) { skipped.push({ type: q.type, question: q.question, reason: "needs 2+ options" }); break; }
        items.push({ ident, title, canvasType: "multiple_choice_question", points,
          promptHtml: paragraphsToHtml(q.question), choices, feedback: { neutralHtml: neutralHtml() } });
        break;
      }
      case "true_false": {
        items.push({ ident, title, canvasType: "true_false_question", points,
          promptHtml: paragraphsToHtml(q.question),
          choices: [
            { ident: nextId(), html: "True", correct: q.correctAnswer === true },
            { ident: nextId(), html: "False", correct: q.correctAnswer !== true },
          ],
          feedback: { neutralHtml: neutralHtml() } });
        break;
      }
      case "essay":
      case "short_answer": {
        const guide = q.type === "short_answer" ? shortAnswerGuide(q) : essayGuide(q);
        items.push({ ident, title, canvasType: "essay_question", points,
          promptHtml: paragraphsToHtml(q.question), feedback: { neutralHtml: neutralHtml(guide) } });
        break;
      }
      case "fill_blank": {
        const b = buildBlanks(q, nextId);
        if (!b) { skipped.push({ type: q.type, question: q.question, reason: "no blanks found" }); break; }
        items.push({ ident, title, canvasType: "fill_in_multiple_blanks_question", points,
          promptHtml: b.promptHtml, blanks: b.list, feedback: { neutralHtml: neutralHtml() } });
        break;
      }
      case "matching": {
        const m = buildMatching(q, nextId);
        if (!m) { skipped.push({ type: q.type, question: q.question, reason: "needs 2+ pairs" }); break; }
        items.push({ ident, title, canvasType: "matching_question", points,
          promptHtml: paragraphsToHtml(q.question || "Match each item on the left with the correct item on the right."),
          lefts: m.lefts, rights: m.rights, feedback: { neutralHtml: neutralHtml() } });
        break;
      }
      default:
        skipped.push({ type: q.type, question: q.question, reason: "not supported in Canvas quizzes" });
    }
  });

  const gradedCount = items.filter((it) => it.canvasType !== "text_only_question").length;

  const quiz = {
    title: plain(assignment?.title) || "Untitled quiz",
    description: paragraphsToHtml(plain(assignment?.instructions)),
    pointsPossible: items.reduce((s, it) => s + (it.points || 0), 0),
    quizType: opts.quizType || "assignment",
    timeLimit: opts.timeLimit ?? "",          // minutes; "" = no limit
    allowedAttempts: opts.allowedAttempts ?? 1, // -1 = unlimited
    scoringPolicy: opts.scoringPolicy || "keep_highest",
    shuffleAnswers: !!opts.shuffleAnswers,
    showCorrectAnswers: opts.showCorrectAnswers !== false,
    oneQuestionAtATime: !!opts.oneQuestionAtATime,
    cantGoBack: !!opts.cantGoBack,
    dueAt: opts.dueAt || "",
    unlockAt: opts.unlockAt || "",
    lockAt: opts.lockAt || "",
    accessCode: opts.accessCode || "",
  };

  return { quiz, items, skipped, gradedCount };
}
