import 'dart:collection';
import 'dart:math' as math;

import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
import 'package:asciineverdie/src/core/model/equipment_slot.dart';
import 'package:asciineverdie/src/core/model/game_state.dart';
import 'package:asciineverdie/src/core/model/pq_config.dart';
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
import 'package:asciineverdie/src/core/util/roman.dart';

// Mirrors core utility functions from the original Delphi sources (Main.pas / NewGuy.pas).

/// 장비 생성 결과 (구조화된 데이터로 l10n 지원)
class EquipResult {
  const EquipResult({
    required this.baseName,
    this.modifiers = const [],
    this.plusValue = 0,
  });

  /// 기본 장비 이름 (예: "VPN Cloak")
  final String baseName;

  /// 수식어 목록 (예: ["Holey", "Deprecated"])
  final List<String> modifiers;

  /// +/- 수치 (예: -1, +2)
  final int plusValue;

  /// 영문 전체 이름 생성 (기존 방식)
  String get displayName {
    var name = baseName;
    for (final mod in modifiers) {
      name = '$mod $name';
    }
    if (plusValue != 0) {
      name = '${plusValue > 0 ? '+' : ''}$plusValue $name';
    }
    return name;
  }
}

/// 아이템 생성 결과 (구조화된 데이터로 l10n 지원)
class ItemResult {
  const ItemResult({this.attrib, this.special, this.itemOf, this.boringItem});

  /// 아이템 속성 (예: "Golden")
  final String? attrib;

  /// 특수 아이템 (예: "Iterator")
  final String? special;

  /// "~의" 접미사 (예: "Monitoring")
  final String? itemOf;

  /// 단순 아이템 (보링 아이템용)
  final String? boringItem;

  /// 영문 전체 이름 생성 (기존 방식)
  String get displayName {
    if (boringItem != null) return boringItem!;
    if (attrib != null && special != null && itemOf != null) {
      return '$attrib $special of $itemOf';
    }
    if (attrib != null && special != null) {
      return '$attrib $special';
    }
    return '';
  }
}

int levelUpTimeSeconds(int level) {
  // 10시간 내 레벨 100 도달 목표 (선형 성장)
  // 레벨 1: ~2분, 레벨 100: ~7분
  final seconds = 120 + (level * 3);
  return seconds;
}

/// 초 단위 시간을 사람이 읽기 쉬운 형태로 변환 (원본 Main.pas:1265-1271)
/// l10n 지원
String roughTime(int seconds) {
  if (seconds < 120) {
    return l10n.roughTimeSeconds(seconds);
  } else if (seconds < 60 * 120) {
    return l10n.roughTimeMinutes(seconds ~/ 60);
  } else if (seconds < 60 * 60 * 48) {
    return l10n.roughTimeHours(seconds ~/ 3600);
  } else {
    return l10n.roughTimeDays(seconds ~/ (3600 * 24));
  }
}

String pluralize(String s) {
  if (_ends(s, 'y')) return '${s.substring(0, s.length - 1)}ies';
  if (_ends(s, 'us')) return '${s.substring(0, s.length - 2)}i';
  if (_ends(s, 'ch') || _ends(s, 'x') || _ends(s, 's')) return '${s}es';
  if (_ends(s, 'f')) return '${s.substring(0, s.length - 1)}ves';
  if (_ends(s, 'man') || _ends(s, 'Man')) {
    return '${s.substring(0, s.length - 2)}en';
  }
  return '${s}s';
}

String indefinite(String s, int qty) {
  if (qty == 1) {
    const vowels = 'AEIOUÜaeiouü';
    final first = s.isNotEmpty ? s[0] : 'a';
    final article = vowels.contains(first) ? 'an' : 'a';
    return '$article $s';
  }
  return '$qty ${pluralize(s)}';
}

String definite(String s, int qty) {
  if (qty > 1) {
    s = pluralize(s);
  }
  return 'the $s';
}

String generateName(DeterministicRandom rng) {
  const kParts = [
    'br|cr|dr|fr|gr|j|kr|l|m|n|pr||||r|sh|tr|v|wh|x|y|z',
    'a|a|e|e|i|i|o|o|u|u|ae|ie|oo|ou',
    'b|ck|d|g|k|m|n|p|t|v|x|z',
  ];

  var result = '';
  for (var i = 0; i <= 5; i++) {
    result += _pick(kParts[i % 3], rng);
  }
  if (result.isEmpty) return result;
  return '${result[0].toUpperCase()}${result.substring(1)}';
}

// Random helpers
int randomLow(DeterministicRandom rng, int below) {
  return math.min(rng.nextInt(below), rng.nextInt(below));
}

String pick(List<String> values, DeterministicRandom rng) {
  if (values.isEmpty) return '';
  return values[rng.nextInt(values.length)];
}

String pickLow(List<String> values, DeterministicRandom rng) {
  if (values.isEmpty) return '';
  return values[randomLow(rng, values.length)];
}

// Item name generators (match Main.pas)
String boringItem(PqConfig config, DeterministicRandom rng) {
  return pick(config.boringItems, rng);
}

String interestingItem(PqConfig config, DeterministicRandom rng) {
  final attr = pick(config.itemAttrib, rng);
  final special = pick(config.specials, rng);
  return '$attr $special';
}

String specialItem(PqConfig config, DeterministicRandom rng) {
  return '${interestingItem(config, rng)} of ${pick(config.itemOfs, rng)}';
}

/// 구조화된 아이템 결과 반환 (l10n 지원)
ItemResult boringItemStructured(PqConfig config, DeterministicRandom rng) {
  return ItemResult(boringItem: pick(config.boringItems, rng));
}

/// 구조화된 아이템 결과 반환 (l10n 지원)
ItemResult interestingItemStructured(PqConfig config, DeterministicRandom rng) {
  return ItemResult(
    attrib: pick(config.itemAttrib, rng),
    special: pick(config.specials, rng),
  );
}

/// 구조화된 아이템 결과 반환 (l10n 지원)
ItemResult specialItemStructured(PqConfig config, DeterministicRandom rng) {
  return ItemResult(
    attrib: pick(config.itemAttrib, rng),
    special: pick(config.specials, rng),
    itemOf: pick(config.itemOfs, rng),
  );
}

/// 구조화된 아이템 결과 반환 (l10n 지원)
ItemResult winItemStructured(
  PqConfig config,
  DeterministicRandom rng,
  int inventoryCount,
) {
  final threshold = math.max(250, rng.nextInt(999));
  if (inventoryCount > threshold) {
    return const ItemResult(); // 빈 결과
  }
  return specialItemStructured(config, rng);
}

String pickWeapon(PqConfig config, DeterministicRandom rng, int level) {
  return _lPick(config.weapons, rng, level);
}

String pickShield(PqConfig config, DeterministicRandom rng, int level) {
  return _lPick(config.shields, rng, level);
}

String pickArmor(PqConfig config, DeterministicRandom rng, int level) {
  return _lPick(config.armors, rng, level);
}

String pickSpell(PqConfig config, DeterministicRandom rng, int goalLevel) {
  return _lPick(config.spells, rng, goalLevel);
}

/// 원본 Main.pas:776-789 LPick: 6회 시도하여 목표 레벨에 가장 가까운 아이템 선택
String _lPick(List<String> items, DeterministicRandom rng, int goal) {
  if (items.isEmpty) return '';
  var result = pick(items, rng);
  var bestLevel = _parseLevel(result);

  for (var i = 0; i < 5; i++) {
    final candidate = pick(items, rng);
    final candLevel = _parseLevel(candidate);
    if ((goal - candLevel).abs() < (goal - bestLevel).abs()) {
      result = candidate;
      bestLevel = candLevel;
    }
  }
  return result;
}

int _parseLevel(String entry) {
  final parts = entry.split('|');
  if (parts.length < 2) return 0;
  return int.tryParse(parts[1].replaceAll('+', '')) ?? 0;
}

String addModifier(
  DeterministicRandom rng,
  String baseName,
  List<String> modifiers,
  int plus,
) {
  var name = baseName;
  var remaining = plus;
  var count = 0;

  while (count < 2 && remaining != 0) {
    final modifier = pick(modifiers, rng);
    final parts = modifier.split('|');
    if (parts.isEmpty) break;
    final label = parts[0];
    final qual = parts.length > 1 ? int.tryParse(parts[1]) ?? 0 : 0;
    if (name.contains(label)) break; // avoid repeats
    if (remaining.abs() < qual.abs()) break;
    name = '$label $name';
    remaining -= qual;
    count++;
  }

  if (remaining != 0) {
    name = '${remaining > 0 ? '+' : ''}$remaining $name';
  }
  return name;
}

// Character/stat growth
int levelUpTime(int level) => levelUpTimeSeconds(level);

String winSpell(
  PqConfig config,
  DeterministicRandom rng,
  int wisdom,
  int level,
) {
  // 원본 Main.pas:770-774: RandomLow로 인덱스 선택 (리스트 앞쪽 선호)
  final maxIndex = math.min(wisdom + level, config.spells.length);
  if (maxIndex <= 0) return '';
  final index = randomLow(rng, maxIndex);
  final entry = config.spells[index];
  final parts = entry.split('|');
  final name = parts[0];
  final currentRank = romanToInt(parts.length > 1 ? parts[1] : 'I');
  final nextRank = math.max(1, currentRank + 1);
  return '$name|${intToRoman(nextRank)}';
}

String winItem(PqConfig config, DeterministicRandom rng, int inventoryCount) {
  // If inventory is already very large, signal caller to duplicate an existing item.
  final threshold = math.max(250, rng.nextInt(999));
  if (inventoryCount > threshold) return '';
  return specialItem(config, rng);
}

int rollStat(DeterministicRandom rng) {
  // 3d6 roll.
  return 3 + rng.nextInt(6) + rng.nextInt(6) + rng.nextInt(6);
}

int random64Below(DeterministicRandom rng, int below) {
  if (below <= 0) return 0;
  final hi = rng.nextUint32();
  final lo = rng.nextUint32();
  final combined = (hi << 32) | lo;
  return (combined % below).toInt();
}

/// 장비 생성 (원본 Main.pas:791-830 WinEquip)
/// [slotIndex]: 0=Weapon, 1=Shield, 2-10=Armor 계열
String winEquip(
  PqConfig config,
  DeterministicRandom rng,
  int level,
  int slotIndex,
) {
  // 원본 로직:
  // posn = 0: Weapon → K.Weapons, OffenseAttrib
  // posn = 1: Shield → K.Shields, DefenseAttrib
  // posn >= 2: Armor → K.Armors, DefenseAttrib
  final bool isWeapon = slotIndex == 0;
  final List<String> items;
  if (slotIndex == 0) {
    items = config.weapons;
  } else if (slotIndex == 1) {
    items = config.shields;
  } else {
    items = config.armors;
  }
  final better = isWeapon ? config.offenseAttrib : config.defenseAttrib;
  final worse = isWeapon ? config.offenseBad : config.defenseBad;

  final base = _lPick(items, rng, level);
  final parts = base.split('|');
  final baseName = parts[0];
  final qual = parts.length > 1
      ? int.tryParse(parts[1].replaceAll('+', '')) ?? 0
      : 0;

  final plus = level - qual;
  final modifiers = plus >= 0 ? better : worse;
  return addModifier(rng, baseName, modifiers, plus);
}

/// EquipmentSlot enum을 사용하는 편의 함수
String winEquipBySlot(
  PqConfig config,
  DeterministicRandom rng,
  int level,
  EquipmentSlot slot,
) {
  return winEquip(config, rng, level, slot.index);
}

/// 구조화된 장비 생성 결과 반환 (l10n 지원)
EquipResult winEquipStructured(
  PqConfig config,
  DeterministicRandom rng,
  int level,
  int slotIndex,
) {
  final bool isWeapon = slotIndex == 0;
  final List<String> items;
  if (slotIndex == 0) {
    items = config.weapons;
  } else if (slotIndex == 1) {
    items = config.shields;
  } else {
    items = config.armors;
  }
  final better = isWeapon ? config.offenseAttrib : config.defenseAttrib;
  final worse = isWeapon ? config.offenseBad : config.defenseBad;

  final base = _lPick(items, rng, level);
  final parts = base.split('|');
  final baseName = parts[0];
  final qual = parts.length > 1
      ? int.tryParse(parts[1].replaceAll('+', '')) ?? 0
      : 0;

  final plus = level - qual;
  final modifierList = plus >= 0 ? better : worse;
  return _addModifierStructured(rng, baseName, modifierList, plus);
}

/// 구조화된 장비 결과 반환 (내부 함수)
EquipResult _addModifierStructured(
  DeterministicRandom rng,
  String baseName,
  List<String> modifiers,
  int plus,
) {
  final collectedModifiers = <String>[];
  var remaining = plus;
  var count = 0;

  while (count < 2 && remaining != 0) {
    final modifier = pick(modifiers, rng);
    final parts = modifier.split('|');
    if (parts.isEmpty) break;
    final label = parts[0];
    final qual = parts.length > 1 ? int.tryParse(parts[1]) ?? 0 : 0;
    if (collectedModifiers.contains(label)) break; // avoid repeats
    if (remaining.abs() < qual.abs()) break;
    collectedModifiers.add(label);
    remaining -= qual;
    count++;
  }

  return EquipResult(
    baseName: baseName,
    modifiers: collectedModifiers,
    plusValue: remaining,
  );
}

/// EquipmentSlot enum을 사용하는 구조화된 버전
EquipResult winEquipBySlotStructured(
  PqConfig config,
  DeterministicRandom rng,
  int level,
  EquipmentSlot slot,
) {
  return winEquipStructured(config, rng, level, slot.index);
}

int winStatIndex(DeterministicRandom rng, List<int> statValues) {
  // 원본 Main.pas:870-883
  // 50%: 모든 8개 스탯 중 랜덤
  // 50%: 첫 6개(STR~CHA)만 제곱 가중치로 선택
  if (rng.nextInt(2) == 0) {
    // Odds(1,2): 모든 스탯 중 완전 랜덤 선택
    return rng.nextInt(statValues.length);
  }
  // 원본: for i := 0 to 5 do Inc(t, Square(GetI(Stats,i)));
  // 첫 6개(STR, CON, DEX, INT, WIS, CHA)만 제곱 가중치 적용
  const firstSixCount = 6;
  var total = 0;
  for (var i = 0; i < firstSixCount && i < statValues.length; i++) {
    total += statValues[i] * statValues[i];
  }
  if (total == 0) return rng.nextInt(firstSixCount);
  var pickValue = random64Below(rng, total);
  for (var i = 0; i < firstSixCount; i++) {
    pickValue -= statValues[i] * statValues[i];
    if (pickValue < 0) return i;
  }
  return firstSixCount - 1;
}

Stats winStat(Stats stats, DeterministicRandom rng) {
  final values = <int>[
    stats.str,
    stats.con,
    stats.dex,
    stats.intelligence,
    stats.wis,
    stats.cha,
    stats.hpMax,
    stats.mpMax,
  ];
  final idx = winStatIndex(rng, values);
  switch (idx) {
    case 0:
      return stats.copyWith(str: stats.str + 1);
    case 1:
      return stats.copyWith(con: stats.con + 1);
    case 2:
      return stats.copyWith(dex: stats.dex + 1);
    case 3:
      return stats.copyWith(intelligence: stats.intelligence + 1);
    case 4:
      return stats.copyWith(wis: stats.wis + 1);
    case 5:
      return stats.copyWith(cha: stats.cha + 1);
    case 6:
      return stats.copyWith(hpMax: stats.hpMax + 1);
    case 7:
      return stats.copyWith(mpMax: stats.mpMax + 1);
    default:
      return stats;
  }
}

MonsterTaskResult monsterTask(
  PqConfig config,
  DeterministicRandom rng,
  int level,
  String? questMonster, // optional monster name from quest
  int? questLevel,
) {
  var targetLevel = level;

  for (var i = level; i > 0; i--) {
    if (rng.nextInt(5) < 2) {
      targetLevel += rng.nextInt(2) * 2 - 1; // RandSign
    }
  }
  if (targetLevel < 1) targetLevel = 1;

  String monster;
  int monsterLevel;
  String part;
  bool definite = false;

  // 원본 Main.pas:537-547: 가끔 NPC를 몬스터로 사용
  if (rng.nextInt(25) == 0) {
    final raceEn = pick(config.races, rng).split('|').first;
    final race = l10n.translateRace(raceEn);
    if (rng.nextInt(2) == 0) {
      // 'passing Race Class' 형태
      final klassEn = pick(config.klasses, rng).split('|').first;
      final klass = l10n.translateKlass(klassEn);
      monster = l10n.modifierPassing('$race $klass');
    } else {
      // 'Title Name the Race' 형태 (원본은 PickLow(Titles) 사용)
      final titleEn = pickLow(config.titles, rng);
      final title = l10n.translateTitle(titleEn);
      monster = l10n.namedMonsterFormat(generateName(rng), '$title $race');
      definite = true;
    }
    monsterLevel = targetLevel;
    part = '*'; // NPC는 WinItem 호출
  } else if (questMonster != null && rng.nextInt(4) == 0) {
    // Use quest monster.
    monster = questMonster;
    final parts = questMonster.split('|');
    monsterLevel = questLevel ?? targetLevel;
    part = parts.length > 2 ? parts[2] : '';
  } else {
    // Pick closest level among random samples.
    monster = pick(config.monsters, rng);
    monsterLevel = _monsterLevel(monster);
    for (var i = 0; i < 5; i++) {
      final candidate = pick(config.monsters, rng);
      final candLevel = _monsterLevel(candidate);
      if ((targetLevel - candLevel).abs() <
          (targetLevel - monsterLevel).abs()) {
        monster = candidate;
        monsterLevel = candLevel;
      }
    }
    // 몬스터 데이터에서 부위 정보 추출 (예: "Rat|0|tail")
    final monsterParts = monster.split('|');
    part = monsterParts.length > 2 ? monsterParts[2] : '';
  }

  // 기본 몬스터 이름 (부위 정보 제외)
  final baseName = monster.split('|').first;

  // Adjust quantity and adjectives based on level delta.
  var qty = 1;
  final levelDiff = targetLevel - monsterLevel;
  // 몬스터 이름 번역 (l10n 지원)
  var name = l10n.translateMonster(baseName);

  if (levelDiff > 10) {
    qty =
        (targetLevel + rng.nextInt(monsterLevel == 0 ? 1 : monsterLevel)) ~/
        (monsterLevel == 0 ? 1 : monsterLevel);
    if (qty < 1) qty = 1;
    targetLevel ~/= qty;
  }

  if (levelDiff <= -10) {
    name = l10n.modifierImaginary(name);
  } else if (levelDiff < -5) {
    final i = 5 - rng.nextInt(10 + levelDiff + 1);
    name = _sick(i, _young((monsterLevel - targetLevel) - i, name));
  } else if (levelDiff < 0) {
    if (rng.nextInt(2) == 1) {
      name = _sick(levelDiff, name);
    } else {
      name = _young(levelDiff, name);
    }
  } else if (levelDiff >= 10) {
    name = l10n.modifierMessianic(name);
  } else if (levelDiff > 5) {
    final i = 5 - rng.nextInt(10 - levelDiff + 1);
    name = _big(i, _special((levelDiff) - i, name));
  } else if (levelDiff > 0) {
    if (rng.nextInt(2) == 1) {
      name = _big(levelDiff, name);
    } else {
      name = _special(levelDiff, name);
    }
  }

  if (!definite) {
    // l10n 지원: 한국어/일본어에서는 관사 불필요
    name = l10n.indefiniteL10n(name, qty);
  }

  return MonsterTaskResult(
    displayName: name,
    baseName: baseName,
    level: monsterLevel * qty,
    part: part,
  );
}

/// monsterTask의 반환 타입 (원본 fTask.Caption 정보 포함)
class MonsterTaskResult {
  const MonsterTaskResult({
    required this.displayName,
    required this.baseName,
    required this.level,
    required this.part,
  });

  /// 화면에 표시할 몬스터 이름 (형용사 포함, 예: "a sick Goblin")
  final String displayName;

  /// 기본 몬스터 이름 (형용사 제외, 예: "Goblin")
  final String baseName;

  /// 몬스터 레벨
  final int level;

  /// 전리품 부위 (예: "claw", "tail", "*"는 WinItem 호출)
  final String part;
}

enum RewardKind { spell, equip, stat, item }

class QuestResult {
  const QuestResult({
    required this.caption,
    required this.reward,
    this.monsterName,
    this.monsterLevel,
    this.monsterIndex,
  });

  final String caption;
  final RewardKind reward;

  /// 몬스터 전체 데이터 (예: "Rat|5|tail") - 원본 fQuest.Caption
  final String? monsterName;

  /// 몬스터 레벨 (파싱된 값)
  final int? monsterLevel;

  /// 몬스터 인덱스 (config.monsters에서의 위치) - 원본 fQuest.Tag
  final int? monsterIndex;
}

QuestResult completeQuest(PqConfig config, DeterministicRandom rng, int level) {
  final rewardRoll = rng.nextInt(4);
  final reward = switch (rewardRoll) {
    0 => RewardKind.spell,
    1 => RewardKind.equip,
    2 => RewardKind.stat,
    _ => RewardKind.item,
  };

  final questRoll = rng.nextInt(5);
  switch (questRoll) {
    case 0:
      // Exterminate: 4번 시도하여 레벨에 가장 가까운 몬스터 선택
      // 원본 Main.pas:936-954
      var best = '';
      var bestLevel = 0;
      var bestIndex = 0;
      for (var i = 0; i < 4; i++) {
        final monsterIndex = rng.nextInt(config.monsters.length);
        final m = config.monsters[monsterIndex];
        final l = _monsterLevel(m);
        if (i == 0 || (l - level).abs() < (bestLevel - level).abs()) {
          best = m;
          bestLevel = l;
          bestIndex = monsterIndex;
        }
      }
      final nameEn = best.split('|').first;
      final name = l10n.translateMonster(nameEn);
      return QuestResult(
        caption: l10n.questPatch(l10n.definiteL10n(name, 2)),
        reward: reward,
        monsterName: best,
        monsterLevel: bestLevel,
        monsterIndex: bestIndex,
      );
    case 1:
      // interestingItem: attrib + special 조합 후 번역
      final attr = pick(config.itemAttrib, rng);
      final special = pick(config.specials, rng);
      final item = l10n.translateInterestingItem(attr, special);
      return QuestResult(
        caption: l10n.questLocate(l10n.definiteL10n(item, 1)),
        reward: reward,
      );
    case 2:
      final itemEn = boringItem(config, rng);
      final item = l10n.translateBoringItem(itemEn);
      return QuestResult(caption: l10n.questTransfer(item), reward: reward);
    case 3:
      final itemEn = boringItem(config, rng);
      final item = l10n.translateBoringItem(itemEn);
      return QuestResult(
        caption: l10n.questDownload(l10n.indefiniteL10n(item, 1)),
        reward: reward,
      );
    default:
      // Stabilize: 2번 시도하여 레벨에 가장 가까운 몬스터 선택
      // 원본 Main.pas:971-984 (fQuest.Caption := '' 처리됨)
      var best = '';
      var bestLevel = 0;
      for (var i = 0; i < 2; i++) {
        final m = pick(config.monsters, rng);
        final l = _monsterLevel(m);
        if (i == 0 || (l - level).abs() < (bestLevel - level).abs()) {
          best = m;
          bestLevel = l;
        }
      }
      final nameEn = best.split('|').first;
      final name = l10n.translateMonster(nameEn);
      // Stabilize는 fQuest.Caption := '' 로 비움 → monsterIndex 미저장
      return QuestResult(
        caption: l10n.questStabilize(l10n.definiteL10n(name, 2)),
        reward: reward,
      );
  }
}

class ActResult {
  const ActResult({
    required this.actTitle,
    required this.plotBarMaxSeconds,
    required this.rewards,
  });

  final String actTitle;
  final int plotBarMaxSeconds;
  final List<RewardKind> rewards;
}

/// Act별 Plot Bar 최대값 (초) - 10시간 완주 목표
const _actPlotBarSeconds = [
  300, // Prologue: 5분
  7200, // Act I: 2시간
  10800, // Act II: 3시간
  10800, // Act III: 3시간
  5400, // Act IV: 1.5시간
  1800, // Act V: 30분
];

ActResult completeAct(int existingActCount) {
  final nextActIndex = existingActCount;
  final title = l10n.actTitle(intToRoman(nextActIndex));
  final plotBarMax = existingActCount < _actPlotBarSeconds.length
      ? _actPlotBarSeconds[existingActCount]
      : 3600;

  final rewards = <RewardKind>[];
  if (existingActCount > 1) {
    rewards.add(RewardKind.item);
  }
  if (existingActCount > 2) {
    rewards.add(RewardKind.equip);
  }

  return ActResult(
    actTitle: title,
    plotBarMaxSeconds: plotBarMax,
    rewards: rewards,
  );
}

class TaskResult {
  const TaskResult({
    required this.caption,
    required this.durationMillis,
    required this.progress,
  });

  final String caption;
  final int durationMillis;
  final ProgressState progress;
}

/// Starts a task: resets task bar and sets caption.
TaskResult startTask(
  ProgressState progress,
  String caption,
  int durationMillis,
) {
  final updated = progress.copyWith(
    task: ProgressBarState(position: 0, max: durationMillis),
  );
  return TaskResult(
    caption: '$caption...',
    durationMillis: durationMillis,
    progress: updated,
  );
}

class DequeueResult {
  const DequeueResult({
    required this.progress,
    required this.queue,
    required this.caption,
    required this.taskType,
    required this.kind,
  });

  final ProgressState progress;
  final QueueState queue;
  final String caption;
  final TaskType taskType;
  final QueueKind kind;
}

/// Process the queue when current task is done. Returns null if nothing to do.
DequeueResult? dequeue(ProgressState progress, QueueState queue) {
  // Only act when the task bar is finished.
  if (progress.task.position < progress.task.max) return null;
  if (queue.entries.isEmpty) return null;

  final entries = Queue<QueueEntry>.from(queue.entries);
  if (entries.isEmpty) return null;

  final next = entries.removeFirst();
  final taskResult = startTask(progress, next.caption, next.durationMillis);
  return DequeueResult(
    progress: taskResult.progress,
    queue: QueueState(entries: entries.toList()),
    caption: taskResult.caption,
    taskType: next.taskType,
    kind: next.kind,
  );
}

int _monsterLevel(String entry) {
  final parts = entry.split('|');
  if (parts.length < 2) return 0;
  return int.tryParse(parts[1]) ?? 0;
}

String _sick(int m, String s) {
  switch (m) {
    case -5:
    case 5:
      return l10n.modifierDead(s);
    case -4:
    case 4:
      return l10n.modifierComatose(s);
    case -3:
    case 3:
      return l10n.modifierCrippled(s);
    case -2:
    case 2:
      return l10n.modifierSick(s);
    case -1:
    case 1:
      return l10n.modifierUndernourished(s);
    default:
      return '$m$s';
  }
}

String _young(int m, String s) {
  switch (-m) {
    case -5:
    case 5:
      return l10n.modifierFoetal(s);
    case -4:
    case 4:
      return l10n.modifierBaby(s);
    case -3:
    case 3:
      return l10n.modifierPreadolescent(s);
    case -2:
    case 2:
      return l10n.modifierTeenage(s);
    case -1:
    case 1:
      return l10n.modifierUnderage(s);
    default:
      return '$m$s';
  }
}

String _big(int m, String s) {
  switch (m) {
    case 1:
    case -1:
      return l10n.modifierGreater(s);
    case 2:
    case -2:
      return l10n.modifierMassive(s);
    case 3:
    case -3:
      return l10n.modifierEnormous(s);
    case 4:
    case -4:
      return l10n.modifierGiant(s);
    case 5:
    case -5:
      return l10n.modifierTitanic(s);
    default:
      return s;
  }
}

String _special(int m, String s) {
  switch (-m) {
    case 1:
    case -1:
      return s.contains(' ') ? l10n.modifierVeteran(s) : l10n.modifierBattle(s);
    case 2:
    case -2:
      return l10n.modifierCursed(s);
    case 3:
    case -3:
      return s.contains(' ') ? l10n.modifierWarrior(s) : l10n.modifierWere(s);
    case 4:
    case -4:
      return l10n.modifierUndead(s);
    case 5:
    case -5:
      return l10n.modifierDemon(s);
    default:
      return s;
  }
}

bool _ends(String s, String suffix) {
  return s.length >= suffix.length &&
      s.substring(s.length - suffix.length) == suffix;
}

String _pick(String pipeSeparated, DeterministicRandom rng) {
  final parts = pipeSeparated.split('|');
  if (parts.isEmpty) return '';
  final idx = rng.nextInt(parts.length);
  return parts[idx];
}

// =============================================================================
// InterplotCinematic 관련 함수들 (Main.pas:456-521)
// =============================================================================

/// NPC 이름 생성 (ImpressiveGuy, Main.pas:514-521)
/// 인상적인 타이틀 + 종족 또는 이름 조합 (l10n 지원)
String impressiveGuy(PqConfig config, DeterministicRandom rng) {
  final titleEn = pick(config.impressiveTitles, rng);
  final title = l10n.translateImpressiveTitle(titleEn);
  switch (rng.nextInt(2)) {
    case 0:
      // "the King of the Elves" / "엘프들의 왕" 형태
      final raceEn = pick(config.races, rng).split('|').first;
      final race = l10n.translateRace(raceEn);
      return l10n.impressiveGuyPattern1(title, race);
    case 1:
      // "King Vrognak of Zoxzik" / "Zoxzik의 왕 Vrognak" 형태
      final name1 = generateName(rng);
      final name2 = generateName(rng);
      return l10n.impressiveGuyPattern2(title, name1, name2);
    default:
      return title;
  }
}

/// 이름 있는 몬스터 생성 (NamedMonster, Main.pas:498-512)
/// 레벨에 맞는 몬스터 선택 후 GenerateName으로 이름 붙이기 (l10n 지원)
String namedMonster(PqConfig config, DeterministicRandom rng, int level) {
  String best = '';
  int bestLevel = 0;

  // 5번 시도해서 레벨에 가장 가까운 몬스터 선택
  for (var i = 0; i < 5; i++) {
    final m = pick(config.monsters, rng);
    final parts = m.split('|');
    final name = parts.first;
    final lev = parts.length > 1 ? (int.tryParse(parts[1]) ?? 0) : 0;

    if (best.isEmpty || (level - lev).abs() < (level - bestLevel).abs()) {
      best = name;
      bestLevel = lev;
    }
  }

  // 몬스터 이름 번역
  final translatedMonster = l10n.translateMonster(best);
  // "GeneratedName the MonsterType" / "몬스터타입 GeneratedName" 형태
  return l10n.namedMonsterFormat(generateName(rng), translatedMonster);
}

/// 플롯 간 시네마틱 이벤트 생성 (InterplotCinematic, Main.pas:456-495)
/// 3가지 시나리오 중 하나를 랜덤 선택 (l10n 지원)
List<QueueEntry> interplotCinematic(
  PqConfig config,
  DeterministicRandom rng,
  int level,
  int plotCount,
) {
  final entries = <QueueEntry>[];

  // 헬퍼: 큐 엔트리 추가 (원본의 Q 함수 역할)
  void q(QueueKind kind, int seconds, String caption) {
    entries.add(
      QueueEntry(kind: kind, durationMillis: seconds * 1000, caption: caption),
    );
  }

  switch (rng.nextInt(3)) {
    case 0:
      // 시나리오 1: 안전한 캐시 영역 도착
      q(QueueKind.task, 1, l10n.cinematicCacheZone1());
      q(QueueKind.task, 2, l10n.cinematicCacheZone2());
      q(QueueKind.task, 2, l10n.cinematicCacheZone3());
      q(QueueKind.task, 1, l10n.cinematicCacheZone4());
      break;

    case 1:
      // 시나리오 2: 강력한 버그와의 전투
      q(QueueKind.task, 1, l10n.cinematicCombat1());
      final nemesis = namedMonster(config, rng, level + 3);
      q(QueueKind.task, 4, l10n.cinematicCombat2(nemesis));

      var s = rng.nextInt(3);
      final combatRounds = rng.nextInt(1 + plotCount);
      for (var i = 0; i < combatRounds; i++) {
        s += 1 + rng.nextInt(2);
        switch (s % 3) {
          case 0:
            q(QueueKind.task, 2, l10n.cinematicCombatLocked(nemesis));
            break;
          case 1:
            q(QueueKind.task, 2, l10n.cinematicCombatCorrupts(nemesis));
            break;
          case 2:
            q(QueueKind.task, 2, l10n.cinematicCombatWorking(nemesis));
            break;
        }
      }

      q(QueueKind.task, 3, l10n.cinematicCombatVictory(nemesis));
      q(QueueKind.task, 2, l10n.cinematicCombatWakeUp());
      break;

    case 2:
      // 시나리오 3: 내부자 위협 발견
      final guy = impressiveGuy(config, rng);
      final itemEn = boringItem(config, rng);
      final item = l10n.translateBoringItem(itemEn);
      q(QueueKind.task, 2, l10n.cinematicBetrayal1(guy));
      q(QueueKind.task, 3, l10n.cinematicBetrayal2(guy));
      q(QueueKind.task, 2, l10n.cinematicBetrayal3(item));
      q(QueueKind.task, 2, l10n.cinematicBetrayal4());
      q(QueueKind.task, 2, l10n.cinematicBetrayal5(guy));
      q(QueueKind.task, 3, l10n.cinematicBetrayal6());
      break;
  }

  // 마지막에 plot 추가
  q(QueueKind.plot, 2, l10n.taskCompiling);

  return entries;
}
