export const STORAGE_KEY = "kernel-panic-progress-v2";

export const RUN_MODES = Object.freeze({
  course: { questionsPerChapter: 3, label: "Kurslauf" },
  learning: { questionsPerChapter: 10, label: "Lernmodus" }
});

export const STRIKE_LIMIT = 3;
export const MAX_ATTEMPTS_PER_QUESTION = 20;

export function cappedAttempts(value) {
  const numeric = Math.floor(Number(value));
  if (!Number.isFinite(numeric) || numeric < 0) return 0;
  return Math.min(MAX_ATTEMPTS_PER_QUESTION, numeric);
}

// Spiegelt score_api.py: integrity = max(20, 100 - mistakes * 2) über die
// übermittelten, gedeckelten Fehlversuche. Nur abgeleitet statt mitgezählt
// bleiben Client und Server bei jedem Verlauf beim selben Ergebnis.
export function deriveIntegrity(attempts) {
  const mistakes = Object.values(attempts ?? {}).reduce(
    (total, value) => total + cappedAttempts(value),
    0
  );
  return Math.max(20, 100 - mistakes * 2);
}

export function solutionText(mission) {
  if (mission.type === "choice") {
    return mission.choices.find((choice) => choice.correct)?.label ?? "";
  }
  if (mission.type === "terminal") {
    return mission.acceptedAnswers?.[0] ?? "";
  }
  if (mission.type === "sequence") {
    return (mission.answer ?? []).join(" ");
  }
  return "";
}

export const EMPTY_STATE = Object.freeze({
  started: false,
  runId: null,
  startedAt: null,
  mode: null,
  runQuestionIds: [],
  completed: [],
  xp: 0,
  integrity: 100,
  attempts: {},
  hints: [],
  strikes: 0,
  strikeChapterId: null
});

export function createRunId(now = Date.now, random = Math.random) {
  return `${now().toString(36)}-${Math.floor(random() * 1_000_000_000).toString(36)}`;
}

export function sanitizeAlias(value) {
  return String(value ?? "")
    .normalize("NFKC")
    .replace(/\s+/g, " ")
    .trim()
    .slice(0, 18);
}

export function calculateLeaderboardScore({ xp, maxXp, integrity }) {
  if (!Number.isFinite(maxXp) || maxXp <= 0) return 0;
  const safeXp = Math.min(maxXp, Math.max(0, Number(xp) || 0));
  const safeIntegrity = Math.min(100, Math.max(20, Number(integrity) || 20));
  return Math.round((safeXp / maxXp) * 750 + safeIntegrity * 2.5);
}

export function normalizeCommand(value) {
  return String(value ?? "")
    .trim()
    .replace(/\s+/g, " ")
    .replace(/\s*\|\s*/g, " | ")
    .replace(/\s*>\s*/g, " > ");
}

export function isTerminalAnswer(value, acceptedAnswers) {
  const normalized = normalizeCommand(value);
  return acceptedAnswers.some(
    (answer) => normalizeCommand(answer) === normalized
  );
}

export function isSequenceCorrect(selected, answer) {
  return (
    selected.length === answer.length &&
    selected.every((token, index) => token === answer[index])
  );
}

export function shuffleItems(items, random = Math.random) {
  const shuffled = [...items];

  for (let index = shuffled.length - 1; index > 0; index -= 1) {
    const target = Math.floor(random() * (index + 1));
    [shuffled[index], shuffled[target]] = [shuffled[target], shuffled[index]];
  }

  const unchanged = shuffled.every((item, index) => item === items[index]);
  if (unchanged && shuffled.length > 1) {
    [shuffled[0], shuffled[shuffled.length - 1]] = [
      shuffled[shuffled.length - 1],
      shuffled[0]
    ];
  }

  return shuffled;
}

export function shuffleSequenceTokens(
  tokens,
  answer = tokens,
  random = Math.random
) {
  const shuffled = shuffleItems(tokens, random);

  const unchanged = isSequenceCorrect(shuffled, tokens);
  const revealsAnswer =
    answer.length > 0 &&
    answer.every((value, index) => shuffled[index] === value);

  if ((unchanged || revealsAnswer) && shuffled.length > 1) {
    const differentIndex = shuffled.findIndex(
      (value, index) => index > 0 && value !== shuffled[0]
    );

    if (differentIndex > 0) {
      [shuffled[0], shuffled[differentIndex]] = [
        shuffled[differentIndex],
        shuffled[0]
      ];
    }
  }

  return shuffled;
}

export function createRunQuestionIds(chapters, mode, random = Math.random) {
  const questionsPerChapter = RUN_MODES[mode]?.questionsPerChapter;
  if (!questionsPerChapter) throw new Error(`Unbekannter Spielmodus: ${mode}`);

  return chapters.flatMap((chapter) =>
    shuffleItems(chapter.questions, random)
      .slice(0, questionsPerChapter)
      .map((question) => question.id)
  );
}

export function calculateMissionScore({
  baseXp,
  wrongAttempts = 0,
  hintUsed = false
}) {
  return Math.max(35, baseXp - wrongAttempts * 10 - (hintUsed ? 15 : 0));
}

export function registerWrongAnswer(
  state,
  { mission, chapterId, strikeLimit = STRIKE_LIMIT }
) {
  const previous = cappedAttempts(state.attempts?.[mission.id]);
  const next = Math.min(MAX_ATTEMPTS_PER_QUESTION, previous + 1);
  const attempts = { ...state.attempts, [mission.id]: next };
  const hintUsed = (state.hints ?? []).includes(mission.id);
  const integrity = deriveIntegrity(attempts);
  const sameChapter = state.strikeChapterId === chapterId;
  const strikes = Math.min(
    strikeLimit,
    (sameChapter ? cappedAttempts(state.strikes) : 0) + 1
  );

  return {
    state: {
      ...state,
      attempts,
      integrity,
      strikes,
      strikeChapterId: chapterId
    },
    penalty: {
      xp:
        calculateMissionScore({
          baseXp: mission.xp,
          wrongAttempts: previous,
          hintUsed
        }) -
        calculateMissionScore({
          baseXp: mission.xp,
          wrongAttempts: next,
          hintUsed
        }),
      integrity: deriveIntegrity(state.attempts) - integrity
    },
    strikes,
    limitReached: strikes >= strikeLimit
  };
}

export function sanitizeProgress(value, questionIds, chapterIds = []) {
  if (!value || typeof value !== "object") return { ...EMPTY_STATE };
  const validIds = new Set(questionIds);
  const mode = Object.hasOwn(RUN_MODES, value.mode) ? value.mode : null;
  const expectedRunLength = mode
    ? RUN_MODES[mode].questionsPerChapter * 10
    : 0;
  const runQuestionIds = Array.isArray(value.runQuestionIds)
    ? [...new Set(value.runQuestionIds.filter((id) => validIds.has(id)))]
    : [];

  if (
    Boolean(value.started) &&
    (!mode || runQuestionIds.length !== expectedRunLength)
  ) {
    return { ...EMPTY_STATE };
  }

  const runIds = new Set(runQuestionIds);
  const completed = Array.isArray(value.completed)
    ? [...new Set(value.completed.filter((id) => runIds.has(id)))]
    : [];
  const hints = Array.isArray(value.hints)
    ? [...new Set(value.hints.filter((id) => runIds.has(id)))]
    : [];
  const attempts =
    value.attempts && typeof value.attempts === "object"
      ? Object.fromEntries(
          Object.entries(value.attempts)
            .filter(([id]) => runIds.has(id))
            .map(([id, count]) => [id, cappedAttempts(count)])
        )
      : {};

  return {
    started: Boolean(value.started),
    runId: value.started
      ? typeof value.runId === "string" && value.runId
        ? value.runId.slice(0, 120)
        : `legacy-${mode ?? "run"}-${runQuestionIds.join("-")}`
      : null,
    startedAt:
      value.started && Number.isFinite(Date.parse(value.startedAt))
        ? new Date(value.startedAt).toISOString()
        : value.started
          ? new Date().toISOString()
          : null,
    mode,
    runQuestionIds,
    completed,
    xp: Number.isFinite(value.xp) ? Math.max(0, Math.floor(value.xp)) : 0,
    integrity: deriveIntegrity(attempts),
    attempts,
    hints,
    strikes: Math.min(STRIKE_LIMIT, cappedAttempts(value.strikes)),
    strikeChapterId: chapterIds.includes(value.strikeChapterId)
      ? value.strikeChapterId
      : null
  };
}

// Sucht die nächste offene Frage des Kapitels hinter afterIndex und läuft
// danach auf den Kapitelanfang um. Der Rundlauf ist wichtig: nach einer
// falschen Antwort ist die aktuelle Frage selbst noch offen und darf nicht
// sofort erneut erscheinen, am Kapitelende aber schon.
export function nextOpenIndexInChapter(
  runQuestions,
  completed,
  chapterId,
  afterIndex = -1
) {
  const open = runQuestions
    .map((question, index) => ({ question, index }))
    .filter(
      ({ question }) =>
        question.chapterId === chapterId && !completed.includes(question.id)
    );

  return (open.find(({ index }) => index > afterIndex) ?? open[0])?.index ?? -1;
}

export function chapterResults(chapters, runQuestions, state) {
  return chapters.map((chapter) => {
    const questions = runQuestions.filter(
      (question) => question.chapterId === chapter.id
    );
    const completed = questions.filter((question) =>
      state.completed.includes(question.id)
    );
    const mistakes = completed.reduce(
      (total, question) => total + Number(state.attempts[question.id] ?? 0),
      0
    );
    const hints = completed.filter((question) =>
      state.hints.includes(question.id)
    ).length;

    return {
      id: chapter.id,
      number: chapter.number,
      topic: chapter.topic,
      total: questions.length,
      completed: completed.length,
      mistakes,
      hints,
      clean: completed.filter(
        (question) =>
          Number(state.attempts[question.id] ?? 0) === 0 &&
          !state.hints.includes(question.id)
      ).length
    };
  });
}
