/**
 * Mode-params DSL: parse and format compact parameter strings.
 *
 * Exact TypeScript port of backend/app/engine/mode_params.py
 * (Python is the reference implementation for parsing; the Python
 * module has no formatter, so formatModeParams() is defined as the
 * canonical inverse of parseModeParams()).
 *
 * A mode-params string encodes voting formula parameters in a compact format.
 * Example: "D30M50B.1G.2T.1"
 *
 * Supported codes:
 *   D = duration_days (int)
 *   M = majority_pct (int, 0-100)
 *   B = base_exponent (float)
 *   G = gradient_exponent (float)
 *   C = constant_base (float)
 *   S = smith_exponent (float)
 *   T = techcomm_exponent (float)
 *   N = ratio_multiplier (float)
 *   R = is_ratio_mode (bool, 0 or 1)
 *
 * Values may start with a dot for decimals < 1, e.g. "B.1" means base_exponent=0.1.
 */

export interface ModeParams {
  duration_days: number
  majority_pct: number
  base_exponent: number
  gradient_exponent: number
  constant_base: number
  smith_exponent: number | null
  techcomm_exponent: number | null
  ratio_multiplier: number | null
  is_ratio_mode: boolean
}

type CodeType = 'int' | 'float' | 'bool'

// Ordered list of recognised codes and their target keys + types
const CODES: Record<string, { key: keyof ModeParams; type: CodeType }> = {
  D: { key: 'duration_days', type: 'int' },
  M: { key: 'majority_pct', type: 'int' },
  B: { key: 'base_exponent', type: 'float' },
  G: { key: 'gradient_exponent', type: 'float' },
  C: { key: 'constant_base', type: 'float' },
  S: { key: 'smith_exponent', type: 'float' },
  T: { key: 'techcomm_exponent', type: 'float' },
  N: { key: 'ratio_multiplier', type: 'float' },
  R: { key: 'is_ratio_mode', type: 'bool' },
}

// Regex: a single uppercase letter followed by a numeric value (int or float,
// possibly starting with '.' for values like .1 meaning 0.1)
const PARAM_RE = /([A-Z])(\d*\.?\d+)/g

function getDefaults(): ModeParams {
  return {
    duration_days: 30,
    majority_pct: 50,
    base_exponent: 0.1,
    gradient_exponent: 0.2,
    constant_base: 0.0,
    smith_exponent: null,
    techcomm_exponent: null,
    ratio_multiplier: null,
    is_ratio_mode: false,
  }
}

/**
 * Parse a mode-params string into a parameter object.
 *
 * Defaults are applied for any code not present in the string.
 *
 * @param paramsStr - Compact parameter string, e.g. "D30M50B.1G.2T.1"
 * @returns Parsed parameters with defaults for codes not found
 * @throws Error if an unrecognised code letter is found
 */
export function parseModeParams(paramsStr: string): ModeParams {
  const result = getDefaults()

  if (!paramsStr || !paramsStr.trim()) {
    return result
  }

  for (const match of paramsStr.matchAll(PARAM_RE)) {
    const code = match[1]!
    const rawValue = match[2]!

    const entry = CODES[code]
    if (!entry) {
      throw new Error(`Code de parametre inconnu : '${code}'`)
    }

    const { key, type } = entry

    if (type === 'int') {
      // Python: int(float(raw)) truncates toward zero
      ;(result as Record<keyof ModeParams, unknown>)[key] = Math.trunc(parseFloat(rawValue))
    } else if (type === 'float') {
      ;(result as Record<keyof ModeParams, unknown>)[key] = parseFloat(rawValue)
    } else {
      ;(result as Record<keyof ModeParams, unknown>)[key] = parseFloat(rawValue) !== 0.0
    }
  }

  return result
}

/**
 * Format a numeric value in DSL form: 0.1 -> ".1", 1 -> "1", 2.5 -> "2.5".
 */
function formatValue(value: number): string {
  const s = String(value)
  return value > 0 && value < 1 ? s.replace(/^0\./, '.') : s
}

/**
 * Format a parameter object back into a canonical mode-params string.
 *
 * Canonical form matches the strings used across the project
 * (e.g. "D30M50B.1G.2S.1"): D, M, B, G always present in that order,
 * then C when non-zero, then S/T/N when set, then R1 when ratio mode.
 * Round-trip: parseModeParams(formatModeParams(p)) is identical to p.
 *
 * @param params - Parameters to encode (missing keys take defaults)
 * @returns Compact string, e.g. "D30M50B.1G.2"
 */
export function formatModeParams(params: Partial<ModeParams> = {}): string {
  const p = getDefaults()
  for (const key of Object.keys(p) as Array<keyof ModeParams>) {
    const value = params[key]
    if (value !== undefined) {
      ;(p[key] as ModeParams[typeof key]) = value
    }
  }

  const parts: string[] = [
    `D${formatValue(p.duration_days)}`,
    `M${formatValue(p.majority_pct)}`,
    `B${formatValue(p.base_exponent)}`,
    `G${formatValue(p.gradient_exponent)}`,
  ]

  if (p.constant_base !== 0) {
    parts.push(`C${formatValue(p.constant_base)}`)
  }
  if (p.smith_exponent !== null) {
    parts.push(`S${formatValue(p.smith_exponent)}`)
  }
  if (p.techcomm_exponent !== null) {
    parts.push(`T${formatValue(p.techcomm_exponent)}`)
  }
  if (p.ratio_multiplier !== null) {
    parts.push(`N${formatValue(p.ratio_multiplier)}`)
  }
  if (p.is_ratio_mode) {
    parts.push('R1')
  }

  return parts.join('')
}
