import type { NormalizedQuestion, QuestionInput, QuestionOptionInput, UserAnswer } from "./types.ts";

export const MAX_QUESTIONS = 8;
export const MAX_OPTIONS = 8;

function requiredText(value: unknown, field: string, maxLength: number): string {
  if (typeof value !== "string") throw new Error(`${field} must be a string`);
  const normalized = value.trim();
  if (!normalized) throw new Error(`${field} must not be empty`);
  if (normalized.length > maxLength) throw new Error(`${field} must be at most ${maxLength} characters`);
  return normalized;
}

function normalizeOption(option: QuestionOptionInput, questionId: string, index: number): QuestionOptionInput {
  return {
    value: requiredText(option?.value, `questions[${questionId}].options[${index}].value`, 128),
    label: requiredText(option?.label, `questions[${questionId}].options[${index}].label`, 160),
    ...(option?.description === undefined
      ? {}
      : { description: requiredText(option.description, `questions[${questionId}].options[${index}].description`, 500) }),
  };
}

export function normalizeQuestions(input: readonly QuestionInput[]): NormalizedQuestion[] {
  if (!Array.isArray(input) || input.length === 0) throw new Error("questions must contain at least one question");
  if (input.length > MAX_QUESTIONS) throw new Error(`questions must contain at most ${MAX_QUESTIONS} questions`);

  const ids = new Set<string>();
  return input.map((question, index) => {
    if (question === null || typeof question !== "object" || Array.isArray(question)) {
      throw new Error(`questions[${index}] must be an object`);
    }
    const id = requiredText(question?.id, `questions[${index}].id`, 64);
    if (!/^[A-Za-z][A-Za-z0-9_-]*$/u.test(id)) {
      throw new Error(`questions[${index}].id must start with a letter and contain only letters, numbers, _ or -`);
    }
    if (ids.has(id)) throw new Error(`duplicate question id: ${id}`);
    ids.add(id);

    const prompt = requiredText(question?.prompt, `questions[${index}].prompt`, 1_000);
    const label = question.label === undefined
      ? `Q${index + 1}`
      : requiredText(question.label, `questions[${index}].label`, 48);
    if (question.type !== "select" && question.type !== "text") {
      throw new Error(`questions[${index}].type must be select or text`);
    }

    if (question.allowCustom !== undefined && typeof question.allowCustom !== "boolean") {
      throw new Error(`questions[${index}].allowCustom must be a boolean`);
    }
    if (question.required !== undefined && typeof question.required !== "boolean") {
      throw new Error(`questions[${index}].required must be a boolean`);
    }

    const rawOptions = question.options ?? [];
    if (!Array.isArray(rawOptions)) throw new Error(`questions[${index}].options must be an array`);
    if (rawOptions.length > MAX_OPTIONS) {
      throw new Error(`questions[${index}].options must contain at most ${MAX_OPTIONS} options`);
    }
    if (question.type === "text" && rawOptions.length > 0) {
      throw new Error(`text question ${id} must not define options`);
    }
    if (question.type === "select" && rawOptions.length === 0) {
      throw new Error(`select question ${id} must define at least one option`);
    }

    const options = rawOptions.map((option, optionIndex) => normalizeOption(option, id, optionIndex));
    const values = new Set<string>();
    for (const option of options) {
      if (values.has(option.value)) throw new Error(`duplicate option value ${option.value} in question ${id}`);
      values.add(option.value);
    }

    return {
      id,
      label,
      prompt,
      type: question.type,
      options,
      allowCustom: question.type === "select" && question.allowCustom !== false,
      required: question.required !== false,
    };
  });
}

export function orderedAnswers(questions: readonly NormalizedQuestion[], answers: ReadonlyMap<string, UserAnswer>): UserAnswer[] {
  return questions.flatMap((question) => {
    const answer = answers.get(question.id);
    return answer ? [answer] : [];
  });
}

export function formatAnswers(answers: readonly UserAnswer[]): string {
  return answers
    .map((answer) => {
      if (answer.skipped) return `${answer.id}: skipped`;
      if (answer.type === "text" || answer.custom) return `${answer.id}: user wrote: ${answer.answer}`;
      return `${answer.id}: user selected: ${answer.label} (value: ${answer.value})`;
    })
    .join("\n");
}
