// Package vocab implements the vocabulary garden: it captures words the writer
// looks up and schedules them for gentle spaced-repetition review. The scheduler
// is a deliberately forgiving SM-2-lite / Leitner hybrid — no streaks to break,
// no harsh resets beyond a single step back — because this is a confidence
// builder, not a drill sergeant.
package vocab

import "math"

// Grade is the writer's self-assessment after seeing a flashcard's answer.
type Grade string

const (
	GradeAgain Grade = "again" // didn't recall it — show again soon
	GradeGood  Grade = "good"  // recalled it — advance one rung
	GradeEasy  Grade = "easy"  // knew it instantly — advance a little further
)

// ladder is the Leitner interval ladder in days for the first several successful
// reviews: 1 → 3 → 7 → 16 → 35. Beyond the ladder, intervals grow by the card's
// ease factor so well-known words drift far into the future.
var ladder = []int{1, 3, 7, 16, 35}

const (
	minEase        = 1.3
	maxEase        = 3.0  // ease ceiling — keeps "easy" from compounding growth without bound
	startEase      = 2.5
	easyBonus      = 1.3   // multiplier applied on top of a "good" step for "easy"
	easeAgainDelta = -0.20 // ease nudged down on a lapse (still floored at minEase)
	easeEasyDelta  = 0.15  // ease nudged up when a card feels easy

	// maxInterval caps how far a card can drift into the future. Even a word
	// graded "easy" many times resurfaces about once a year, so nothing silently
	// leaves the garden forever (the whole point is to keep words in gentle
	// rotation, not to retire them).
	maxInterval = 365
)

// State is the spaced-repetition state of one card. It mirrors the scheduling
// columns on vocab_words so a review is "load State → next(grade) → persist".
type State struct {
	Reps     int
	Interval int // days until the next review
	Ease     float64
	Lapses   int
}

// next returns the card's state after a review with the given grade. It never
// mutates the receiver. "again" steps the card back to a 1-day interval and
// counts a lapse (but only nudges ease down, never wipes progress harshly);
// "good" advances one rung of the ladder; "easy" advances a rung and a bit more.
func (s State) next(grade Grade) State {
	ease := s.Ease
	if ease == 0 {
		ease = startEase
	}

	switch grade {
	case GradeAgain:
		return State{
			Reps:     0,
			Interval: 1,
			Ease:     clampEase(ease + easeAgainDelta),
			Lapses:   s.Lapses + 1,
		}
	case GradeEasy:
		ease = clampEase(ease + easeEasyDelta)
		reps := s.Reps + 1
		return State{
			Reps:     reps,
			Interval: clampInterval(int(math.Round(float64(goodInterval(reps, s.Interval, ease)) * easyBonus))),
			Ease:     ease,
			Lapses:   s.Lapses,
		}
	default: // GradeGood
		reps := s.Reps + 1
		return State{
			Reps:     reps,
			Interval: clampInterval(goodInterval(reps, s.Interval, ease)),
			Ease:     ease,
			Lapses:   s.Lapses,
		}
	}
}

// goodInterval returns the day-interval for a successful review: the explicit
// ladder while it lasts, then geometric growth by ease once past it.
func goodInterval(reps, prevInterval int, ease float64) int {
	if reps >= 1 && reps <= len(ladder) {
		return ladder[reps-1]
	}
	prev := prevInterval
	if prev < ladder[len(ladder)-1] {
		prev = ladder[len(ladder)-1]
	}
	return int(math.Round(float64(prev) * ease))
}

func clampEase(e float64) float64 {
	if e < minEase {
		return minEase
	}
	if e > maxEase {
		return maxEase
	}
	return e
}

func clampInterval(d int) int {
	if d > maxInterval {
		return maxInterval
	}
	return d
}
