/**
 * SM-2 Algorithm Implementation
 *
 * Implements the SuperMemo SM-2 spaced repetition algorithm
 * as specified in the MemoHanzi specification.
 *
 * Reference: https://www.supermemo.com/en/archives1990-2015/english/ol/sm2
 */

import { Difficulty } from "@prisma/client"

/**
 * Progress data for a single card
 */
export interface CardProgress {
  easeFactor: number
  interval: number // in days
  consecutiveCorrect: number
  incorrectCount: number
  nextReviewDate: Date | null
}

/**
 * Initial values for a new card (without nextReviewDate as it's set on creation)
 */
export const INITIAL_PROGRESS = {
  easeFactor: 2.5,
  interval: 1,
  consecutiveCorrect: 0,
  incorrectCount: 0,
}

/**
 * Result of calculating the next review
 */
export interface ReviewResult {
  easeFactor: number
  interval: number
  consecutiveCorrect: number
  incorrectCount: number
  nextReviewDate: Date
}

/**
 * Calculate the next review for a correct answer
 *
 * @param progress Current card progress
 * @param reviewDate Date of the review (defaults to now)
 * @returns Updated progress values
 */
export function calculateCorrectAnswer(
  progress: CardProgress,
  reviewDate: Date = new Date()
): ReviewResult {
  let newInterval: number
  let newEaseFactor: number
  let newConsecutiveCorrect: number

  // Calculate new interval based on consecutive correct count
  if (progress.consecutiveCorrect === 0) {
    newInterval = 1
  } else if (progress.consecutiveCorrect === 1) {
    newInterval = 6
  } else {
    newInterval = Math.round(progress.interval * progress.easeFactor)
  }

  // Increase ease factor (making future intervals longer)
  newEaseFactor = progress.easeFactor + 0.1

  // Increment consecutive correct count
  newConsecutiveCorrect = progress.consecutiveCorrect + 1

  // Calculate next review date
  const nextReviewDate = new Date(reviewDate)
  nextReviewDate.setDate(nextReviewDate.getDate() + newInterval)

  return {
    easeFactor: newEaseFactor,
    interval: newInterval,
    consecutiveCorrect: newConsecutiveCorrect,
    incorrectCount: progress.incorrectCount,
    nextReviewDate,
  }
}

/**
 * Calculate the next review for an incorrect answer
 *
 * @param progress Current card progress
 * @param reviewDate Date of the review (defaults to now)
 * @returns Updated progress values
 */
export function calculateIncorrectAnswer(
  progress: CardProgress,
  reviewDate: Date = new Date()
): ReviewResult {
  // Reset interval to 1 day
  const newInterval = 1

  // Reset consecutive correct count
  const newConsecutiveCorrect = 0

  // Decrease ease factor (but not below 1.3)
  const newEaseFactor = Math.max(1.3, progress.easeFactor - 0.2)

  // Increment incorrect count
  const newIncorrectCount = progress.incorrectCount + 1

  // Calculate next review date (1 day from now)
  const nextReviewDate = new Date(reviewDate)
  nextReviewDate.setDate(nextReviewDate.getDate() + newInterval)

  return {
    easeFactor: newEaseFactor,
    interval: newInterval,
    consecutiveCorrect: newConsecutiveCorrect,
    incorrectCount: newIncorrectCount,
    nextReviewDate,
  }
}

/**
 * Re-export Difficulty enum from Prisma for convenience
 */
export { Difficulty }

/**
 * Shuffle array using Fisher-Yates algorithm
 */
function shuffleArray<T>(array: T[]): T[] {
  const shuffled = [...array]
  for (let i = shuffled.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1))
    ;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]
  }
  return shuffled
}

/**
 * Card for selection with progress and metadata
 */
export interface SelectableCard {
  id: string
  nextReviewDate: Date | null
  incorrectCount: number
  consecutiveCorrect: number
  manualDifficulty: Difficulty
}

/**
 * Select cards for a learning session
 *
 * Algorithm:
 * 1. Filter out SUSPENDED cards
 * 2. Filter cards that are due (nextReviewDate <= now)
 * 3. Apply priority: HARD cards first, NORMAL, then EASY
 * 4. Sort by: nextReviewDate ASC, incorrectCount DESC, consecutiveCorrect ASC
 * 5. Limit to cardsPerSession
 *
 * @param cards Available cards
 * @param cardsPerSession Maximum number of cards to select
 * @param now Current date (defaults to now)
 * @returns Selected cards for the session
 */
export function selectCardsForSession(
  cards: SelectableCard[],
  cardsPerSession: number,
  now: Date = new Date(),
  shuffle: boolean = true
): SelectableCard[] {
  // Filter out suspended cards
  const activeCards = cards.filter(
    (card) => card.manualDifficulty !== Difficulty.SUSPENDED
  )

  // Filter cards that are due (nextReviewDate <= now or null for new cards)
  const dueCards = activeCards.filter(
    (card) => card.nextReviewDate === null || card.nextReviewDate <= now
  )

  // Apply difficulty priority and sort
  const sortedCards = dueCards.sort((a, b) => {
    // Priority by difficulty: HARD > NORMAL > EASY
    const difficultyPriority = {
      [Difficulty.HARD]: 0,
      [Difficulty.MEDIUM]: 1,
      [Difficulty.EASY]: 2,
      [Difficulty.SUSPENDED]: 3, // Should not appear due to filter
    }

    const aPriority = difficultyPriority[a.manualDifficulty]
    const bPriority = difficultyPriority[b.manualDifficulty]

    if (aPriority !== bPriority) {
      return aPriority - bPriority
    }

    // Sort by nextReviewDate (null = new cards, should come first)
    if (a.nextReviewDate === null && b.nextReviewDate !== null) return -1
    if (a.nextReviewDate !== null && b.nextReviewDate === null) return 1
    if (a.nextReviewDate !== null && b.nextReviewDate !== null) {
      const dateCompare = a.nextReviewDate.getTime() - b.nextReviewDate.getTime()
      if (dateCompare !== 0) return dateCompare
    }

    // Sort by incorrectCount DESC (more incorrect = higher priority)
    if (a.incorrectCount !== b.incorrectCount) {
      return b.incorrectCount - a.incorrectCount
    }

    // Sort by consecutiveCorrect ASC (fewer correct = higher priority)
    if (a.consecutiveCorrect !== b.consecutiveCorrect) {
      return a.consecutiveCorrect - b.consecutiveCorrect
    }

    // Random tiebreaker for cards with equal priority
    return Math.random() - 0.5
  })

  // Limit to cardsPerSession
  const selectedCards = sortedCards.slice(0, cardsPerSession)

  // Final shuffle: randomize the order of selected cards for presentation
  // This prevents always showing hard/struggling cards first
  return shuffle ? shuffleArray(selectedCards) : selectedCards
}

/**
 * Hanzi option for wrong answer generation
 */
export interface HanziOption {
  id: string
  simplified: string
  pinyin: string
  hskLevel: string
}

/**
 * Generate wrong answers for a multiple choice question
 *
 * Selects 3 random incorrect pinyin from the same HSK level,
 * ensuring no duplicates.
 *
 * @param correctAnswer The correct hanzi
 * @param sameHskOptions Available hanzi from the same HSK level
 * @returns Array of 3 wrong pinyin options
 */
export function generateWrongAnswers(
  correctAnswer: HanziOption,
  sameHskOptions: HanziOption[]
): string[] {
  // Filter out the correct answer and any with duplicate pinyin
  const candidates = sameHskOptions.filter(
    (option) =>
      option.id !== correctAnswer.id && option.pinyin !== correctAnswer.pinyin
  )

  // If not enough candidates, throw error
  if (candidates.length < 3) {
    throw new Error(
      `Not enough wrong answers available. Need 3, found ${candidates.length}`
    )
  }

  // Fisher-Yates shuffle
  const shuffled = [...candidates]
  for (let i = shuffled.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1))
    ;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]
  }

  // Take first 3
  return shuffled.slice(0, 3).map((option) => option.pinyin)
}

/**
 * Shuffle an array of options (for randomizing answer positions)
 * Uses Fisher-Yates shuffle algorithm
 *
 * @param options Array to shuffle
 * @returns Shuffled array
 */
export function shuffleOptions<T>(options: T[]): T[] {
  const shuffled = [...options]
  for (let i = shuffled.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1))
    ;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]
  }
  return shuffled
}
