import { validateNotation } from './validateNotation'

export type TokenType =
  | 'core'
  | 'dropLowest' // L, LN
  | 'dropHighest' // H, HN
  | 'dropCondition' // D{...}
  | 'keepHighest' // K, KN
  | 'keepLowest' // kl, klN
  | 'reroll' // R{...}
  | 'explode' // !
  | 'compound' // !!
  | 'penetrate' // !p
  | 'cap' // C{...}
  | 'replace' // V{...}
  | 'unique' // U, U{...}
  | 'countSuccesses' // S{...}
  | 'plus' // +N
  | 'minus' // -N
  | 'multiply' // *N
  | 'multiplyTotal' // **N
  | 'unknown'

export interface Token {
  readonly text: string
  readonly type: TokenType
  readonly start: number
  readonly end: number
  readonly description: string
}

interface ModifierEntry {
  readonly type: Exclude<TokenType, 'core' | 'unknown'>
  readonly pattern: RegExp
}

function describeCoreToken(text: string): string {
  const stripped = text.replace(/^[+-]/, '')
  const result = validateNotation(stripped)
  if (result.valid) return result.description[0]?.[0] ?? text
  const match = /^(\d+)[Dd](\d+)/.exec(stripped)
  if (!match) return text
  const qty = parseInt(match[1] ?? '1', 10)
  const sides = match[2] ?? '?'
  return `Roll ${qty} ${sides}-sided ${qty === 1 ? 'die' : 'dice'}`
}

function describeModifierToken(tokenText: string): string {
  const result = validateNotation(`1d6${tokenText}`)
  if (!result.valid) return tokenText
  const descriptions = result.description[0] ?? []
  const modifierDescriptions = descriptions.slice(1) // skip "Roll 1 6-sided die"
  return modifierDescriptions.join(', ') || tokenText
}

// Order matters — more specific patterns must come before ambiguous ones
const MODIFIERS: readonly ModifierEntry[] = [
  // ** before * to avoid partial match
  { type: 'multiplyTotal', pattern: /^\*\*\d+/ },
  { type: 'multiply', pattern: /^\*\d+/ },
  // !! and !p before ! to avoid partial match
  { type: 'compound', pattern: /^!!\d*/ },
  { type: 'penetrate', pattern: /^!p\d*/i },
  { type: 'explode', pattern: /^!/ },
  // Drop variants
  { type: 'dropHighest', pattern: /^[Hh]\d*/ },
  { type: 'dropLowest', pattern: /^[Ll]\d*/ },
  { type: 'dropCondition', pattern: /^[Dd]\{[^}]+\}/ },
  // Keep: kl before K to avoid K matching first char of kl
  { type: 'keepLowest', pattern: /^[Kk][Ll]\d*/ },
  { type: 'keepHighest', pattern: /^[Kk]\d*/ },
  // Brace-based modifiers — closing } required; partial input stays unknown
  { type: 'reroll', pattern: /^[Rr]\{[^}]+\}\d*/ },
  { type: 'cap', pattern: /^[Cc]\{[^}]+\}/ },
  { type: 'replace', pattern: /^[Vv]\{[^}]+\}/ },
  { type: 'unique', pattern: /^[Uu](?:\{[^}]+\})?/ },
  { type: 'countSuccesses', pattern: /^[Ss]\{\d+(?:,\d+)?\}/ },
  // Arithmetic — only meaningful after a core token
  { type: 'plus', pattern: /^\+\d+/ },
  { type: 'minus', pattern: /^-\d+/ }
]

function appendUnknown(tokens: Token[], char: string, cursor: number): void {
  const last = tokens[tokens.length - 1]
  if (last?.type === 'unknown') {
    tokens[tokens.length - 1] = { ...last, text: last.text + char, end: cursor + 1 }
  } else {
    tokens.push({ text: char, type: 'unknown', start: cursor, end: cursor + 1, description: '' })
  }
}

function parseFrom(notation: string, cursor: number, tokens: Token[]): readonly Token[] {
  if (cursor >= notation.length) return tokens

  const remaining = notation.slice(cursor)

  // A second dice pool (e.g. +1d20 in "1d6+1d20") must be detected before
  // the plus/minus modifier patterns would consume the leading +/- sign.
  const newPoolMatch = /^[+-]\d+[Dd][1-9]\d*/.exec(remaining)
  if (newPoolMatch) {
    const text = newPoolMatch[0]
    tokens.push({
      text,
      type: 'core',
      start: cursor,
      end: cursor + text.length,
      description: describeCoreToken(text)
    })
    return parseFrom(notation, cursor + text.length, tokens)
  }

  for (const entry of MODIFIERS) {
    const m = remaining.match(entry.pattern)
    if (m) {
      const text = m[0]
      tokens.push({
        text,
        type: entry.type,
        start: cursor,
        end: cursor + text.length,
        description: describeModifierToken(text)
      })
      return parseFrom(notation, cursor + text.length, tokens)
    }
  }

  appendUnknown(tokens, notation[cursor] ?? '', cursor)
  return parseFrom(notation, cursor + 1, tokens)
}

export function tokenize(notation: string): readonly Token[] {
  if (notation.length === 0) return []

  const tokens: Token[] = []
  const coreMatch = /^[+-]?\d+[Dd]\d+/.exec(notation)

  if (coreMatch) {
    const text = coreMatch[0]
    tokens.push({
      text,
      type: 'core',
      start: 0,
      end: text.length,
      description: describeCoreToken(text)
    })
    return parseFrom(notation, text.length, tokens)
  }

  return parseFrom(notation, 0, tokens)
}
