import { defineStore } from "pinia"
import { computed, ref } from "vue"

/**
 * Quiz question type ids. Mirrors the legacy `QuizQuestionDefinitions.js`
 * type set so consumers migrating from v1 retain feature parity.
 */
export type QuizQuestionType =
  | "single-choice"
  | "multiple-choice"
  | "true-false"
  | "text-match"
  | "numeric-range"
  | "essay"
  | "matching"
  | "ordering"
  | "fill-blank"
  | "matching-categories"
  | "ai-analysis"

export interface QuizOption {
  id: string
  label: string
  value: string | number
  correct?: boolean
  score?: number
}

/** A left/right pair for matching questions. */
export interface MatchPair {
  left: string
  right: string
}

/** One blank in a fill-in-the-blank question, with the accepted answers. */
export interface FillBlank {
  id: string
  answers: string[]
}

/** A category bucket for matching-categories questions. */
export interface QuizCategory {
  slug: string
  label: string
  items: string[]
}

/** One identifying item in an ai-analysis question's criteria list. */
export interface AiCriterion {
  key: string
  name: string
  description: string
  points: number
}

export type AiScoringMethod = "standard" | "item_count" | "ai_judgment"

export interface QuizQuestion {
  id: string
  /** Bank that owns this question. Always set. */
  bankId: string
  type: QuizQuestionType
  prompt: string
  options: QuizOption[]

  // Generic, type-agnostic fields ---------------------------------------
  helpText?: string
  hint?: string
  icon?: string
  required?: boolean
  points?: number
  timeLimit?: number
  feedback?: { correct?: string; incorrect?: string }

  // multiple-choice -----------------------------------------------------
  allowPartialCredit?: boolean

  // true-false / numeric-range / text-match (single) -------------------
  correctAnswer?: string | number | boolean

  // text-match ----------------------------------------------------------
  correctAnswers?: string[]
  caseSensitive?: boolean
  trimWhitespace?: boolean

  // numeric-range -------------------------------------------------------
  tolerance?: number
  numericRangeMin?: number
  numericRangeMax?: number

  // essay ---------------------------------------------------------------
  minWords?: number
  maxWords?: number
  gradingRubric?: string

  // matching ------------------------------------------------------------
  matchPairs?: MatchPair[]
  shuffleMatches?: boolean

  // ordering ------------------------------------------------------------
  correctOrder?: string[]
  partialCreditEnabled?: boolean

  // fill-blank ----------------------------------------------------------
  blanks?: FillBlank[]

  // matching-categories -------------------------------------------------
  categories?: QuizCategory[]
  itemsPerCategory?: number

  // ai-analysis ---------------------------------------------------------
  scoringMethod?: AiScoringMethod
  aiPrompt?: string
  aiCriteria?: AiCriterion[]
  pointsPerItem?: number
  maxPoints?: number
  confidenceThreshold?: number
  aiModel?: string
  maxSubmissions?: number
  submissionMultipliers?: string
  useAiFeedback?: boolean
}

export interface QuestionBank {
  id: string
  name: string
  description?: string
}

/**
 * QuizBuilder schema. The builder's scope is questions and the banks
 * that group them. Quiz-level metadata (title, scoring, …) is owned by
 * the host page, not the builder. The schema always contains at least
 * one bank — the default initial bank.
 */
export interface QuizBuilderSchema {
  questionBanks: QuestionBank[]
  questions: QuizQuestion[]
}

/** Identifies which question is currently selected. */
export interface QuizBuilderSelection {
  id: string | null
}

let idCounter = 0
function nextId(prefix: string): string {
  idCounter += 1
  return `${prefix}-${Date.now()}-${idCounter}`
}

/** Test helper to make ids deterministic. */
export function __resetQuizBuilderIdCounter(): void {
  idCounter = 0
}

function emptySchema(): QuizBuilderSchema {
  const defaultBank: QuestionBank = {
    id: nextId("bank"),
    name: "Default bank",
  }
  return {
    questionBanks: [defaultBank],
    questions: [],
  }
}

/**
 * Produce the type-specific default fields for a brand-new question of
 * `type`. Generic fields like `id`, `bankId`, `points`, `required` are
 * added by the caller.
 */
function defaultsForType(
  type: QuizQuestionType,
): Pick<QuizQuestion, "prompt" | "options"> & Partial<QuizQuestion> {
  const readableType = type.replace(/-/g, " ")
  const base = {
    prompt: `New ${readableType} question`,
    options: [] as QuizOption[],
  }
  switch (type) {
    case "single-choice":
    case "multiple-choice":
      return {
        ...base,
        options: [
          { id: nextId("opt"), label: "Option A", value: "a" },
          { id: nextId("opt"), label: "Option B", value: "b" },
        ],
        ...(type === "multiple-choice" ? { allowPartialCredit: true } : {}),
      }
    case "true-false":
      return { ...base, correctAnswer: true }
    case "text-match":
      return {
        ...base,
        correctAnswers: [""],
        caseSensitive: false,
        trimWhitespace: true,
      }
    case "numeric-range":
      return { ...base, correctAnswer: 0, tolerance: 0 }
    case "essay":
      return base
    case "matching":
      return {
        ...base,
        matchPairs: [
          { left: "", right: "" },
          { left: "", right: "" },
        ],
        shuffleMatches: true,
      }
    case "ordering":
      return { ...base, correctOrder: ["", ""] }
    case "fill-blank":
      return {
        ...base,
        blanks: [{ id: nextId("blank"), answers: [""] }],
        caseSensitive: false,
      }
    case "matching-categories":
      return {
        ...base,
        categories: [
          { slug: nextId("cat"), label: "", items: ["", "", "", ""] },
          { slug: nextId("cat"), label: "", items: ["", "", "", ""] },
        ],
        itemsPerCategory: 4,
      }
    case "ai-analysis":
      return {
        ...base,
        scoringMethod: "standard",
        aiPrompt: "",
        aiCriteria: [
          { key: nextId("crit"), name: "", description: "", points: 100 },
        ],
        pointsPerItem: 50,
        maxPoints: 100,
        confidenceThreshold: 0.75,
        aiModel: "",
      }
  }
}

/** Fields preserved when a question's type changes. */
const PRESERVED_KEYS: ReadonlyArray<keyof QuizQuestion> = [
  "prompt",
  "helpText",
  "hint",
  "icon",
  "required",
  "points",
  "timeLimit",
  "feedback",
]

export const useQuizBuilderStore = defineStore(
  "gx-modui/quizBuilder",
  () => {
    const builders = ref<Record<string, QuizBuilderSchema>>({})
    const selections = ref<Record<string, QuizBuilderSelection>>({})

    function ensureBuilder(builderId: string): QuizBuilderSchema {
      if (!builders.value[builderId]) {
        builders.value[builderId] = emptySchema()
        selections.value[builderId] = { id: null }
      }
      return builders.value[builderId]!
    }

    /**
     * Load an external schema. Tolerates legacy shapes — if `questionBanks`
     * is empty or missing, a default bank is created and any orphan
     * questions are assigned to it.
     */
    function loadSchema(
      builderId: string,
      schema: Partial<QuizBuilderSchema>,
    ): void {
      const incomingBanks = (schema.questionBanks ?? []).map((b) => ({ ...b }))
      const banks: QuestionBank[] =
        incomingBanks.length > 0
          ? incomingBanks
          : [{ id: nextId("bank"), name: "Default bank" }]
      const defaultBankId = banks[0]!.id
      const validBankIds = new Set(banks.map((b) => b.id))
      const questions = (schema.questions ?? []).map((q) => ({
        ...q,
        bankId:
          q.bankId && validBankIds.has(q.bankId) ? q.bankId : defaultBankId,
        options: (q.options ?? []).map((o) => ({ ...o })),
      }))
      builders.value[builderId] = { questionBanks: banks, questions }
      selections.value[builderId] = { id: null }
    }

    function resetBuilder(builderId: string): void {
      delete builders.value[builderId]
      delete selections.value[builderId]
    }

    /**
     * Append a fresh question of `type` to `bankId`. Type-specific
     * fields are seeded by `defaultsForType`. Returns the new id.
     */
    function addQuestion(
      builderId: string,
      type: QuizQuestionType,
      bankId: string,
    ): string {
      const schema = ensureBuilder(builderId)
      const id = nextId(`q-${type}`)
      const defaults = defaultsForType(type)
      const question: QuizQuestion = {
        id,
        bankId,
        type,
        required: false,
        ...defaults,
      }
      schema.questions = [...schema.questions, question]
      return id
    }

    /** Remove a question by id. */
    function removeQuestion(builderId: string, questionId: string): void {
      const schema = ensureBuilder(builderId)
      schema.questions = schema.questions.filter((q) => q.id !== questionId)
      const sel = selections.value[builderId]
      if (sel?.id === questionId) {
        selections.value[builderId] = { id: null }
      }
    }

    /**
     * Patch a question's properties. When `patch.type` changes the
     * type, type-specific fields are reseeded and generic fields
     * (prompt, helpText, points, …) are preserved.
     */
    function updateQuestion(
      builderId: string,
      questionId: string,
      patch: Partial<Omit<QuizQuestion, "id" | "bankId">>,
    ): void {
      const schema = ensureBuilder(builderId)
      const idx = schema.questions.findIndex((q) => q.id === questionId)
      if (idx < 0) {
        return
      }
      const current = schema.questions[idx]!
      let merged: QuizQuestion
      if (patch.type && patch.type !== current.type) {
        const preserved: Partial<QuizQuestion> = {}
        for (const key of PRESERVED_KEYS) {
          if (current[key] !== undefined) {
            ;(preserved as Record<string, unknown>)[key] = current[key]
          }
        }
        const defaults = defaultsForType(patch.type)
        merged = {
          id: current.id,
          bankId: current.bankId,
          type: patch.type,
          ...defaults,
          ...preserved,
          // Caller-supplied fields win over both preserved + defaults
          // (except `type`, already locked above).
          ...patch,
        } as QuizQuestion
      } else {
        merged = { ...current, ...patch }
      }
      const next = [...schema.questions]
      next[idx] = merged
      schema.questions = next
    }

    /** Append a new empty option to a question. Returns the option id. */
    function addOption(builderId: string, questionId: string): string {
      const schema = ensureBuilder(builderId)
      const idx = schema.questions.findIndex((q) => q.id === questionId)
      if (idx < 0) {
        return ""
      }
      const current = schema.questions[idx]!
      const optId = nextId("opt")
      const newOption: QuizOption = {
        id: optId,
        label: `Option ${String.fromCharCode(65 + current.options.length)}`,
        value: "",
      }
      const next = [...schema.questions]
      next[idx] = { ...current, options: [...current.options, newOption] }
      schema.questions = next
      return optId
    }

    /** Remove an option from a question. */
    function removeOption(
      builderId: string,
      questionId: string,
      optionId: string,
    ): void {
      const schema = ensureBuilder(builderId)
      const idx = schema.questions.findIndex((q) => q.id === questionId)
      if (idx < 0) {
        return
      }
      const current = schema.questions[idx]!
      const next = [...schema.questions]
      next[idx] = {
        ...current,
        options: current.options.filter((o) => o.id !== optionId),
      }
      schema.questions = next
    }

    /** Patch an option's properties. */
    function updateOption(
      builderId: string,
      questionId: string,
      optionId: string,
      patch: Partial<Omit<QuizOption, "id">>,
    ): void {
      const schema = ensureBuilder(builderId)
      const idx = schema.questions.findIndex((q) => q.id === questionId)
      if (idx < 0) {
        return
      }
      const current = schema.questions[idx]!
      const optIdx = current.options.findIndex((o) => o.id === optionId)
      if (optIdx < 0) {
        return
      }
      const nextOptions = [...current.options]
      nextOptions[optIdx] = { ...nextOptions[optIdx]!, ...patch }
      if (patch.correct === true && current.type === "single-choice") {
        for (let i = 0; i < nextOptions.length; i++) {
          if (i !== optIdx) {
            nextOptions[i] = { ...nextOptions[i]!, correct: false }
          }
        }
      }
      const next = [...schema.questions]
      next[idx] = { ...current, options: nextOptions }
      schema.questions = next
    }

    /** Create a fresh question bank. Returns the new bank id. */
    function addBank(builderId: string, name?: string): string {
      const schema = ensureBuilder(builderId)
      const id = nextId("bank")
      const banks = schema.questionBanks
      const bank: QuestionBank = {
        id,
        name: name ?? `Bank ${banks.length + 1}`,
      }
      builders.value[builderId] = {
        ...schema,
        questionBanks: [...banks, bank],
      }
      return id
    }

    /**
     * Remove a bank by id along with any questions inside it. Refuses
     * to remove the last remaining bank — the schema must always have
     * at least one bank.
     */
    function removeBank(builderId: string, bankId: string): void {
      const schema = ensureBuilder(builderId)
      if (schema.questionBanks.length <= 1) {
        return
      }
      const sel = selections.value[builderId]
      builders.value[builderId] = {
        ...schema,
        questionBanks: schema.questionBanks.filter((b) => b.id !== bankId),
        questions: schema.questions.filter((q) => q.bankId !== bankId),
      }
      if (sel?.id) {
        const stillExists = builders.value[builderId]!.questions.some(
          (q) => q.id === sel.id,
        )
        if (!stillExists) {
          selections.value[builderId] = { id: null }
        }
      }
    }

    /** Patch a bank's properties (name, description). */
    function updateBank(
      builderId: string,
      bankId: string,
      patch: Partial<Omit<QuestionBank, "id">>,
    ): void {
      const schema = ensureBuilder(builderId)
      const banks = schema.questionBanks
      const idx = banks.findIndex((b) => b.id === bankId)
      if (idx < 0) {
        return
      }
      const next = [...banks]
      next[idx] = { ...next[idx]!, ...patch }
      builders.value[builderId] = { ...schema, questionBanks: next }
    }

    /**
     * Duplicate a question (with fresh id + fresh option ids), placing
     * the copy at the end of the same bank — or `toBankId` when given.
     * Returns the new question id.
     */
    function copyQuestion(
      builderId: string,
      questionId: string,
      toBankId?: string,
    ): string {
      const schema = ensureBuilder(builderId)
      const source = schema.questions.find((q) => q.id === questionId)
      if (!source) {
        return ""
      }
      const newId = nextId(`q-${source.type}`)
      const copy: QuizQuestion = {
        ...source,
        id: newId,
        bankId: toBankId ?? source.bankId,
        options: source.options.map((o) => ({ ...o, id: nextId("opt") })),
      }
      schema.questions = [...schema.questions, copy]
      return newId
    }

    /**
     * Move a question to a different index within its bank. `toIndex`
     * is relative to questions in the same bank; questions in other
     * banks retain their relative order.
     */
    function reorderQuestionInBank(
      builderId: string,
      bankId: string,
      questionId: string,
      toIndex: number,
    ): void {
      const schema = ensureBuilder(builderId)
      const target = schema.questions.find((q) => q.id === questionId)
      if (!target || target.bankId !== bankId) {
        return
      }
      const inScopeIds = schema.questions
        .filter((q) => q.bankId === bankId)
        .map((q) => q.id)
      const fromIdx = inScopeIds.indexOf(questionId)
      if (fromIdx < 0) {
        return
      }
      const clamped = Math.max(0, Math.min(toIndex, inScopeIds.length - 1))
      if (clamped === fromIdx) {
        return
      }
      inScopeIds.splice(fromIdx, 1)
      inScopeIds.splice(clamped, 0, questionId)

      let cursor = 0
      const next: QuizQuestion[] = []
      for (const q of schema.questions) {
        if (q.bankId === bankId) {
          const nextQId = inScopeIds[cursor]!
          cursor += 1
          const replacement = schema.questions.find((qq) => qq.id === nextQId)!
          next.push(replacement)
        } else {
          next.push(q)
        }
      }
      schema.questions = next
    }

    function select(builderId: string, id: string | null): void {
      selections.value[builderId] = { id }
    }

    function getSelection(builderId: string): QuizBuilderSelection {
      return selections.value[builderId] ?? { id: null }
    }

    function getSchema(builderId: string): QuizBuilderSchema {
      return ensureBuilder(builderId)
    }

    return {
      builders,
      selections,
      ensureBuilder,
      loadSchema,
      resetBuilder,
      addQuestion,
      removeQuestion,
      updateQuestion,
      addOption,
      removeOption,
      updateOption,
      addBank,
      removeBank,
      updateBank,
      copyQuestion,
      reorderQuestionInBank,
      select,
      getSelection,
      getSchema,
    }
  },
)

/** Composable wrapping store access for a single builder instance. */
export function useQuizBuilder(builderId: string) {
  const store = useQuizBuilderStore()
  store.ensureBuilder(builderId)
  return {
    schema: computed<QuizBuilderSchema>(() => store.getSchema(builderId)),
    selection: computed<QuizBuilderSelection>(() =>
      store.getSelection(builderId),
    ),
    addQuestion: (type: QuizQuestionType, bankId: string) =>
      store.addQuestion(builderId, type, bankId),
    removeQuestion: (id: string) => store.removeQuestion(builderId, id),
    updateQuestion: (
      id: string,
      patch: Partial<Omit<QuizQuestion, "id" | "bankId">>,
    ) => store.updateQuestion(builderId, id, patch),
    addOption: (questionId: string) => store.addOption(builderId, questionId),
    removeOption: (questionId: string, optionId: string) =>
      store.removeOption(builderId, questionId, optionId),
    updateOption: (
      questionId: string,
      optionId: string,
      patch: Partial<Omit<QuizOption, "id">>,
    ) => store.updateOption(builderId, questionId, optionId, patch),
    addBank: (name?: string) => store.addBank(builderId, name),
    removeBank: (bankId: string) => store.removeBank(builderId, bankId),
    updateBank: (bankId: string, patch: Partial<Omit<QuestionBank, "id">>) =>
      store.updateBank(builderId, bankId, patch),
    copyQuestion: (questionId: string, toBankId?: string) =>
      store.copyQuestion(builderId, questionId, toBankId),
    reorderQuestionInBank: (
      bankId: string,
      questionId: string,
      toIndex: number,
    ) => store.reorderQuestionInBank(builderId, bankId, questionId, toIndex),
    select: (id: string | null) => store.select(builderId, id),
    loadSchema: (schema: Partial<QuizBuilderSchema>) =>
      store.loadSchema(builderId, schema),
    reset: () => store.resetBuilder(builderId),
  }
}
