<?php
declare(strict_types=1);

function start_waiting_match(array &$match, string $away, array $awayUser): void {
  $match['status'] = 'ready';
  ensure_user_squad($awayUser);
  $match['players']['away'] = $away;
  $match['teams']['away'] = user_team($awayUser);
  $awayBase = team_base_rating((string)$awayUser['teamId']);
  $match['units']['away'] = user_roster($awayUser, 'away');
  $match['bench']['away'] = user_bench($awayUser, 'away');
  ensure_match_engine($match);
  $match['engine']['baseRatings']['away'] = $awayBase;
  reset_units($match);
  $match['lastTick'] = microtime(true);
  $match['updated'] = time();
}

function clamp_float(float $value, float $min, float $max): float {
  return max($min, min($max, $value));
}

function clamp_int(int $value, int $min, int $max): int {
  return max($min, min($max, $value));
}

function goal_min_x(): float {
  return 0.5 - (GOAL_WIDTH_M / PITCH_WIDTH_M) / 2;
}

function goal_max_x(): float {
  return 0.5 + (GOAL_WIDTH_M / PITCH_WIDTH_M) / 2;
}

function base_formation(): array {
  return [
    ['GK', 0.50, 0.94],
    ['LB', 0.18, 0.80],
    ['CB', 0.38, 0.82],
    ['CB', 0.62, 0.82],
    ['RB', 0.82, 0.80],
    ['LM', 0.16, 0.64],
    ['CM', 0.38, 0.67],
    ['CM', 0.62, 0.67],
    ['RM', 0.84, 0.64],
    ['ST', 0.38, 0.55],
    ['ST', 0.62, 0.55],
  ];
}

function manager_formations(): array {
  return [
    '4-4-2' => [
      ['GK', 0.50, 0.94],
      ['LB', 0.18, 0.80], ['CB', 0.38, 0.82], ['CB', 0.62, 0.82], ['RB', 0.82, 0.80],
      ['LM', 0.16, 0.64], ['CM', 0.38, 0.67], ['CM', 0.62, 0.67], ['RM', 0.84, 0.64],
      ['ST', 0.38, 0.55], ['ST', 0.62, 0.55],
    ],
    '5-3-2' => [
      ['GK', 0.50, 0.94],
      ['LB', 0.12, 0.79], ['CB', 0.30, 0.82], ['CB', 0.50, 0.84], ['CB', 0.70, 0.82], ['RB', 0.88, 0.79],
      ['CM', 0.30, 0.64], ['CM', 0.50, 0.66], ['CM', 0.70, 0.64],
      ['ST', 0.40, 0.52], ['ST', 0.60, 0.52],
    ],
    '4-3-3' => [
      ['GK', 0.50, 0.94],
      ['LB', 0.18, 0.80], ['CB', 0.38, 0.82], ['CB', 0.62, 0.82], ['RB', 0.82, 0.80],
      ['CM', 0.30, 0.66], ['CM', 0.50, 0.68], ['CM', 0.70, 0.66],
      ['LM', 0.25, 0.52], ['ST', 0.50, 0.48], ['RM', 0.75, 0.52],
    ],
    '4-5-1' => [
      ['GK', 0.50, 0.94],
      ['LB', 0.18, 0.80], ['CB', 0.38, 0.82], ['CB', 0.62, 0.82], ['RB', 0.82, 0.80],
      ['LM', 0.14, 0.62], ['CM', 0.32, 0.66], ['CM', 0.50, 0.68], ['CM', 0.68, 0.66], ['RM', 0.86, 0.62],
      ['ST', 0.50, 0.50],
    ],
    '3-5-2' => [
      ['GK', 0.50, 0.94],
      ['CB', 0.30, 0.82], ['CB', 0.50, 0.84], ['CB', 0.70, 0.82],
      ['LM', 0.12, 0.63], ['CM', 0.32, 0.66], ['CM', 0.50, 0.68], ['CM', 0.68, 0.66], ['RM', 0.88, 0.63],
      ['ST', 0.40, 0.52], ['ST', 0.60, 0.52],
    ],
    '4-2-3-1' => [
      ['GK', 0.50, 0.94],
      ['LB', 0.18, 0.80], ['CB', 0.38, 0.82], ['CB', 0.62, 0.82], ['RB', 0.82, 0.80],
      ['CM', 0.40, 0.69], ['CM', 0.60, 0.69],
      ['LM', 0.22, 0.56], ['CM', 0.50, 0.57], ['RM', 0.78, 0.56],
      ['ST', 0.50, 0.47],
    ],
  ];
}

function valid_manager_formation(string $formation): string {
  $formation = trim($formation);
  return isset(manager_formations()[$formation]) ? $formation : '4-4-2';
}

function manager_user_formation(array $user): string {
  return valid_manager_formation((string)($user['manager']['formation'] ?? '4-4-2'));
}

function manager_formation_options(): array {
  return array_keys(manager_formations());
}

function role_family(string $role): string {
  $role = strtoupper($role);
  if ($role === 'GK') return 'keeper';
  if (in_array($role, ['CB', 'LB', 'RB'], true)) return 'defender';
  if (in_array($role, ['CM', 'LM', 'RM'], true)) return 'midfielder';
  return 'attacker';
}

function canonical_player_role(string $role, string $fallback = 'CM'): string {
  $role = str_replace(['-', '_'], ' ', strtoupper(trim($role)));
  $map = [
    'GOALKEEPER' => 'GK', 'KEEPER' => 'GK',
    'LEFT BACK' => 'LB', 'LEFT WING BACK' => 'LB', 'LWB' => 'LB',
    'RIGHT BACK' => 'RB', 'RIGHT WING BACK' => 'RB', 'RWB' => 'RB',
    'CENTRE BACK' => 'CB', 'CENTER BACK' => 'CB', 'DEFENDER' => 'CB', 'DEFENCE' => 'CB', 'DF' => 'CB',
    'LEFT MIDFIELD' => 'LM', 'LEFT MIDFIELDER' => 'LM', 'LEFT WING' => 'LM', 'LEFT WINGER' => 'LM', 'LW' => 'LM',
    'RIGHT MIDFIELD' => 'RM', 'RIGHT MIDFIELDER' => 'RM', 'RIGHT WING' => 'RM', 'RIGHT WINGER' => 'RM', 'RW' => 'RM',
    'DEFENSIVE MIDFIELD' => 'CM', 'DEFENSIVE MIDFIELDER' => 'CM', 'CENTRAL MIDFIELD' => 'CM', 'CENTRAL MIDFIELDER' => 'CM',
    'ATTACKING MIDFIELD' => 'CM', 'ATTACKING MIDFIELDER' => 'CM', 'MIDFIELD' => 'CM', 'MIDFIELDER' => 'CM', 'MF' => 'CM',
    'CENTRE FORWARD' => 'ST', 'CENTER FORWARD' => 'ST', 'FORWARD' => 'ST', 'ATTACKER' => 'ST', 'STRIKER' => 'ST', 'CF' => 'ST', 'FW' => 'ST',
  ];
  if (isset($map[$role])) return $map[$role];
  if (in_array($role, ['GK', 'LB', 'RB', 'CB', 'LM', 'RM', 'CM', 'ST'], true)) return $role;
  return $fallback;
}

function player_seed(array $player): int {
  return (int)crc32((string)($player['id'] ?? '') . '|' . (string)($player['name'] ?? 'player') . '|' . (string)($player['shirtName'] ?? ''));
}

function canonical_player_foot(string $foot): string {
  $foot = strtoupper(trim($foot));
  if (in_array($foot, ['L', 'LEFT'], true)) return 'L';
  if (in_array($foot, ['R', 'RIGHT'], true)) return 'R';
  if (in_array($foot, ['B', 'BOTH', 'TWO', 'TWOFOOTED', 'TWO-FOOTED'], true)) return 'B';
  return '';
}

function player_preferred_foot(array $player): string {
  $foot = canonical_player_foot((string)($player['foot'] ?? $player['preferredFoot'] ?? ''));
  if ($foot !== '') return $foot;
  $roll = abs(player_seed($player)) % 100;
  foreach (['naturalRole', 'role', 'slotRole'] as $key) {
    $role = canonical_player_role((string)($player[$key] ?? ''), '');
    if (in_array($role, ['LB', 'LM'], true)) return $roll < 12 ? 'B' : 'L';
    if (in_array($role, ['RB', 'RM'], true)) return $roll < 12 ? 'B' : 'R';
  }
  if ($roll < 18) return 'B';
  if ($roll < 42) return 'L';
  return 'R';
}

function player_side_role(array $player, string $leftRole, string $rightRole): string {
  $foot = player_preferred_foot($player);
  if ($foot === 'L') return $leftRole;
  if ($foot === 'R') return $rightRole;
  $x = $player['homeX'] ?? $player['x'] ?? null;
  if (is_numeric($x)) {
    $x = (float)$x;
    if ($x < 0.45) return $leftRole;
    if ($x > 0.55) return $rightRole;
  }
  foreach (['slotRole', 'role', 'naturalRole'] as $key) {
    $role = canonical_player_role((string)($player[$key] ?? ''), '');
    if ($role === $leftRole || $role === $rightRole) return $role;
  }
  $seed = player_seed($player);
  return ($seed % 2 === 0) ? $leftRole : $rightRole;
}

function infer_role_bucket_from_stats(array $player): string {
  $s = is_array($player['stats'] ?? null) ? $player['stats'] : [];
  if (!$s) return canonical_player_role((string)($player['role'] ?? 'CM'));
  $v = fn(string $key): int => clamp_int((int)($s[$key] ?? 0), 0, 100);
  $scores = [
    'GK' => ($v('keeping') * 2.6 + $v('positioning') * .7 + $v('composure') * .45) / 3.75 - $v('shooting') * .12 - $v('crossing') * .08,
    'CB' => ($v('tackling') * 1.6 + $v('heading') * 1.25 + $v('strength') * 1.0 + $v('positioning') * 1.0 + $v('discipline') * .5) / 5.35 - $v('shooting') * .10 - $v('crossing') * .05,
    'FB' => ($v('tackling') * .9 + $v('speed') * 1.1 + $v('crossing') * .9 + $v('stamina') * .65 + $v('workrate') * .6 + $v('passing') * .25) / 4.4 - $v('shooting') * .06,
    'CM' => ($v('passing') * 1.35 + $v('vision') * 1.1 + $v('passcontrol') * .9 + $v('workrate') * .7 + $v('tackling') * .5 + $v('positioning') * .8) / 5.35,
    'WM' => ($v('speed') * 1.0 + $v('crossing') * 1.15 + $v('dribbling') * .95 + $v('passing') * .65 + $v('stamina') * .45) / 4.2 - $v('tackling') * .05,
    'ST' => ($v('shooting') * 1.4 + $v('shotcontrol') * 1.2 + $v('positioning') * .8 + $v('composure') * .65 + $v('heading') * .45 + $v('speed') * .45 + $v('crit') * .25) / 5.2 - $v('tackling') * .22 - $v('crossing') * .08,
  ];
  arsort($scores);
  return (string)array_key_first($scores);
}

function infer_role_from_stats(array $player): string {
  $role = infer_role_bucket_from_stats($player);
  if ($role === 'FB') return player_side_role($player, 'LB', 'RB');
  if ($role === 'WM') return player_side_role($player, 'LM', 'RM');
  return $role;
}

function player_natural_role(array $player): string {
  $natural = canonical_player_role((string)($player['naturalRole'] ?? ''), '');
  if ($natural !== '') return $natural;
  $legacy = trim((string)($player['legacyRole'] ?? ''));
  if ($legacy !== '') return canonical_player_role(market_role_from_legacy($legacy, (int)crc32((string)($player['name'] ?? $legacy))), 'CM');
  if (!empty($player['stats']) && is_array($player['stats'])) return infer_role_from_stats($player);
  $role = canonical_player_role((string)($player['role'] ?? ''), '');
  if ($role !== '') return $role;
  $slot = canonical_player_role((string)($player['slotRole'] ?? ''), '');
  if ($slot !== '') return $slot;
  return 'CM';
}

function player_slot_role(array $player): string {
  return canonical_player_role((string)($player['slotRole'] ?? $player['role'] ?? player_natural_role($player)), player_natural_role($player));
}

function player_display_role(array $player): string {
  $role = player_natural_role($player);
  if (player_preferred_foot($player) === 'B') {
    $bucket = !empty($player['stats']) && is_array($player['stats']) ? infer_role_bucket_from_stats($player) : '';
    if ($bucket === 'FB' || in_array($role, ['LB', 'RB'], true)) return 'LB/RB';
    if ($bucket === 'WM' || in_array($role, ['LM', 'RM'], true)) return 'LM/RM';
  }
  return $role;
}

function player_foot_slot_bonus(array $player, ?string $slotRole = null): int {
  $slotRole = canonical_player_role((string)($slotRole ?? $player['slotRole'] ?? $player['role'] ?? ''), '');
  if (!in_array($slotRole, ['LB', 'LM', 'RB', 'RM'], true)) return 0;
  $foot = player_preferred_foot($player);
  if ($foot === 'B') return 1;
  if (in_array($slotRole, ['LB', 'LM'], true)) return $foot === 'L' ? 1 : -1;
  return $foot === 'R' ? 1 : -1;
}

function player_side_pair(string $role): string {
  $role = canonical_player_role($role, '');
  if (in_array($role, ['LB', 'RB'], true)) return 'fullback';
  if (in_array($role, ['LM', 'RM'], true)) return 'wide-mid';
  return $role;
}

function player_position_penalty_percent(array $player, ?string $slotRole = null): int {
  return 0;
}

function player_condition_multiplier(array $player): float {
  $multiplier = 1.0;
  foreach (['matchStatMultiplier', 'dailyFormMultiplier'] as $key) {
    $value = (float)($player[$key] ?? 1.0);
    if ($value > 0 && abs($value - 1.0) > 0.0001) $multiplier *= $value;
  }
  return clamp_float($multiplier, 0.1, 2.0);
}

function player_effective_rating(array $player): int {
  $rating = (int)($player['rating'] ?? 50) + player_foot_slot_bonus($player);
  $conditionMultiplier = player_condition_multiplier($player);
  if (abs($conditionMultiplier - 1.0) > 0.0001) $rating = (int)round($rating * $conditionMultiplier);
  return clamp_int($rating, 0, 100);
}

function ensure_player_roles(array &$player): void {
  $player['naturalRole'] = player_natural_role($player);
  $player['role'] = $player['naturalRole'];
  $player['foot'] = player_preferred_foot($player);
  if (empty($player['slotRole'])) $player['slotRole'] = canonical_player_role((string)($player['role'] ?? $player['naturalRole']), $player['naturalRole']);
}

function role_fit_score(array $player, string $slotRole, array $dailyForm = []): int {
  if ($dailyForm) $player = apply_daily_form_to_player($player, $dailyForm);
  ensure_player_roles($player);
  $role = player_natural_role($player);
  $slotRole = canonical_player_role($slotRole, $role);
  $base = player_skill_points($player);
  if ($role === $slotRole) $base += 5000;
  elseif (role_family($role) === role_family($slotRole)) $base += 2500;
  elseif ($slotRole !== 'GK' && $role !== 'GK') $base += 700;
  if ($slotRole === 'GK' && $role !== 'GK') $base -= 6000;
  if ($slotRole !== 'GK' && $role === 'GK') $base -= 5000;
  $base += player_foot_slot_bonus($player, $slotRole) * 120;
  return $base;
}

function hungarian_min_assignment(array $costs): ?array {
  $rowCount = count($costs);
  if ($rowCount === 0) return [];
  $colCount = count($costs[0] ?? []);
  if ($colCount === 0 || $rowCount > $colCount) return null;

  $u = array_fill(0, $rowCount + 1, 0);
  $v = array_fill(0, $colCount + 1, 0);
  $p = array_fill(0, $colCount + 1, 0);
  $way = array_fill(0, $colCount + 1, 0);

  for ($i = 1; $i <= $rowCount; $i++) {
    $p[0] = $i;
    $j0 = 0;
    $minv = array_fill(0, $colCount + 1, PHP_INT_MAX);
    $used = array_fill(0, $colCount + 1, false);
    do {
      $used[$j0] = true;
      $i0 = (int)$p[$j0];
      $delta = PHP_INT_MAX;
      $j1 = 0;
      for ($j = 1; $j <= $colCount; $j++) {
        if ($used[$j]) continue;
        $cur = (int)$costs[$i0 - 1][$j - 1] - (int)$u[$i0] - (int)$v[$j];
        if ($cur < $minv[$j]) {
          $minv[$j] = $cur;
          $way[$j] = $j0;
        }
        if ($minv[$j] < $delta) {
          $delta = (int)$minv[$j];
          $j1 = $j;
        }
      }
      if ($delta === PHP_INT_MAX || $j1 === 0) return null;
      for ($j = 0; $j <= $colCount; $j++) {
        if ($used[$j]) {
          $u[(int)$p[$j]] += $delta;
          $v[$j] -= $delta;
        } else {
          $minv[$j] -= $delta;
        }
      }
      $j0 = $j1;
    } while ((int)$p[$j0] !== 0);

    do {
      $j1 = (int)$way[$j0];
      $p[$j0] = $p[$j1];
      $j0 = $j1;
    } while ($j0 !== 0);
  }

  $assignment = array_fill(0, $rowCount, -1);
  for ($j = 1; $j <= $colCount; $j++) {
    $row = (int)$p[$j];
    if ($row > 0) $assignment[$row - 1] = $j - 1;
  }
  foreach ($assignment as $playerIndex) {
    if ((int)$playerIndex < 0) return null;
  }
  return $assignment;
}

function best_formation_assignment(array $pool, array $slots, array $dailyForm = []): ?array {
  $playerCount = count($pool);
  $slotCount = count($slots);
  if ($playerCount < $slotCount || $playerCount > 60) return null;

  $costs = [];
  foreach ($slots as $slotIndex => $slot) {
    $role = (string)($slot[0] ?? 'CM');
    $costs[$slotIndex] = [];
    foreach ($pool as $playerIndex => $player) {
      $score = role_fit_score($player, $role, $dailyForm);
      $costs[$slotIndex][$playerIndex] = -$score;
    }
  }

  return hungarian_min_assignment($costs);
}

function fast_formation_assignment(array $pool, array $slots, array $dailyForm = []): ?array {
  $playerCount = count($pool);
  $slotCount = count($slots);
  if ($playerCount < $slotCount) return null;
  $assignment = [];
  $used = [];
  foreach ($slots as $slotIndex => $slot) {
    $role = (string)($slot[0] ?? 'CM');
    $bestIndex = -1;
    $bestScore = PHP_INT_MIN;
    foreach ($pool as $playerIndex => $player) {
      if (isset($used[$playerIndex])) continue;
      $score = role_fit_score($player, $role, $dailyForm);
      if ($score > $bestScore) {
        $bestScore = $score;
        $bestIndex = (int)$playerIndex;
      }
    }
    if ($bestIndex < 0) return null;
    $assignment[$slotIndex] = $bestIndex;
    $used[$bestIndex] = true;
  }
  return $assignment;
}

function apply_formation_to_squad(array &$squad, string $formation, array $dailyForm = [], bool $fast = false, array $excludedPlayerIds = []): bool {
  $formation = valid_manager_formation($formation);
  $slots = manager_formations()[$formation];
  $allPool = array_values(array_merge($squad['starters'] ?? [], $squad['bench'] ?? []));
  $excluded = [];
  foreach ($excludedPlayerIds as $id) {
    $id = trim((string)$id);
    if ($id !== '') $excluded[$id] = true;
  }
  $pool = [];
  $poolOriginalIndexes = [];
  foreach ($allPool as $originalIndex => $player) {
    $id = (string)($player['id'] ?? '');
    if ($id !== '' && isset($excluded[$id])) continue;
    $poolOriginalIndexes[] = (int)$originalIndex;
    $pool[] = $player;
  }
  if (count($pool) < 11) return false;
  $assignment = $fast ? fast_formation_assignment($pool, $slots, $dailyForm) : best_formation_assignment($pool, $slots, $dailyForm);
  if ($assignment === null) return false;
  $starters = [];
  $used = [];
  $usedOriginal = [];
  foreach ($slots as $slotIndex => $slot) {
    [$role, $x, $homeY] = $slot;
    $bestIndex = (int)($assignment[$slotIndex] ?? -1);
    if ($bestIndex < 0 || !isset($pool[$bestIndex]) || isset($used[$bestIndex])) return false;
    $player = $pool[$bestIndex];
    ensure_player_roles($player);
    $used[$bestIndex] = true;
    $originalIndex = (int)($poolOriginalIndexes[$bestIndex] ?? -1);
    if ($originalIndex >= 0) $usedOriginal[$originalIndex] = true;
    $player['slotRole'] = $role;
    $player['role'] = player_natural_role($player);
    $player['homeX'] = (float)$x;
    $player['homeY'] = (float)$homeY;
    $player['x'] = (float)$x;
    $player['y'] = (float)$homeY;
    $starters[] = $player;
  }
  $bench = [];
  foreach ($allPool as $index => $player) {
    if (isset($usedOriginal[$index])) continue;
    ensure_player_roles($player);
    $natural = player_natural_role($player);
    $player['role'] = $natural;
    $player['slotRole'] = $natural;
    $bench[] = $player;
  }
  $squad['starters'] = $starters;
  $squad['bench'] = $bench;
  ensure_squad_stats($squad);
  return true;
}

function apply_squad_formation(array &$user, string $formation, array $dailyForm = [], array $excludedPlayerIds = []): bool {
  ensure_user_squad($user);
  $formation = valid_manager_formation($formation);
  if (!apply_formation_to_squad($user['squad'], $formation, $dailyForm, false, $excludedPlayerIds)) return false;
  $user['manager']['formation'] = $formation;
  unset($user['manager']['manualLineupOverrideAt']);
  return true;
}

function player_stat_keys(bool $keeper = false): array {
  $keys = [
    'shooting', 'passing', 'tackling', 'heading', 'passcontrol', 'shotcontrol', 'speed', 'crit',
    'stamina', 'strength', 'positioning', 'vision', 'composure', 'aggression', 'discipline',
    'crossing', 'dribbling', 'workrate',
  ];
  if ($keeper) $keys[] = 'keeping';
  return $keys;
}

function role_stat_bonus(string $role, string $stat): int {
  $bonus = [
    'GK' => ['shooting' => -26, 'passing' => -4, 'tackling' => -18, 'heading' => -12, 'passcontrol' => 4, 'shotcontrol' => -10, 'speed' => -4, 'crit' => -8, 'stamina' => 0, 'strength' => 3, 'positioning' => 14, 'vision' => 3, 'composure' => 9, 'aggression' => -8, 'discipline' => 8, 'crossing' => -20, 'dribbling' => -12, 'workrate' => 1, 'keeping' => 18],
    'RB' => ['shooting' => -10, 'passing' => 3, 'tackling' => 9, 'heading' => 2, 'passcontrol' => 2, 'shotcontrol' => -5, 'speed' => 8, 'crit' => -2, 'stamina' => 9, 'strength' => 3, 'positioning' => 5, 'vision' => 2, 'composure' => 1, 'aggression' => 4, 'discipline' => 2, 'crossing' => 10, 'dribbling' => 4, 'workrate' => 10],
    'LB' => ['shooting' => -10, 'passing' => 3, 'tackling' => 9, 'heading' => 2, 'passcontrol' => 2, 'shotcontrol' => -5, 'speed' => 8, 'crit' => -2, 'stamina' => 9, 'strength' => 3, 'positioning' => 5, 'vision' => 2, 'composure' => 1, 'aggression' => 4, 'discipline' => 2, 'crossing' => 10, 'dribbling' => 4, 'workrate' => 10],
    'CB' => ['shooting' => -15, 'passing' => -2, 'tackling' => 14, 'heading' => 12, 'passcontrol' => -4, 'shotcontrol' => -8, 'speed' => -2, 'crit' => -3, 'stamina' => 4, 'strength' => 12, 'positioning' => 12, 'vision' => -1, 'composure' => 5, 'aggression' => 6, 'discipline' => 1, 'crossing' => -9, 'dribbling' => -8, 'workrate' => 6],
    'RM' => ['shooting' => 2, 'passing' => 8, 'tackling' => -2, 'heading' => -3, 'passcontrol' => 10, 'shotcontrol' => 3, 'speed' => 12, 'crit' => 2, 'stamina' => 10, 'strength' => -2, 'positioning' => 4, 'vision' => 7, 'composure' => 3, 'aggression' => 1, 'discipline' => 2, 'crossing' => 13, 'dribbling' => 12, 'workrate' => 9],
    'LM' => ['shooting' => 2, 'passing' => 8, 'tackling' => -2, 'heading' => -3, 'passcontrol' => 10, 'shotcontrol' => 3, 'speed' => 12, 'crit' => 2, 'stamina' => 10, 'strength' => -2, 'positioning' => 4, 'vision' => 7, 'composure' => 3, 'aggression' => 1, 'discipline' => 2, 'crossing' => 13, 'dribbling' => 12, 'workrate' => 9],
    'CM' => ['shooting' => 1, 'passing' => 12, 'tackling' => 4, 'heading' => 0, 'passcontrol' => 10, 'shotcontrol' => 4, 'speed' => 3, 'crit' => 3, 'stamina' => 7, 'strength' => 2, 'positioning' => 8, 'vision' => 12, 'composure' => 8, 'aggression' => 3, 'discipline' => 5, 'crossing' => 3, 'dribbling' => 5, 'workrate' => 10],
    'ST' => ['shooting' => 15, 'passing' => 0, 'tackling' => -12, 'heading' => 8, 'passcontrol' => 4, 'shotcontrol' => 14, 'speed' => 5, 'crit' => 8, 'stamina' => 4, 'strength' => 7, 'positioning' => 12, 'vision' => 2, 'composure' => 12, 'aggression' => 4, 'discipline' => -1, 'crossing' => -4, 'dribbling' => 8, 'workrate' => 5],
  ];
  return $bonus[$role][$stat] ?? 0;
}

function stat_value(int $base, string $role, string $stat): int {
  return clamp_int($base + role_stat_bonus($role, $stat) + mt_rand(-9, 9), 0, 100);
}

function random_weight_class(): string {
  $roll = mt_rand(1, 100);
  if ($roll <= 28) return 'light';
  if ($roll >= 73) return 'heavy';
  return 'medium';
}

function ensure_player_stats(array &$player): void {
  ensure_player_roles($player);
  $role = player_natural_role($player);
  if (!isset($player['stats']) || !is_array($player['stats'])) $player['stats'] = [];
  $base = (int)($player['rating'] ?? 50);
  foreach (player_stat_keys($role === 'GK') as $stat) {
    if (!array_key_exists($stat, $player['stats'])) $player['stats'][$stat] = stat_value($base, $role, $stat);
  }
  if (!in_array(($player['weightClass'] ?? ''), ['light', 'medium', 'heavy'], true)) $player['weightClass'] = random_weight_class();
  $player['rating'] = average_stats($player['stats']);
}

function manager_initial_age_max_for_owner(string $ownerId): int {
  $ownerId = clean_name($ownerId);
  return in_array($ownerId, ['jdc', 'bodin'], true) ? 27 : 44;
}

function manager_seeded_player_age(array $player, string $ownerId = '', int $maxAge = 44): int {
  $maxAge = clamp_int($maxAge, 17, 44);
  $span = max(1, $maxAge - 16);
  $seed = implode('|', [
    (string)($player['id'] ?? ''),
    (string)($player['name'] ?? ''),
    (string)($player['shirtNumber'] ?? $player['legacyNumber'] ?? ''),
    $ownerId,
  ]);
  return 17 + ((int)sprintf('%u', crc32($seed)) % $span);
}

function manager_age_origin_owner(string $ownerId, string $teamId = '', array $player = []): string {
  $ownerId = clean_name($ownerId);
  if ($ownerId !== '') return $ownerId;
  $origin = (string)($player['purchasedFromId'] ?? $player['sourceTeamId'] ?? '');
  if ($origin !== '' && !in_array($origin, ['market', 'loan-pool'], true)) return $origin;
  $source = source_team_id($teamId);
  if ($source !== '') return manager_cpu_id($source);
  return $origin !== '' ? $origin : 'market';
}

function ensure_player_age_profile(array &$player, string $ownerId = '', string $teamId = '', string $teamName = '', string $divisionName = '', ?int $initialMaxAge = null): void {
  if (manager_is_emergency_loan_player($player)) return;
  ensure_player_stats($player);
  $ownerId = clean_name($ownerId);
  $maxAge = $initialMaxAge ?? manager_initial_age_max_for_owner($ownerId);
  if (!isset($player['age']) || !is_numeric($player['age'])) {
    $player['age'] = manager_seeded_player_age($player, $ownerId, $maxAge);
  } else {
    $player['age'] = clamp_int((int)$player['age'], 17, 45);
  }
  if (empty($player['ageProfileVersion'])) {
    if ($ownerId !== '' && in_array($ownerId, ['jdc', 'bodin'], true) && (int)$player['age'] > 27) {
      $player['age'] = manager_seeded_player_age($player, $ownerId, 27);
    }
    $player['ageProfileVersion'] = 1;
  }
  if (empty($player['originOwnerId'])) $player['originOwnerId'] = manager_age_origin_owner($ownerId, $teamId, $player);
  if (empty($player['originTeamId'])) {
    $originTeamId = source_team_id($teamId);
    if ($originTeamId === '') $originTeamId = (string)($player['sourceTeamId'] ?? $player['purchasedFromId'] ?? '');
    $player['originTeamId'] = $originTeamId;
  }
  if (empty($player['originTeamName'])) $player['originTeamName'] = $teamName !== '' ? $teamName : (string)($player['sourceTeam'] ?? $player['purchasedFrom'] ?? '');
  if (empty($player['originDivisionName'])) $player['originDivisionName'] = $divisionName !== '' ? $divisionName : (string)($player['sourceDivision'] ?? $player['purchasedFromDivision'] ?? '');
  if (empty($player['originPlayerId'])) $player['originPlayerId'] = (string)($player['id'] ?? '');
  if (empty($player['originName'])) $player['originName'] = (string)($player['name'] ?? 'Player');
  if (empty($player['originRole'])) $player['originRole'] = player_natural_role($player);
  if (!isset($player['ageBaseStats']) || !is_array($player['ageBaseStats'])) $player['ageBaseStats'] = $player['stats'];
  if (!isset($player['ageBaseRating']) || !is_numeric($player['ageBaseRating'])) $player['ageBaseRating'] = (int)($player['rating'] ?? average_stats($player['stats']));
  if (empty($player['ageBaseFoot'])) $player['ageBaseFoot'] = player_preferred_foot($player);
  if (empty($player['ageBaseWeightClass'])) $player['ageBaseWeightClass'] = (string)($player['weightClass'] ?? random_weight_class());
}

function ensure_squad_age_profiles(array &$squad, string $ownerId = '', string $teamId = '', string $teamName = '', string $divisionName = '', ?int $initialMaxAge = null): void {
  foreach (['starters', 'bench'] as $group) {
    if (empty($squad[$group]) || !is_array($squad[$group])) continue;
    foreach ($squad[$group] as &$player) ensure_player_age_profile($player, $ownerId, $teamId, $teamName, $divisionName, $initialMaxAge);
    unset($player);
  }
}

function ensure_squad_stats(array &$squad): void {
  foreach (['starters', 'bench'] as $group) {
    if (empty($squad[$group]) || !is_array($squad[$group])) continue;
    foreach ($squad[$group] as &$player) ensure_player_stats($player);
    unset($player);
  }
}

function player_name(string $side, int $index, string $role): string {
  $first = ['Tom', 'Jack', 'Sam', 'Joe', 'Ryan', 'Ben', 'Leo', 'Max', 'Callum', 'Harry', 'Owen', 'Evan', 'Luke', 'Noah', 'Alex', 'Finn', 'Liam', 'Jake'];
  $last = ['Ward', 'Hall', 'Cook', 'Stone', 'Page', 'Hill', 'Reed', 'Wood', 'King', 'Fox', 'Gray', 'Cole', 'Shaw', 'Banks', 'Mills', 'Wells', 'Cross', 'Lane'];
  $offset = $side === 'home' ? 0 : 7;
  return $first[($index + $offset) % count($first)] . ' ' . $last[($index * 3 + $offset) % count($last)];
}

function squad_role_from_legacy_row(?array $legacyRow, string $fallback, int $seed = 0): string {
  $fallback = canonical_player_role($fallback, 'CM');
  $legacyRole = trim((string)($legacyRow['legacyRole'] ?? ''));
  if ($legacyRole === '') return $fallback;
  $role = canonical_player_role($legacyRole, '');
  if ($role !== '') return $role;
  $normalized = str_replace(['-', '_'], ' ', strtoupper($legacyRole));
  if (str_contains($normalized, 'GOAL')) return 'GK';
  if (str_contains($normalized, 'LEFT')) {
    if (str_contains($normalized, 'BACK') || str_contains($normalized, 'DEF')) return 'LB';
    if (str_contains($normalized, 'WING') || str_contains($normalized, 'MID')) return 'LM';
  }
  if (str_contains($normalized, 'RIGHT')) {
    if (str_contains($normalized, 'BACK') || str_contains($normalized, 'DEF')) return 'RB';
    if (str_contains($normalized, 'WING') || str_contains($normalized, 'MID')) return 'RM';
  }
  if (str_contains($normalized, 'BACK') || str_contains($normalized, 'DEF')) {
    if (role_family($fallback) === 'defender') return $fallback;
    return ['CB', 'RB', 'LB'][abs($seed) % 3];
  }
  if (str_contains($normalized, 'MID') || str_contains($normalized, 'WING')) {
    if (role_family($fallback) === 'midfielder') return $fallback;
    return ['CM', 'RM', 'LM'][abs($seed) % 3];
  }
  if (str_contains($normalized, 'FORWARD') || str_contains($normalized, 'ATTACK') || str_contains($normalized, 'STRIK')) return 'ST';
  return $fallback;
}

function squad_role_for_legacy_index(string $teamId, string $teamName, int $index, string $fallback): string {
  if ($teamId === '') return canonical_player_role($fallback, 'CM');
  $legacy = legacy_team_players($teamId, $teamName);
  $row = is_array($legacy[$index] ?? null) ? $legacy[$index] : null;
  return squad_role_from_legacy_row($row, $fallback, (int)crc32($teamId . '|' . $teamName . '|' . $index));
}

function squad_reserve_slots(): array {
  return [
    ['GK', 0.50, 0.94],
    ['CB', 0.50, 0.80],
    ['LB', 0.18, 0.78],
    ['RB', 0.82, 0.78],
    ['CM', 0.42, 0.61],
    ['CM', 0.58, 0.61],
    ['LM', 0.16, 0.58],
    ['RM', 0.84, 0.58],
    ['ST', 0.42, 0.36],
    ['ST', 0.58, 0.36],
    ['GK', 0.50, 0.96],
    ['CB', 0.38, 0.82],
    ['CB', 0.62, 0.82],
    ['LB', 0.14, 0.80],
    ['RB', 0.86, 0.80],
    ['CM', 0.50, 0.66],
    ['LM', 0.20, 0.62],
    ['RM', 0.80, 0.62],
    ['ST', 0.50, 0.48],
    ['CM', 0.50, 0.58],
    ['CB', 0.50, 0.84],
    ['ST', 0.50, 0.40],
  ];
}

function squad_reserve_count(string $teamId = '', string $teamName = ''): int {
  $wanted = MANAGER_MAX_BENCH_PLAYERS;
  if ($teamId !== '') {
    $legacyCount = count(legacy_team_players($teamId, $teamName));
    if ($legacyCount > 0) $wanted = max(0, $legacyCount - count(base_formation()));
  }
  return clamp_int($wanted, 0, MANAGER_MAX_SQUAD_RESERVES);
}

function make_roster(string $side, int $baseRating, string $teamId = '', string $teamName = ''): array {
  $players = [];
  foreach (base_formation() as $index => $row) {
    [$slotRole, $x, $homeY] = $row;
    $naturalRole = squad_role_for_legacy_index($teamId, $teamName, $index, $slotRole);
    $y = $side === 'away' ? 1 - $homeY : $homeY;
    $stats = [];
    foreach (player_stat_keys($naturalRole === 'GK') as $stat) {
      $stats[$stat] = stat_value($baseRating, $naturalRole, $stat);
    }
	    $players[] = [
	      'id' => generated_squad_player_id($teamId, $teamName, $side, $index + 1),
	      'shirtNumber' => $index + 1,
	      'name' => player_name($side, $index, $naturalRole),
	      'role' => $naturalRole,
	      'naturalRole' => $naturalRole,
	      'slotRole' => $slotRole,
	      'rating' => average_stats($stats),
      'stats' => $stats,
      'weightClass' => random_weight_class(),
      'homeX' => $x,
      'homeY' => $y,
      'x' => $x,
      'y' => $y,
    ];
  }
  return $players;
}

function make_bench(string $side, int $baseRating, string $teamId = '', string $teamName = ''): array {
  $players = [];
  $slots = squad_reserve_slots();
  $count = squad_reserve_count($teamId, $teamName);
  for ($index = 0; $index < $count; $index++) {
    $row = $slots[$index % count($slots)];
    [$fallbackRole, $x, $homeY] = $row;
    $role = squad_role_for_legacy_index($teamId, $teamName, $index + count(base_formation()), $fallbackRole);
    $y = $side === 'away' ? 1 - $homeY : $homeY;
    $stats = [];
    foreach (player_stat_keys($role === 'GK') as $stat) {
      $stats[$stat] = stat_value($baseRating - 2, $role, $stat);
    }
	    $players[] = [
	      'id' => generated_squad_player_id($teamId, $teamName, $side, $index + 12),
	      'shirtNumber' => $index + 12,
	      'name' => player_name($side, $index + 11, $role),
	      'role' => $role,
	      'naturalRole' => $role,
	      'slotRole' => $role,
	      'rating' => average_stats($stats),
      'stats' => $stats,
      'weightClass' => random_weight_class(),
      'homeX' => $x,
      'homeY' => $y,
      'x' => $x,
      'y' => $y,
    ];
  }
  return $players;
}

function make_named_squad(string $side, int $baseRating, string $teamId = '', string $teamName = ''): array {
  $squad = [
    'starters' => make_roster($side, $baseRating, $teamId, $teamName),
    'bench' => make_bench($side, $baseRating, $teamId, $teamName),
  ];
  if ($teamId !== '') apply_legacy_names_to_squad($squad, $teamId, $teamName);
  normalize_squad_player_ids($squad, $teamId, $teamName, $side);
  if ($teamId !== '') apply_squad_nationalities($squad, $teamId, $teamName);
  apply_formation_to_squad($squad, '4-4-2', [], true);
  return $squad;
}

function player_stat(array $player, string $stat): int {
  if (isset($player['stats'][$stat])) $value = (int)$player['stats'][$stat];
  elseif ($stat === 'passcontrol' || $stat === 'shotcontrol') $value = (int)($player['stats']['control'] ?? $player['rating'] ?? 50);
  elseif ($stat === 'keeping' && player_natural_role($player) === 'GK') $value = (int)($player['rating'] ?? 50);
  else $value = (int)($player['rating'] ?? 50);
  $multiplier = player_condition_multiplier($player);
  if (abs($multiplier - 1.0) > 0.0001) $value = (int)round($value * $multiplier);
  return clamp_int($value, 0, 100);
}

function average_stats(array $stats): int {
  $keys = player_stat_keys(isset($stats['keeping']));
  $sum = 0;
  $count = 0;
  foreach ($keys as $key) {
    if (isset($stats[$key])) {
      $sum += (int)$stats[$key];
      $count++;
    }
  }
  return $count ? (int)round($sum / $count) : 0;
}

function default_engine(int $homeBase = 50, int $awayBase = 50): array {
  return [
    'baseRatings' => ['home' => $homeBase, 'away' => $awayBase],
    'touches' => ['home' => 0, 'away' => 0],
    'possession' => ['home' => 0.0, 'away' => 0.0],
    'shots' => ['home' => 0, 'away' => 0],
    'tackles' => ['home' => 0, 'away' => 0],
    'fouls' => ['home' => 0, 'away' => 0],
    'yellows' => ['home' => 0, 'away' => 0],
    'reds' => ['home' => 0, 'away' => 0],
    'subsUsed' => ['home' => 0, 'away' => 0],
    'saves' => ['home' => 0, 'away' => 0],
    'crits' => ['home' => 0, 'away' => 0],
    'lastTouch' => null,
    'lastTouchAt' => -10.0,
    'lastShotSeq' => 0,
    'lastShotAt' => null,
    'lastShotBy' => null,
    'lastPass' => null,
    'goalSeq' => 0,
    'lastGoal' => null,
    'goalScorers' => [],
    'ballFreeUntil' => 0.0,
    'owner' => null,
    'passTarget' => null,
    'shot' => null,
    'nextDecisionAt' => 0.0,
    'kickoffUntil' => 0.0,
    'kickoffDirection' => null,
    'kickoffSide' => null,
    'kickoffTaker' => null,
    'kickoffReceiver' => null,
    'kickoffWhistled' => false,
    'kickoffWhistleUntil' => 0.0,
    'kickoffWhistleSeq' => 0,
    'freeKickUntil' => 0.0,
    'freeKickSignalUntil' => 0.0,
    'freeKickSide' => null,
    'freeKickSpot' => null,
    'freeKickKicker' => null,
    'penaltySide' => null,
    'penaltyKicker' => null,
    'penaltyUntil' => 0.0,
    'penaltySignalUntil' => 0.0,
    'penaltyWhistled' => false,
    'restartType' => null,
    'restartSide' => null,
    'restartSpot' => null,
    'restartKicker' => null,
    'restartStartedAt' => 0.0,
    'restartRollUntil' => 0.0,
    'restartDeliveryUntil' => 0.0,
    'restartUntil' => 0.0,
	    'restartSignalUntil' => 0.0,
	    'restartWhistled' => false,
	    'restartSeq' => 0,
	    'restartLineBall' => null,
	    'restartOutBall' => null,
	    'playerStats' => ['home' => [], 'away' => []],
	  ];
	}

function ensure_match_engine(array &$match): void {
  if (!isset($match['engine']) || !is_array($match['engine'])) $match['engine'] = default_engine();
  $match['engine'] += default_engine();
	  foreach (['touches', 'possession', 'shots', 'tackles', 'fouls', 'yellows', 'reds', 'subsUsed', 'saves', 'crits'] as $key) {
	    $match['engine'][$key] += default_engine()[$key];
	  }
	  if (!isset($match['engine']['playerStats']) || !is_array($match['engine']['playerStats'])) $match['engine']['playerStats'] = ['home' => [], 'away' => []];
	  $match['engine']['playerStats'] += ['home' => [], 'away' => []];
	  if (!isset($match['units']) || !is_array($match['units'])) $match['units'] = ['home' => [], 'away' => []];
	  if (!isset($match['bench']) || !is_array($match['bench'])) $match['bench'] = ['home' => [], 'away' => []];
  $homeTeam = is_array($match['teams']['home'] ?? null) ? $match['teams']['home'] : [];
  $awayTeam = is_array($match['teams']['away'] ?? null) ? $match['teams']['away'] : [];
  $homeSquad = make_named_squad('home', (int)($match['engine']['baseRatings']['home'] ?? 30), team_source_key($homeTeam), (string)($homeTeam['name'] ?? ''));
  $awaySquad = make_named_squad('away', (int)($match['engine']['baseRatings']['away'] ?? 60), team_source_key($awayTeam), (string)($awayTeam['name'] ?? ''));
  if (empty($match['units']['home'])) $match['units']['home'] = $homeSquad['starters'];
  if (!empty($match['teams']['away']) && empty($match['units']['away'])) $match['units']['away'] = $awaySquad['starters'];
  if (empty($match['bench']['home'])) $match['bench']['home'] = $homeSquad['bench'];
  if (!empty($match['teams']['away']) && empty($match['bench']['away'])) $match['bench']['away'] = $awaySquad['bench'];
  foreach (['home', 'away'] as $side) {
    if (!empty($match['bench'][$side]) && is_array($match['bench'][$side])) {
      $match['bench'][$side] = array_slice($match['bench'][$side], 0, MANAGER_MAX_BENCH_PLAYERS);
    }
  }
  foreach (['units' => 0, 'bench' => 11] as $group => $offset) {
    foreach (['home', 'away'] as $side) {
      if (empty($match[$group][$side]) || !is_array($match[$group][$side])) continue;
      foreach ($match[$group][$side] as $index => &$player) {
        if (empty($player['name'])) $player['name'] = player_name($side, $index + $offset, (string)($player['role'] ?? 'CM'));
      }
	      unset($player);
	    }
	  }
	  ensure_match_telemetry($match);
	}

	function telemetry_player_defaults(array $player = [], string $side = ''): array {
	  return [
	    'id' => (string)($player['id'] ?? ''),
	    'name' => (string)($player['name'] ?? ($player['id'] ?? 'Player')),
	    'role' => (string)($player['role'] ?? ''),
	    'side' => $side,
	    'distanceMeters' => 0.0,
	    'activeSeconds' => 0.0,
	    'idleSeconds' => 0.0,
	    'slackSeconds' => 0.0,
	    'possessionSeconds' => 0.0,
	    'touches' => 0,
	    'passes' => 0,
	    'longPasses' => 0,
	    'widePasses' => 0,
	    'shots' => 0,
	    'tackles' => 0,
	    'fouls' => 0,
	    'yellows' => 0,
	    'reds' => 0,
	    'secondYellowReds' => 0,
	    'directReds' => 0,
	    'saves' => 0,
	    'crits' => 0,
	    'lostBalls' => 0,
	    'complaints' => 0,
	    'samples' => 0,
	    'xSum' => 0.0,
	    'ySum' => 0.0,
	  ];
	}

	function telemetry_key(array $player, int $fallback = 0): string {
	  $id = (string)($player['id'] ?? '');
	  return $id !== '' ? $id : 'p' . $fallback;
	}

	function telemetry_register_player(array &$match, string $side, array $player, int $fallback = 0): void {
	  if (!isset($match['engine']['playerStats'][$side]) || !is_array($match['engine']['playerStats'][$side])) $match['engine']['playerStats'][$side] = [];
	  $key = telemetry_key($player, $fallback);
	  $defaults = telemetry_player_defaults($player, $side);
	  if (!isset($match['engine']['playerStats'][$side][$key]) || !is_array($match['engine']['playerStats'][$side][$key])) {
	    $match['engine']['playerStats'][$side][$key] = $defaults;
	  } else {
	    $match['engine']['playerStats'][$side][$key] = $match['engine']['playerStats'][$side][$key] + $defaults;
	    $match['engine']['playerStats'][$side][$key]['name'] = (string)($player['name'] ?? $match['engine']['playerStats'][$side][$key]['name']);
	    $match['engine']['playerStats'][$side][$key]['role'] = (string)($player['role'] ?? $match['engine']['playerStats'][$side][$key]['role']);
	  }
	}

	function ensure_match_telemetry(array &$match): void {
	  if (!isset($match['engine']['playerStats']) || !is_array($match['engine']['playerStats'])) $match['engine']['playerStats'] = ['home' => [], 'away' => []];
	  foreach (['home', 'away'] as $side) {
	    foreach (($match['units'][$side] ?? []) as $index => $player) telemetry_register_player($match, $side, $player, (int)$index);
	    foreach (($match['bench'][$side] ?? []) as $index => $player) telemetry_register_player($match, $side, $player, 11 + (int)$index);
	  }
	}

	function telemetry_add(array &$match, string $side, array $player, array $fields): void {
	  if (!in_array($side, ['home', 'away'], true)) return;
	  telemetry_register_player($match, $side, $player);
	  $key = telemetry_key($player);
	  foreach ($fields as $field => $amount) {
	    if (!is_numeric($amount)) continue;
	    $match['engine']['playerStats'][$side][$key][$field] = (float)($match['engine']['playerStats'][$side][$key][$field] ?? 0) + (float)$amount;
	  }
	}

	function match_player_card_payload(int $yellow, int $red, bool $secondYellow = false, bool $directRed = false): array {
	  $yellow = max(0, $yellow);
	  $red = max(0, $red);
	  if ($secondYellow) {
	    $yellow = max(2, $yellow);
	    $red = max(1, $red);
	  }
	  if ($yellow >= 2) {
	    $red = max(1, $red);
	    if (!$directRed) $secondYellow = true;
	  }
	  return [
	    'yellow' => $yellow,
	    'red' => $red,
	    'secondYellow' => $secondYellow,
	    'directRed' => $directRed,
	  ];
	}

	function match_player_cards_from_stat(array $stat): array {
	  return match_player_card_payload(
	    (int)($stat['yellows'] ?? 0),
	    (int)($stat['reds'] ?? 0),
	    (int)($stat['secondYellowReds'] ?? 0) > 0,
	    (int)($stat['directReds'] ?? 0) > 0
	  );
	}

	function match_card_payload_has_cards(array $cards): bool {
	  return (int)($cards['yellow'] ?? 0) > 0 || (int)($cards['red'] ?? 0) > 0;
	}

	function manager_normalize_report_card_row(array $row): array {
	  $rawCards = is_array($row['cards'] ?? null) ? $row['cards'] : [];
	  $cards = match_player_card_payload(
	    max((int)($rawCards['yellow'] ?? $rawCards['yellows'] ?? 0), (int)($row['yellows'] ?? 0)),
	    max((int)($rawCards['red'] ?? $rawCards['reds'] ?? 0), (int)($row['reds'] ?? 0)),
	    !empty($rawCards['secondYellow']) || (int)($row['secondYellowReds'] ?? 0) > 0,
	    !empty($rawCards['directRed']) || (int)($row['directReds'] ?? 0) > 0
	  );
	  $row['cards'] = $cards;
	  $row['yellows'] = max((int)($row['yellows'] ?? 0), (int)($cards['yellow'] ?? 0));
	  $row['reds'] = max((int)($row['reds'] ?? 0), (int)($cards['red'] ?? 0));
	  if (!empty($cards['secondYellow'])) $row['secondYellowReds'] = max(1, (int)($row['secondYellowReds'] ?? 0));
	  if (!empty($cards['directRed'])) $row['directReds'] = max(1, (int)($row['directReds'] ?? 0));
	  return $row;
	}

	function manager_normalize_match_report_cards(array $report): array {
	  foreach (['players', 'cardedPlayers'] as $key) {
	    if (empty($report[$key]) || !is_array($report[$key])) continue;
	    $report[$key] = array_values(array_map(fn($row) => is_array($row) ? manager_normalize_report_card_row($row) : $row, $report[$key]));
	  }
	  $totals = [
	    'yellows' => ['home' => 0, 'away' => 0],
	    'reds' => ['home' => 0, 'away' => 0],
	  ];
	  $seen = [];
	  foreach (['players', 'cardedPlayers'] as $key) {
	    foreach (($report[$key] ?? []) as $row) {
	      if (!is_array($row)) continue;
	      $side = (string)($row['side'] ?? '');
	      if (!in_array($side, ['home', 'away'], true)) continue;
	      $playerKey = (string)($row['id'] ?? '');
	      if ($playerKey === '') $playerKey = (string)($row['name'] ?? '');
	      if ($playerKey === '') $playerKey = md5(json_encode($row));
	      $dedupe = $side . ':' . $playerKey;
	      if (isset($seen[$dedupe])) continue;
	      $seen[$dedupe] = true;
	      $totals['yellows'][$side] += (int)($row['yellows'] ?? $row['cards']['yellow'] ?? 0);
	      $totals['reds'][$side] += (int)($row['reds'] ?? $row['cards']['red'] ?? 0);
	    }
	  }
	  foreach (['teamStats', 'discipline'] as $section) {
	    if (!isset($report[$section]) || !is_array($report[$section])) $report[$section] = [];
	    foreach (['yellows', 'reds'] as $field) {
	      if (!isset($report[$section][$field]) || !is_array($report[$section][$field])) $report[$section][$field] = ['home' => 0, 'away' => 0];
	      foreach (['home', 'away'] as $side) {
	        $report[$section][$field][$side] = max((int)($report[$section][$field][$side] ?? 0), (int)$totals[$field][$side]);
	      }
	    }
	  }
	  return $report;
	}

	function player_matches_card_target(array $player, string $id, string $name): bool {
	  $playerId = (string)($player['id'] ?? '');
	  if ($id !== '' && $playerId === $id) return true;
	  return $id === '' && $name !== '' && (string)($player['name'] ?? '') === $name;
	}

	function sync_match_player_cards(array &$match, string $side, array $player): void {
	  if (!in_array($side, ['home', 'away'], true)) return;
	  telemetry_register_player($match, $side, $player);
	  $key = telemetry_key($player);
	  $stat = $match['engine']['playerStats'][$side][$key] ?? [];
	  $cards = match_player_cards_from_stat(is_array($stat) ? $stat : []);
	  $id = (string)($player['id'] ?? '');
	  $name = (string)($player['name'] ?? '');
	  foreach (['units', 'bench'] as $group) {
	    if (empty($match[$group][$side]) || !is_array($match[$group][$side])) continue;
	    foreach ($match[$group][$side] as &$target) {
	      if (!player_matches_card_target($target, $id, $name)) continue;
	      if (match_card_payload_has_cards($cards)) {
	        $target['matchCards'] = $cards;
	      } else {
	        unset($target['matchCards']);
	      }
	      if ((int)($cards['red'] ?? 0) > 0) $target['sentOff'] = true;
	    }
	    unset($target);
	  }
	}

	function send_off_match_player(array &$match, string $side, array $player): void {
	  if (!in_array($side, ['home', 'away'], true)) return;
	  $id = (string)($player['id'] ?? '');
	  $name = (string)($player['name'] ?? '');
	  foreach (($match['units'][$side] ?? []) as $index => $target) {
	    if (!player_matches_card_target($target, $id, $name)) continue;
	    array_splice($match['units'][$side], (int)$index, 1);
	    clear_owner($match);
	    $match['engine']['passTarget'] = null;
	    $match['engine']['shot'] = null;
	    $match['engine']['ballFreeUntil'] = max((float)($match['engine']['ballFreeUntil'] ?? 0), (float)($match['elapsed'] ?? 0) + 0.8);
	    return;
	  }
	}

	function telemetry_complaint_chance(array $player, float $base): float {
	  return clamp_float($base + player_stat($player, 'aggression') / 520 - player_stat($player, 'discipline') / 620 - player_stat($player, 'composure') / 760, 0.0, 0.72);
	}

	function telemetry_maybe_complain(array &$match, string $side, array $player, float $base): void {
	  if (rand_float() < telemetry_complaint_chance($player, $base)) telemetry_add($match, $side, $player, ['complaints' => 1]);
	}

function telemetry_record_movement(array &$match, string $side, array $player, float $oldX, float $oldY, float $step, bool $nearPlay = false, bool $urgent = false): void {
  $dx = (float)($player['x'] ?? $oldX) - $oldX;
  $dy = (float)($player['y'] ?? $oldY) - $oldY;
  $meters = sqrt(($dx * 68.0) ** 2 + ($dy * 105.0) ** 2);
	  $idle = $meters < 0.055 ? $step : 0.0;
	  $slack = ($nearPlay && !$urgent && $meters < 0.035) ? $step : 0.0;
	  telemetry_add($match, $side, $player, [
	    'distanceMeters' => $meters,
	    'activeSeconds' => $meters >= 0.055 ? $step : 0.0,
	    'idleSeconds' => $idle,
	    'slackSeconds' => $slack,
	    'samples' => 1,
	    'xSum' => (float)($player['x'] ?? $oldX),
    'ySum' => (float)($player['y'] ?? $oldY),
  ]);
}

function manager_match_behavior_label(array $row): string {
  $role = strtoupper((string)($row['role'] ?? ''));
  $distance = (float)($row['distanceMeters'] ?? 0);
  if ((int)($row['reds'] ?? 0) > 0 || (int)($row['yellows'] ?? 0) >= 2) return 'SENT OFF';
  if ((int)($row['yellows'] ?? 0) > 0) return 'BOOKED';
  if ((int)($row['complaints'] ?? 0) >= 2) return 'MOANED';
  if ($role !== 'GK' && $distance > 0 && $distance < 220) return 'SLACKED';
  if ((int)($row['lostBalls'] ?? 0) >= 3) return 'LOST BALLS';
  if ((int)($row['saves'] ?? 0) >= 2) return 'KEPT TEAM IN IT';
  if ((int)($row['shots'] ?? 0) >= 3) return 'SHOT A LOT';
  if ((int)($row['tackles'] ?? 0) >= 3) return 'WORKED HARD';
  if ((int)($row['passes'] ?? 0) >= 5) return 'PASSED WELL';
  return 'OK';
}

function manager_report_player_rows(array $match): array {
  $rows = [];
  foreach (['home', 'away'] as $side) {
    $team = is_array($match['teams'][$side] ?? null) ? $match['teams'][$side] : [];
    $teamName = (string)($team['name'] ?? strtoupper($side));
    foreach (($match['engine']['playerStats'][$side] ?? []) as $stat) {
      if (!is_array($stat)) continue;
      $samples = (int)($stat['samples'] ?? 0);
      $actions = (int)($stat['touches'] ?? 0) + (int)($stat['passes'] ?? 0) + (int)($stat['shots'] ?? 0) + (int)($stat['tackles'] ?? 0) + (int)($stat['saves'] ?? 0);
      if ($samples <= 0 && $actions <= 0) continue;
      $distance = (float)($stat['distanceMeters'] ?? 0);
      $positive = (int)($stat['touches'] ?? 0) * 1.1
        + (int)($stat['passes'] ?? 0) * 0.8
        + (int)($stat['longPasses'] ?? 0) * 0.3
        + (int)($stat['widePasses'] ?? 0) * 0.25
        + (int)($stat['shots'] ?? 0) * 1.4
        + (int)($stat['tackles'] ?? 0) * 1.3
        + (int)($stat['saves'] ?? 0) * 1.8
        + (int)($stat['crits'] ?? 0) * 1.6
        + $distance / 125.0;
      $statCards = match_player_cards_from_stat($stat);
      $statYellows = (int)($statCards['yellow'] ?? 0);
      $statReds = (int)($statCards['red'] ?? 0);
      $negative = (int)($stat['lostBalls'] ?? 0) * 1.4
        + (int)($stat['fouls'] ?? 0) * 1.5
        + $statYellows * 2.2
        + $statReds * 6.0
        + (int)($stat['complaints'] ?? 0) * 0.7
        + (float)($stat['slackSeconds'] ?? 0) / 8.0;
      $impact = round($positive - $negative, 1);
      $row = [
        'id' => (string)($stat['id'] ?? ''),
        'name' => (string)($stat['name'] ?? 'Player'),
        'role' => (string)($stat['role'] ?? ''),
        'side' => $side,
        'teamName' => $teamName,
        'distanceMeters' => (int)round($distance),
        'activeSeconds' => round((float)($stat['activeSeconds'] ?? 0), 1),
        'idleSeconds' => round((float)($stat['idleSeconds'] ?? 0), 1),
        'slackSeconds' => round((float)($stat['slackSeconds'] ?? 0), 1),
        'possessionSeconds' => round((float)($stat['possessionSeconds'] ?? 0), 1),
        'touches' => (int)($stat['touches'] ?? 0),
        'passes' => (int)($stat['passes'] ?? 0),
        'longPasses' => (int)($stat['longPasses'] ?? 0),
        'widePasses' => (int)($stat['widePasses'] ?? 0),
        'shots' => (int)($stat['shots'] ?? 0),
        'tackles' => (int)($stat['tackles'] ?? 0),
        'fouls' => (int)($stat['fouls'] ?? 0),
        'yellows' => $statYellows,
        'reds' => $statReds,
        'secondYellowReds' => !empty($statCards['secondYellow']) ? 1 : (int)($stat['secondYellowReds'] ?? 0),
        'directReds' => !empty($statCards['directRed']) ? 1 : (int)($stat['directReds'] ?? 0),
        'saves' => (int)($stat['saves'] ?? 0),
        'crits' => (int)($stat['crits'] ?? 0),
        'lostBalls' => (int)($stat['lostBalls'] ?? 0),
        'complaints' => (int)($stat['complaints'] ?? 0),
        'impact' => $impact,
      ];
      $row['cards'] = $statCards;
      $row['behavior'] = manager_match_behavior_label($row);
      $row['terrible'] = $impact <= -2.0 || (float)$row['slackSeconds'] >= 6.0 || ((int)$row['lostBalls'] >= 4 && (int)$row['touches'] <= 4);
      $rows[] = $row;
    }
  }
  usort($rows, fn($a, $b) => ((float)($b['impact'] ?? 0) <=> (float)($a['impact'] ?? 0)) ?: ((int)($b['distanceMeters'] ?? 0) <=> (int)($a['distanceMeters'] ?? 0)));
  return $rows;
}

function manager_report_top(array $players, string $field, bool $lowest = false): ?array {
  $best = null;
  $bestValue = $lowest ? PHP_FLOAT_MAX : -PHP_FLOAT_MAX;
  foreach ($players as $player) {
    $value = (float)($player[$field] ?? 0);
    if ((!$lowest && $value <= 0) || ($lowest && $value >= 999999)) continue;
    if (($lowest && $value < $bestValue) || (!$lowest && $value > $bestValue)) {
      $bestValue = $value;
      $best = $player;
    }
  }
  return $best;
}

function manager_report_slacker(array $players): ?array {
  $candidates = array_values(array_filter($players, function ($player) {
    if (strtoupper((string)($player['role'] ?? '')) === 'GK') return false;
    return is_numeric($player['distanceMeters'] ?? null);
  }));
  if (!$candidates) {
    $candidates = array_values(array_filter($players, fn($player) => is_numeric($player['distanceMeters'] ?? null)));
  }
  if (!$candidates) return null;
  usort($candidates, fn($a, $b) => ((float)($a['distanceMeters'] ?? 0) <=> (float)($b['distanceMeters'] ?? 0)) ?: ((float)($a['impact'] ?? 0) <=> (float)($b['impact'] ?? 0)));
  return $candidates[0] ?? null;
}

function manager_report_sum(array $players, string $side, string $field): int {
  $sum = 0;
  foreach ($players as $player) {
    if ((string)($player['side'] ?? '') === $side) $sum += (int)($player[$field] ?? 0);
  }
  return $sum;
}

function manager_report_carded_players(array $players): array {
  return array_values(array_filter($players, fn($player) => is_array($player) && match_card_payload_has_cards(is_array($player['cards'] ?? null) ? $player['cards'] : match_player_cards_from_stat($player))));
}

function manager_match_side_has_player_name(array $match, string $side, string $name): bool {
  if ($name === '') return false;
  foreach (($match['units'][$side] ?? []) as $player) {
    if (strcasecmp((string)($player['name'] ?? ''), $name) === 0) return true;
  }
  return false;
}

function manager_report_goal_scorer_rows(array $goals, array $match = []): array {
  $rows = [];
  foreach ($goals as $index => $goal) {
    if (!is_array($goal)) continue;
    $side = (string)($goal['side'] ?? '');
    if (!in_array($side, ['home', 'away'], true)) continue;
    $scorerId = (string)($goal['scorerId'] ?? '');
    $scorer = trim((string)($goal['scorer'] ?? ''));
    if ($scorerId !== '' && $match) $scorer = player_name_by_id($match, $side, $scorerId) ?? $scorer;
    if ($match && $scorer !== '' && !manager_match_side_has_player_name($match, $side, $scorer)) $scorer = '';
    $assist = trim((string)($goal['assist'] ?? ''));
    if ($match && $assist !== '' && !manager_match_side_has_player_name($match, $side, $assist)) $assist = '';
    $at = (float)($goal['at'] ?? $goal['minute'] ?? 0);
    $rows[] = [
      'seq' => (int)($goal['seq'] ?? ($index + 1)),
      'at' => $at,
      'minute' => clamp_int((int)round($at), 1, 90),
      'side' => $side,
      'team' => (string)($goal['team'] ?? ($match ? side_team_name($match, $side) : strtoupper($side))),
      'scorer' => $scorer !== '' ? $scorer : 'Unknown',
      'scorerId' => $scorerId,
      'assist' => $assist,
      'penalty' => !empty($goal['penalty']),
    ];
  }
  return $rows;
}

function manager_simulated_goal_scorer_rows(array $players, array $score, array $homeTeam, array $awayTeam): array {
  $rows = [];
  foreach (['home' => 0, 'away' => 1] as $side => $scoreIndex) {
    $count = max(0, (int)($score[$scoreIndex] ?? 0));
    if ($count <= 0) continue;
    $pool = array_values(array_filter($players, fn($player) => is_array($player) && (string)($player['side'] ?? '') === $side && strtoupper((string)($player['role'] ?? '')) !== 'GK'));
    usort($pool, function ($a, $b) {
      $roleScore = fn($p) => match (strtoupper((string)($p['role'] ?? ''))) {
        'ST' => 6,
        'LM', 'RM' => 4,
        'CM' => 3,
        'LB', 'RB' => 2,
        default => 1,
      };
      return ($roleScore($b) <=> $roleScore($a))
        ?: ((int)($b['shots'] ?? 0) <=> (int)($a['shots'] ?? 0))
        ?: ((float)($b['impact'] ?? 0) <=> (float)($a['impact'] ?? 0));
    });
    if (!$pool) continue;
    for ($i = 0; $i < $count; $i++) {
      $player = $pool[$i % count($pool)];
      $minute = clamp_int((int)round(8 + (($i + 1) / ($count + 1)) * 74), 1, 90);
      $rows[] = [
        'seq' => count($rows) + 1,
        'at' => $minute,
        'minute' => $minute,
        'side' => $side,
        'team' => (string)(($side === 'home' ? $homeTeam : $awayTeam)['name'] ?? strtoupper($side)),
        'scorer' => (string)($player['name'] ?? 'Player'),
        'scorerId' => (string)($player['id'] ?? ''),
        'assist' => '',
        'penalty' => false,
      ];
    }
  }
  usort($rows, fn($a, $b) => ((int)($a['minute'] ?? 0) <=> (int)($b['minute'] ?? 0)) ?: ((int)($a['seq'] ?? 0) <=> (int)($b['seq'] ?? 0)));
  return $rows;
}

function manager_build_match_report(array $season, array $fixture, int $roundIndex, int $fixtureIndex, array $score, array $match): array {
  ensure_match_engine($match);
  $players = manager_report_player_rows($match);
  $slacker = manager_report_slacker($players);
  $moaner = manager_report_top($players, 'complaints');
  $worst = manager_report_top($players, 'impact', true);
  if ($worst !== null && empty($worst['terrible']) && (float)($worst['impact'] ?? 0) > -1.0) $worst = null;
  $teamYellows = [
    'home' => max((int)($match['engine']['yellows']['home'] ?? 0), manager_report_sum($players, 'home', 'yellows')),
    'away' => max((int)($match['engine']['yellows']['away'] ?? 0), manager_report_sum($players, 'away', 'yellows')),
  ];
  $teamReds = [
    'home' => max((int)($match['engine']['reds']['home'] ?? 0), manager_report_sum($players, 'home', 'reds')),
    'away' => max((int)($match['engine']['reds']['away'] ?? 0), manager_report_sum($players, 'away', 'reds')),
  ];
  return [
    'id' => (string)($season['id'] ?? 'season') . ':' . $roundIndex . ':' . $fixtureIndex,
    'seasonId' => (string)($season['id'] ?? ''),
    'roundIndex' => $roundIndex,
    'fixtureIndex' => $fixtureIndex,
    'round' => $fixture['round'] ?? '',
    'phase' => (string)($fixture['phase'] ?? 'league'),
    'playedAt' => time(),
    'simulated' => false,
    'home' => (string)($fixture['home'] ?? ''),
    'away' => (string)($fixture['away'] ?? ''),
    'homeTeam' => $match['teams']['home'] ?? [],
    'awayTeam' => $match['teams']['away'] ?? [],
    'score' => [(int)($score[0] ?? 0), (int)($score[1] ?? 0)],
    'teamStats' => [
      'possession' => $match['engine']['possession'] ?? ['home' => 0, 'away' => 0],
      'touches' => $match['engine']['touches'] ?? ['home' => 0, 'away' => 0],
      'shots' => $match['engine']['shots'] ?? ['home' => 0, 'away' => 0],
      'tackles' => $match['engine']['tackles'] ?? ['home' => 0, 'away' => 0],
      'fouls' => $match['engine']['fouls'] ?? ['home' => 0, 'away' => 0],
      'yellows' => $teamYellows,
      'reds' => $teamReds,
      'saves' => $match['engine']['saves'] ?? ['home' => 0, 'away' => 0],
    ],
    'discipline' => [
      'yellows' => $teamYellows,
      'reds' => $teamReds,
      'freeKicks' => [
        'home' => (int)($match['engine']['fouls']['away'] ?? 0),
        'away' => (int)($match['engine']['fouls']['home'] ?? 0),
      ],
      'penalties' => [
        'home' => count(array_filter($match['engine']['goalScorers'] ?? [], fn($goal) => ($goal['side'] ?? '') === 'home' && !empty($goal['penalty']))),
        'away' => count(array_filter($match['engine']['goalScorers'] ?? [], fn($goal) => ($goal['side'] ?? '') === 'away' && !empty($goal['penalty']))),
      ],
      'complaints' => [
        'home' => manager_report_sum($players, 'home', 'complaints'),
        'away' => manager_report_sum($players, 'away', 'complaints'),
      ],
    ],
    'highlights' => [
      'mostBall' => manager_report_top($players, 'possessionSeconds'),
      'workhorse' => manager_report_top($players, 'distanceMeters'),
      'slacker' => $slacker,
      'moaner' => ($moaner && (int)($moaner['complaints'] ?? 0) > 0) ? $moaner : null,
      'worst' => $worst,
    ],
    'cardedPlayers' => manager_report_carded_players($players),
    'goalScorers' => manager_report_goal_scorer_rows($match['engine']['goalScorers'] ?? [], $match),
    'players' => array_slice($players, 0, 28),
  ];
}

function manager_simulated_player_row(array $player, string $side, string $teamName, int $gf, int $ga): array {
  $role = strtoupper((string)($player['role'] ?? 'CM'));
  $speed = player_stat($player, 'speed');
  $workrate = player_stat($player, 'workrate');
  $stamina = player_stat($player, 'stamina');
  $passing = player_stat($player, 'passing');
  $passControl = player_stat($player, 'passcontrol');
  $shooting = player_stat($player, 'shooting');
  $shotControl = player_stat($player, 'shotcontrol');
  $tackling = player_stat($player, 'tackling');
  $positioning = player_stat($player, 'positioning');
  $composure = player_stat($player, 'composure');
  $aggression = player_stat($player, 'aggression');
  $discipline = player_stat($player, 'discipline');
  $keeping = player_stat($player, 'keeping');
  $roleMove = match ($role) {
    'GK' => 0.45,
    'CB', 'LB', 'RB' => 0.86,
    'CM', 'LM', 'RM' => 1.08,
    default => 0.98,
  };
  $distance = (int)round((5200 + $speed * 26 + $workrate * 31 + $stamina * 18 + mt_rand(-520, 760)) * $roleMove / 1.35);
  $touchBase = match ($role) {
    'GK' => 12,
    'CB', 'LB', 'RB' => 34,
    'CM', 'LM', 'RM' => 48,
    default => 29,
  };
  $touches = max(1, (int)round($touchBase + ($passing + $passControl + $positioning) / 12 + mt_rand(-8, 11)));
  $passes = max(0, (int)round($touches * match ($role) {
    'GK' => 0.46,
    'ST' => 0.34,
    'CB', 'LB', 'RB' => 0.58,
    default => 0.66,
  } * clamp_float(($passing + $composure) / 135, 0.50, 1.22)));
  $shots = max(0, (int)round(match ($role) {
    'ST' => 1.3 + $gf * 0.34,
    'LM', 'RM', 'CM' => 0.45 + $gf * 0.16,
    'CB', 'LB', 'RB' => 0.08,
    default => 0.0,
  } * clamp_float(($shooting + $shotControl + $positioning) / 165, 0.25, 1.45) + mt_rand(-1, 1)));
  $tackles = max(0, (int)round(match ($role) {
    'GK' => 0,
    'CB', 'LB', 'RB' => 2.1,
    'CM', 'LM', 'RM' => 1.25,
    default => 0.55,
  } * clamp_float(($tackling + $workrate + $positioning) / 165, 0.35, 1.55) + max(0, $ga - $gf) * 0.25 + mt_rand(-1, 2)));
  $lostBalls = max(0, (int)round((100 - ($passControl * 0.48 + $composure * 0.34 + $passing * 0.18)) / 28 + mt_rand(-1, 2)));
  $fouls = max(0, (int)round((($aggression * 0.62 + (100 - $discipline) * 0.72 + (100 - $tackling) * 0.18) / 100) + mt_rand(-1, 1)));
  $yellowChance = clamp_int((int)round($aggression * 0.18 + (100 - $discipline) * 0.20), 3, 38);
  $yellows = $fouls > 0 && mt_rand(1, 100) <= $yellowChance ? 1 : 0;
  if ($fouls > 1 && $yellows > 0 && mt_rand(1, 100) <= clamp_int((int)round($yellowChance * 0.28 + max(0, $fouls - 1) * 5), 1, 18)) $yellows = 2;
  $directRed = $fouls > 0 && mt_rand(1, 100) <= clamp_int((int)round($aggression * 0.035 + (100 - $discipline) * 0.045 + max(0, $fouls - 1) * 2), 0, 8);
  if ($directRed) $yellows = 0;
  $secondYellow = !$directRed && $yellows >= 2;
  $reds = ($directRed || $secondYellow) ? 1 : 0;
  $complaints = mt_rand(1, 100) <= clamp_int((int)round($aggression * 0.24 + (100 - $discipline) * 0.18 + max(0, $ga - $gf) * 7), 3, 46) ? 1 : 0;
  $slackSeconds = max(0.0, round((100 - $workrate) / 15 + mt_rand(-2, 5) / 2, 1));
  $saves = $role === 'GK' ? max(0, (int)round(($keeping / 32) + max(0, $ga - $gf) * 0.30 + mt_rand(-1, 2))) : 0;
  $crits = $shots > 0 && mt_rand(1, 100) <= player_stat($player, 'crit') ? 1 : 0;
  $possession = round(($touches * 0.75 + $passes * 0.42 + mt_rand(0, 18) / 10), 1);
  $positive = $touches * 1.1 + $passes * 0.8 + $shots * 1.4 + $tackles * 1.3 + $saves * 1.8 + $crits * 1.6 + $distance / 125.0;
  $negative = $lostBalls * 1.4 + $fouls * 1.5 + $yellows * 2.2 + $reds * 6.0 + $complaints * 0.7 + $slackSeconds / 8.0;
  $row = [
    'id' => (string)($player['id'] ?? ''),
    'name' => (string)($player['name'] ?? 'Player'),
    'role' => $role,
    'side' => $side,
    'teamName' => $teamName,
    'distanceMeters' => $distance,
    'activeSeconds' => 0,
    'idleSeconds' => 0,
    'slackSeconds' => $slackSeconds,
    'possessionSeconds' => $possession,
    'touches' => $touches,
    'passes' => $passes,
    'longPasses' => max(0, (int)round($passes * (in_array($role, ['GK', 'CB', 'LB', 'RB'], true) ? 0.16 : 0.08))),
    'widePasses' => in_array($role, ['LB', 'RB', 'LM', 'RM'], true) ? max(0, (int)round($passes * 0.28)) : max(0, (int)round($passes * 0.05)),
    'shots' => $shots,
    'tackles' => $tackles,
    'fouls' => $fouls,
    'yellows' => $yellows,
    'reds' => $reds,
    'secondYellowReds' => $secondYellow ? 1 : 0,
    'directReds' => $directRed ? 1 : 0,
    'saves' => $saves,
    'crits' => $crits,
    'lostBalls' => $lostBalls,
    'complaints' => $complaints,
    'impact' => round($positive - $negative, 1),
  ];
  $row['cards'] = match_player_cards_from_stat($row);
  $row['yellows'] = (int)($row['cards']['yellow'] ?? $row['yellows']);
  $row['reds'] = (int)($row['cards']['red'] ?? $row['reds']);
  if (!empty($row['cards']['secondYellow'])) $row['secondYellowReds'] = max(1, (int)($row['secondYellowReds'] ?? 0));
  if (!empty($row['cards']['directRed'])) $row['directReds'] = max(1, (int)($row['directReds'] ?? 0));
  $row['behavior'] = manager_match_behavior_label($row);
  $row['terrible'] = (float)$row['impact'] <= -2.0 || (float)$row['slackSeconds'] >= 6.0 || ((int)$row['lostBalls'] >= 4 && (int)$row['touches'] <= 4);
  return $row;
}

function manager_build_simulated_match_report(array $season, array $fixture, int $roundIndex, int $fixtureIndex, array $score, array $homeData, array $awayData): array {
  $homeTeam = $homeData['team'] ?? ['name' => (string)($fixture['home'] ?? 'Home')];
  $awayTeam = $awayData['team'] ?? ['name' => (string)($fixture['away'] ?? 'Away')];
  $players = [];
  foreach (($homeData['squad']['starters'] ?? []) as $player) $players[] = manager_simulated_player_row($player, 'home', (string)($homeTeam['name'] ?? 'Home'), (int)($score[0] ?? 0), (int)($score[1] ?? 0));
  foreach (($awayData['squad']['starters'] ?? []) as $player) $players[] = manager_simulated_player_row($player, 'away', (string)($awayTeam['name'] ?? 'Away'), (int)($score[1] ?? 0), (int)($score[0] ?? 0));
  usort($players, fn($a, $b) => ((float)($b['impact'] ?? 0) <=> (float)($a['impact'] ?? 0)) ?: ((int)($b['distanceMeters'] ?? 0) <=> ((int)($a['distanceMeters'] ?? 0))));
  $sum = function (string $side, string $field) use ($players): int {
    $value = 0;
    foreach ($players as $player) if (($player['side'] ?? '') === $side) $value += (int)($player[$field] ?? 0);
    return $value;
  };
  $possHome = 0.0;
  $possAway = 0.0;
  foreach ($players as $player) {
    if (($player['side'] ?? '') === 'home') $possHome += (float)($player['possessionSeconds'] ?? 0);
    if (($player['side'] ?? '') === 'away') $possAway += (float)($player['possessionSeconds'] ?? 0);
  }
  $slacker = manager_report_slacker($players);
  $moaner = manager_report_top($players, 'complaints');
  $worst = manager_report_top($players, 'impact', true);
  if ($worst !== null && empty($worst['terrible']) && (float)($worst['impact'] ?? 0) > -1.0) $worst = null;
  return [
    'id' => (string)($season['id'] ?? 'season') . ':' . $roundIndex . ':' . $fixtureIndex,
    'seasonId' => (string)($season['id'] ?? ''),
    'roundIndex' => $roundIndex,
    'fixtureIndex' => $fixtureIndex,
    'round' => $fixture['round'] ?? '',
    'phase' => (string)($fixture['phase'] ?? 'league'),
    'playedAt' => time(),
    'simulated' => true,
    'home' => (string)($fixture['home'] ?? ''),
    'away' => (string)($fixture['away'] ?? ''),
    'homeTeam' => $homeTeam,
    'awayTeam' => $awayTeam,
    'score' => [(int)($score[0] ?? 0), (int)($score[1] ?? 0)],
    'teamStats' => [
      'possession' => ['home' => round($possHome, 1), 'away' => round($possAway, 1)],
      'touches' => ['home' => $sum('home', 'touches'), 'away' => $sum('away', 'touches')],
      'shots' => ['home' => $sum('home', 'shots'), 'away' => $sum('away', 'shots')],
      'tackles' => ['home' => $sum('home', 'tackles'), 'away' => $sum('away', 'tackles')],
      'fouls' => ['home' => $sum('home', 'fouls'), 'away' => $sum('away', 'fouls')],
      'yellows' => ['home' => $sum('home', 'yellows'), 'away' => $sum('away', 'yellows')],
      'reds' => ['home' => $sum('home', 'reds'), 'away' => $sum('away', 'reds')],
      'saves' => ['home' => $sum('home', 'saves'), 'away' => $sum('away', 'saves')],
    ],
    'discipline' => [
      'yellows' => ['home' => $sum('home', 'yellows'), 'away' => $sum('away', 'yellows')],
      'reds' => ['home' => $sum('home', 'reds'), 'away' => $sum('away', 'reds')],
      'freeKicks' => ['home' => $sum('away', 'fouls'), 'away' => $sum('home', 'fouls')],
      'penalties' => ['home' => 0, 'away' => 0],
      'complaints' => ['home' => $sum('home', 'complaints'), 'away' => $sum('away', 'complaints')],
    ],
    'highlights' => [
      'mostBall' => manager_report_top($players, 'possessionSeconds'),
      'workhorse' => manager_report_top($players, 'distanceMeters'),
      'slacker' => $slacker,
      'moaner' => ($moaner && (int)($moaner['complaints'] ?? 0) > 0) ? $moaner : null,
      'worst' => $worst,
    ],
    'cardedPlayers' => manager_report_carded_players($players),
    'goalScorers' => manager_simulated_goal_scorer_rows($players, $score, $homeTeam, $awayTeam),
    'players' => array_slice($players, 0, 28),
  ];
}

function manager_store_match_report(array &$season, array $report): void {
  $report = manager_normalize_match_report_cards($report);
  manager_record_match_report_praise($season, $report);
  if (!isset($season['matchReports']) || !is_array($season['matchReports'])) $season['matchReports'] = [];
  $id = (string)($report['id'] ?? '');
  if ($id !== '') {
    $season['matchReports'] = array_values(array_filter($season['matchReports'], fn($old) => !is_array($old) || (string)($old['id'] ?? '') !== $id));
  }
  $season['matchReports'][] = $report;
  if (count($season['matchReports']) > 20) $season['matchReports'] = array_slice($season['matchReports'], -20);
}

function manager_card_lookup_keys(array $player): array {
  $keys = [];
  $id = (string)($player['id'] ?? '');
  $name = (string)($player['name'] ?? '');
  if ($id !== '') $keys[] = 'id:' . $id;
  if ($name !== '') $keys[] = 'name:' . $name;
  return $keys;
}

function manager_card_row_payload(array $row): array {
  $cards = is_array($row['cards'] ?? null)
    ? $row['cards']
    : match_player_card_payload(
      (int)($row['yellows'] ?? 0),
      (int)($row['reds'] ?? 0),
      (int)($row['secondYellowReds'] ?? 0) > 0,
      (int)($row['directReds'] ?? 0) > 0
    );
  $cards['minute'] = (int)($row['minute'] ?? 0);
  return $cards;
}

function manager_cards_from_report_side(array $report, string $side): array {
  $cards = [];
  foreach (array_merge($report['cardedPlayers'] ?? [], $report['players'] ?? []) as $row) {
    if (!is_array($row) || (string)($row['side'] ?? '') !== $side) continue;
    $payload = manager_card_row_payload($row);
    if (!match_card_payload_has_cards($payload)) continue;
    foreach (manager_card_lookup_keys($row) as $key) $cards[$key] = $payload;
  }
  return $cards;
}

function manager_cards_from_match_side(array $match, string $side): array {
  ensure_match_engine($match);
  $cards = [];
  foreach (($match['engine']['playerStats'][$side] ?? []) as $row) {
    if (!is_array($row)) continue;
    $payload = match_player_cards_from_stat($row);
    if (!match_card_payload_has_cards($payload)) continue;
    foreach (manager_card_lookup_keys($row) as $key) $cards[$key] = $payload;
  }
  return $cards;
}

function manager_live_match_cards_for_team(array $state, array $season, string $teamId): array {
  foreach (($season['rounds'] ?? []) as $round) {
    foreach (($round['fixtures'] ?? []) as $fixture) {
      if (($fixture['status'] ?? '') !== 'watching') continue;
      $side = null;
      if ((string)($fixture['home'] ?? '') === $teamId) $side = 'home';
      if ((string)($fixture['away'] ?? '') === $teamId) $side = 'away';
      $matchId = (string)($fixture['matchId'] ?? '');
      if ($side === null || $matchId === '' || empty($state['matches'][$matchId]) || !is_array($state['matches'][$matchId])) continue;
      return manager_cards_from_match_side($state['matches'][$matchId], $side);
    }
  }
  return [];
}

function manager_store_last_match_cards(array &$season, string $home, string $away, array $report): void {
  if (!isset($season['lastMatchCards']) || !is_array($season['lastMatchCards'])) $season['lastMatchCards'] = [];
  $season['lastMatchCards'][$home] = manager_cards_from_report_side($report, 'home');
  $season['lastMatchCards'][$away] = manager_cards_from_report_side($report, 'away');
}

function manager_annotate_squad_cards(array $squad, array $season, string $teamId, ?array $state = null): array {
  $cards = is_array($season['lastMatchCards'][$teamId] ?? null) ? $season['lastMatchCards'][$teamId] : [];
  if ($state !== null) $cards = array_replace($cards, manager_live_match_cards_for_team($state, $season, $teamId));
  foreach (['starters', 'bench'] as $group) {
    if (empty($squad[$group]) || !is_array($squad[$group])) continue;
    foreach ($squad[$group] as &$player) {
      $payload = null;
      foreach (manager_card_lookup_keys($player) as $key) {
        if (isset($cards[$key]) && is_array($cards[$key])) {
          $payload = $cards[$key];
          break;
        }
      }
      if ($payload && match_card_payload_has_cards($payload)) {
        $player['matchCards'] = $payload;
        $player['yellowCards'] = (int)($payload['yellow'] ?? 0);
        $player['redCards'] = (int)($payload['red'] ?? 0);
      } else {
        unset($player['matchCards'], $player['yellowCards'], $player['redCards']);
      }
    }
    unset($player);
  }
  return $squad;
}

function reset_units(array &$match): void {
  foreach (['home', 'away'] as $side) {
    if (empty($match['units'][$side])) continue;
    foreach ($match['units'][$side] as &$player) {
      $player['x'] = $player['homeX'];
      $player['y'] = clamp_side_y($side, (float)$player['homeY']);
      $player['vx'] = 0.0;
      $player['vy'] = 0.0;
    }
    unset($player);
  }
}

function average_rating(array $players): int {
  if (!$players) return 0;
  $sum = 0;
  foreach ($players as $player) {
    ensure_player_stats($player);
    $sum += player_effective_rating($player);
  }
  return (int)round($sum / count($players));
}

function player_skill_points(array $player): int {
  $keys = player_stat_keys(player_natural_role($player) === 'GK');
  $sum = 0;
  foreach ($keys as $key) $sum += player_stat($player, $key);
  return $sum ?: (int)($player['rating'] ?? 0) * 8;
}

function squad_top_players(array $squad, int $limit = 16): array {
  $players = array_values(array_merge($squad['starters'] ?? [], $squad['bench'] ?? []));
  usort($players, fn($a, $b) => player_skill_points($b) <=> player_skill_points($a));
  return array_slice($players, 0, $limit);
}

function squad_skill_points(array $squad, int $limit = 16): int {
  $sum = 0;
  foreach (squad_top_players($squad, $limit) as $player) $sum += player_skill_points($player);
  return $sum;
}

function squad_match_skill_points(array $squad): int {
  $players = array_values($squad['starters'] ?? []);
  if (!$players) $players = array_values($squad['bench'] ?? []);
  $sum = 0;
  foreach ($players as $player) $sum += player_skill_points($player);
  return $sum;
}

function manager_fixture_form_seed(array $season, array $fixture, string $sideId, int $roundIndex = -1, int $fixtureIndex = -1): string {
  return implode('|', [
    (string)($season['id'] ?? ''),
    (string)($season['seasonYear'] ?? ''),
    (string)($fixture['phase'] ?? 'league'),
    (string)($fixture['round'] ?? ''),
    (string)($fixture['cupKey'] ?? ''),
    (string)($fixture['cupStage'] ?? ''),
    (string)($fixture['playoffKey'] ?? ''),
    (string)($fixture['home'] ?? ''),
    (string)($fixture['away'] ?? ''),
    (string)$roundIndex,
    (string)$fixtureIndex,
    $sideId,
  ]);
}

function seeded_index(string $seed, int $count): int {
  if ($count <= 0) return 0;
  return (int)(sprintf('%u', crc32($seed)) % $count);
}

function seeded_player_id(string $seed, array $ids): string {
  $bestId = '';
  $bestScore = null;
  foreach ($ids as $id) {
    $id = (string)$id;
    if ($id === '') continue;
    $score = (int)sprintf('%u', crc32($seed . '|' . $id));
    if ($bestScore === null || $score < $bestScore || ($score === $bestScore && strcmp($id, $bestId) < 0)) {
      $bestScore = $score;
      $bestId = $id;
    }
  }
  return $bestId;
}

function manager_normalize_daily_form(array $form): array {
  $boosts = [];
  foreach (($form['boosts'] ?? []) as $id) {
    $id = (string)$id;
    if ($id !== '' && !isset($boosts[$id])) $boosts[$id] = $id;
  }
  $legacyBoost = (string)($form['boost'] ?? '');
  if ($legacyBoost !== '' && !isset($boosts[$legacyBoost])) $boosts[$legacyBoost] = $legacyBoost;
  $boostValues = array_values($boosts);
  return [
    'boost' => $boostValues[0] ?? '',
    'boosts' => $boostValues,
    'drop' => (string)($form['drop'] ?? ''),
  ];
}

function manager_daily_form_payload(array $boosts, string $drop): array {
  $boostMap = [];
  foreach ($boosts as $id) {
    $id = (string)$id;
    if ($id !== '' && !isset($boostMap[$id])) $boostMap[$id] = $id;
  }
  $boostValues = array_values($boostMap);
  return [
    'boost' => $boostValues[0] ?? '',
    'boosts' => $boostValues,
    'drop' => $drop,
  ];
}

function manager_daily_form_candidates(array $squad): array {
  foreach (['starters', 'bench'] as $group) {
    $ids = [];
    foreach (($squad[$group] ?? []) as $player) {
      $id = (string)($player['id'] ?? '');
      if ($id !== '' && !isset($ids[$id])) $ids[$id] = $id;
    }
    if ($ids) return array_values($ids);
  }
  return [];
}

function manager_daily_form_all_ids(array $squad): array {
  $ids = [];
  foreach (['starters', 'bench'] as $group) {
    foreach (($squad[$group] ?? []) as $player) {
      $id = (string)($player['id'] ?? '');
      if ($id !== '' && !isset($ids[$id])) $ids[$id] = $id;
    }
  }
  return array_values($ids);
}

function manager_daily_form_for_fixture(array $season, array $fixture, string $sideId, array $squad, int $roundIndex = -1, int $fixtureIndex = -1): array {
  if (($fixture['status'] ?? 'pending') === 'played') return [];
  $key = manager_daily_form_key($fixture, $sideId, $roundIndex, $fixtureIndex);
  $stored = $season['dailyForms'][$key] ?? null;
  $ids = manager_daily_form_candidates($squad);
  $allIds = manager_daily_form_all_ids($squad);
  $allLookup = array_flip(array_map('strval', $allIds));
  $formTop = (string)(manager_league_form_state($season, $sideId)['trend'] ?? '') === 'up';
  $seed = manager_fixture_form_seed($season, $fixture, $sideId, $roundIndex, $fixtureIndex);
  if (is_array($stored)) {
    $form = manager_normalize_daily_form($stored);
    if ($formTop) {
      $boosts = array_values(array_filter($form['boosts'], fn($id) => isset($allLookup[(string)$id])));
      $boostLookup = array_flip(array_map('strval', $boosts));
      $boostPool = array_values(array_filter($ids ?: $allIds, fn($id) => !isset($boostLookup[(string)$id])));
      if (count($boosts) < 2 && $boostPool) $boosts[] = seeded_player_id($seed . '|boost2', $boostPool);
      if ($boosts) return manager_daily_form_payload(array_slice($boosts, 0, 2), '');
    } else {
      $boosts = array_values(array_filter($form['boosts'] ?? [], fn($id) => isset($allLookup[(string)$id])));
      $boost = (string)($boosts[0] ?? '');
      if ($boost === '') $boost = seeded_player_id($seed . '|boost', $ids ?: $allIds);
      $drop = (string)($form['drop'] ?? '');
      $dropValid = $drop !== '' && isset($allLookup[$drop]) && $drop !== $boost;
      $dropPool = array_values(array_filter($ids ?: $allIds, fn($id) => (string)$id !== $boost));
      if (!$dropValid) $drop = $dropPool ? seeded_player_id($seed . '|drop-repair', $dropPool) : '';
      if ($boost !== '' || $drop !== '') return manager_daily_form_payload(array_filter([$boost], fn($id) => (string)$id !== ''), $drop);
    }
  }
  if (!$ids) return [];
  $boost = seeded_player_id($seed . '|boost', $ids);
  $dropPool = array_values(array_filter($ids, fn($id) => (string)$id !== (string)$boost));
  if ($formTop) {
    $boost2 = $dropPool ? seeded_player_id($seed . '|boost2', $dropPool) : '';
    return manager_daily_form_payload(array_filter([$boost, $boost2], fn($id) => (string)$id !== ''), '');
  }
  $drop = '';
  if ($dropPool) $drop = seeded_player_id($seed . '|drop', $dropPool);
  return manager_daily_form_payload([$boost], $drop);
}

function manager_daily_form_key(array $fixture, string $sideId, int $roundIndex = -1, int $fixtureIndex = -1): string {
  return implode(':', [
    max(-1, $roundIndex),
    max(-1, $fixtureIndex),
    preg_replace('/[^a-zA-Z0-9_.-]+/', '_', (string)($fixture['phase'] ?? 'league')),
    preg_replace('/[^a-zA-Z0-9_.-]+/', '_', $sideId),
  ]);
}

function manager_daily_form_side_key(string $sideId): string {
  return preg_replace('/[^a-zA-Z0-9_.-]+/', '_', $sideId);
}

function manager_clear_future_daily_forms_for_side(array &$season, string $sideId, int $roundIndex, int $fixtureIndex): int {
  if ($sideId === '' || empty($season['dailyForms']) || !is_array($season['dailyForms'])) return 0;
  $sideKey = manager_daily_form_side_key($sideId);
  $removed = 0;
  foreach (array_keys($season['dailyForms']) as $key) {
    $parts = explode(':', (string)$key, 4);
    if (count($parts) < 4 || $parts[3] !== $sideKey) continue;
    $storedRound = (int)$parts[0];
    $storedFixture = (int)$parts[1];
    if ($storedRound < $roundIndex || ($storedRound === $roundIndex && $storedFixture <= $fixtureIndex)) continue;
    unset($season['dailyForms'][$key]);
    $removed++;
  }
  return $removed;
}

function manager_lock_fixture_form_for_side(array &$season, array $fixture, string $sideId, array $sideData, int $roundIndex = -1, int $fixtureIndex = -1): void {
  if (($fixture['status'] ?? 'pending') === 'played') return;
  $key = manager_daily_form_key($fixture, $sideId, $roundIndex, $fixtureIndex);
  if (!isset($season['dailyForms']) || !is_array($season['dailyForms'])) $season['dailyForms'] = [];
  if (isset($season['dailyForms'][$key]) && is_array($season['dailyForms'][$key])) return;
  $form = manager_daily_form_for_fixture($season, $fixture, $sideId, $sideData['squad'] ?? [], $roundIndex, $fixtureIndex);
  if (!$form) return;
  $season['dailyForms'][$key] = [
    'boost' => (string)($form['boost'] ?? ''),
    'boosts' => array_values(array_map('strval', $form['boosts'] ?? array_filter([(string)($form['boost'] ?? '')]))),
    'drop' => (string)($form['drop'] ?? ''),
    'lockedAt' => time(),
  ];
}

function manager_lock_next_form_for_side(array &$season, string $sideId, array $sideData): void {
  $next = manager_next_fixture_for_side($season, $sideId);
  if (!$next) return;
  manager_lock_fixture_form_for_side($season, $next['fixture'], $sideId, $sideData, (int)$next['roundIndex'], (int)$next['fixtureIndex']);
}

function apply_daily_form_to_player(array $player, array $form): array {
  $form = manager_normalize_daily_form($form);
  $id = (string)($player['id'] ?? '');
  if ($id !== '' && in_array($id, $form['boosts'] ?? [], true)) {
    $player['dailyForm'] = 'up';
    $player['dailyFormPct'] = 10;
    $player['dailyFormMultiplier'] = 1.10;
  } elseif ($id !== '' && (string)($form['drop'] ?? '') === $id) {
    $player['dailyForm'] = 'down';
    $player['dailyFormPct'] = -10;
    $player['dailyFormMultiplier'] = 0.90;
  } else {
    unset($player['dailyForm'], $player['dailyFormPct'], $player['dailyFormMultiplier']);
  }
  return $player;
}

function apply_daily_form_to_squad(array $squad, array $form): array {
  if (!$form) return $squad;
  foreach (['starters', 'bench'] as $group) {
    if (empty($squad[$group]) || !is_array($squad[$group])) continue;
    foreach ($squad[$group] as &$player) $player = apply_daily_form_to_player($player, $form);
    unset($player);
  }
  return $squad;
}

function public_daily_form_summary(array $squad, array $form): array {
  $form = manager_normalize_daily_form($form);
  $out = ['boost' => null, 'boosts' => [], 'drop' => null];
  if (!$form) return $out;
  foreach (['starters', 'bench'] as $group) {
    foreach (($squad[$group] ?? []) as $player) {
      $id = (string)($player['id'] ?? '');
      if ($id === '') continue;
      $row = [
        'id' => $id,
        'name' => (string)($player['name'] ?? 'Player'),
        'group' => strtoupper($group === 'starters' ? 'STARTING' : 'BENCH'),
      ];
      if (in_array($id, $form['boosts'] ?? [], true)) {
        $boostRow = $row + ['pct' => 10];
        if (!$out['boost']) $out['boost'] = $boostRow;
        $out['boosts'][] = $boostRow;
      }
      if ((string)($form['drop'] ?? '') === $id) $out['drop'] = $row + ['pct' => -10];
    }
  }
  return $out;
}

function manager_apply_fixture_form_to_side_data(array $season, array $fixture, string $sideId, array $sideData, int $roundIndex = -1, int $fixtureIndex = -1): array {
  $form = manager_daily_form_for_fixture($season, $fixture, $sideId, $sideData['squad'] ?? [], $roundIndex, $fixtureIndex);
  if (!$form) return $sideData;
  $sideData['dailyForm'] = $form;
  $sideData['squad'] = apply_daily_form_to_squad($sideData['squad'] ?? ['starters' => [], 'bench' => []], $form);
  $sideData['matchStrength'] = squad_match_skill_points($sideData['squad']);
  return $sideData;
}

function manager_next_fixture_for_side(array $season, string $sideId): ?array {
  foreach (($season['rounds'] ?? []) as $roundIndex => $round) {
    foreach (($round['fixtures'] ?? []) as $fixtureIndex => $fixture) {
      if (!in_array(($fixture['status'] ?? ''), ['pending', 'watching'], true)) continue;
      if (($fixture['home'] ?? '') === $sideId || ($fixture['away'] ?? '') === $sideId) {
        return ['roundIndex' => (int)$roundIndex, 'fixtureIndex' => (int)$fixtureIndex, 'fixture' => $fixture];
      }
    }
  }
  return null;
}

function manager_apply_next_form_to_side_data(array $season, string $sideId, array $sideData): array {
  $next = manager_next_fixture_for_side($season, $sideId);
  if (!$next) return $sideData;
  return manager_apply_fixture_form_to_side_data($season, $next['fixture'], $sideId, $sideData, (int)$next['roundIndex'], (int)$next['fixtureIndex']);
}

function manager_injury_reasons(): array {
  return [
    'Slipped in the showers after training',
    'Took a warm-up ball straight in the face',
    'Pulled a hamstring celebrating a training goal',
  ];
}

function manager_league_round_indices(array $season, bool $pendingOnly = false): array {
  $indices = [];
  foreach (($season['rounds'] ?? []) as $roundIndex => $round) {
    $hasLeague = false;
    $hasPending = false;
    foreach (($round['fixtures'] ?? []) as $fixture) {
      if (($fixture['phase'] ?? 'league') !== 'league') continue;
      $hasLeague = true;
      if (($fixture['status'] ?? '') === 'pending') $hasPending = true;
    }
    if ($hasLeague && (!$pendingOnly || $hasPending)) $indices[] = (int)$roundIndex;
  }
  return $indices;
}

function manager_ensure_injury_plan(array &$season): void {
  if ((int)($season['injuryVersion'] ?? 0) >= 1) return;
  if (!in_array(($season['status'] ?? ''), ['active', 'playoff'], true)) return;
  $roundIndices = manager_league_round_indices($season, true);
  $season['injuryVersion'] = 1;
  $season['injuryPlan'] = [];
  $season['injuries'] = is_array($season['injuries'] ?? null) ? $season['injuries'] : [];
  if (!$roundIndices) return;
  $reasons = manager_injury_reasons();
  foreach (($season['humanManagers'] ?? []) as $user) {
    $user = clean_name((string)$user);
    if ($user === '') continue;
    $max = min(5, count($roundIndices));
    $count = mt_rand(0, $max);
    if ($count <= 0) {
      $season['injuryPlan'][$user] = [];
      continue;
    }
    $picked = [];
    $pool = $roundIndices;
    while (count($picked) < $count && $pool) {
      $idx = mt_rand(0, count($pool) - 1);
      $picked[] = (int)$pool[$idx];
      array_splice($pool, $idx, 1);
    }
    sort($picked);
    foreach ($picked as $n => $roundIndex) {
      $season['injuryPlan'][$user][] = [
        'id' => 'inj' . substr(sha1((string)($season['id'] ?? '') . '|' . $user . '|' . $roundIndex . '|' . $n), 0, 10),
        'roundIndex' => $roundIndex,
        'duration' => mt_rand(1, 3),
        'reason' => $reasons[mt_rand(0, count($reasons) - 1)],
        'status' => 'pending',
      ];
    }
  }
}

function manager_active_injuries(array $season, string $user): array {
  $out = [];
  foreach (($season['injuries'][$user] ?? []) as $playerId => $injury) {
    if (!is_array($injury) || (int)($injury['remaining'] ?? 0) <= 0) continue;
    $injury['playerId'] = (string)($injury['playerId'] ?? $playerId);
    $out[(string)$injury['playerId']] = $injury;
  }
  return $out;
}

function manager_training_stat_labels(): array {
  return [
    'shooting' => 'SHT',
    'passing' => 'PAS',
    'tackling' => 'TAC',
    'heading' => 'HEA',
    'passcontrol' => 'PCL',
    'shotcontrol' => 'SCL',
    'speed' => 'SPD',
    'crit' => 'CRT',
    'stamina' => 'STA',
    'strength' => 'STR',
    'positioning' => 'POS',
    'vision' => 'VIS',
    'composure' => 'CMP',
    'aggression' => 'AGR',
    'discipline' => 'DIS',
    'crossing' => 'CRS',
    'dribbling' => 'DRB',
    'workrate' => 'WRK',
    'keeping' => 'KEEP',
  ];
}

function manager_training_stat_label(string $stat): string {
  $labels = manager_training_stat_labels();
  return $labels[$stat] ?? strtoupper(substr($stat, 0, 3));
}

function manager_training_role_stats(array $player): array {
  return match (player_natural_role($player)) {
    'GK' => ['keeping', 'positioning', 'composure'],
    'CB' => ['tackling', 'heading', 'strength', 'positioning'],
    'LB', 'RB' => ['speed', 'tackling', 'crossing', 'stamina'],
    'LM', 'RM' => ['crossing', 'speed', 'dribbling', 'passing'],
    'ST' => ['shooting', 'shotcontrol', 'positioning', 'composure'],
    default => ['passing', 'vision', 'passcontrol', 'workrate'],
  };
}

function manager_training_primary_stat(array $player): string {
  ensure_player_stats($player);
  $stats = is_array($player['stats'] ?? null) ? $player['stats'] : [];
  foreach (manager_training_role_stats($player) as $stat) {
    if (array_key_exists($stat, $stats) && (int)$stats[$stat] < 100) return $stat;
  }
  foreach (manager_training_role_stats($player) as $stat) {
    if (array_key_exists($stat, $stats)) return $stat;
  }
  $bestStat = '';
  $bestValue = PHP_INT_MIN;
  foreach ($stats as $stat => $value) {
    if (!is_numeric($value)) continue;
    if ((int)$value > $bestValue) {
      $bestValue = (int)$value;
      $bestStat = (string)$stat;
    }
  }
  return $bestStat !== '' ? $bestStat : 'passing';
}

function manager_active_training(array $season, string $user): array {
  $out = [];
  foreach (($season['training'][$user] ?? []) as $playerId => $training) {
    if (!is_array($training) || (int)($training['remaining'] ?? 0) <= 0) continue;
    $training['playerId'] = (string)($training['playerId'] ?? $playerId);
    $training['stat'] = (string)($training['stat'] ?? 'passing');
    $training['statLabel'] = (string)($training['statLabel'] ?? manager_training_stat_label($training['stat']));
    $training['gain'] = (int)($training['gain'] ?? 2);
    $out[(string)$training['playerId']] = $training;
  }
  return $out;
}

function manager_prune_invalid_training(array &$state, array &$season, string $user, int $max = MANAGER_MAX_TRAINING_PLAYERS): array {
  $user = clean_name($user);
  if ($user === '' || empty($season['training'][$user]) || !is_array($season['training'][$user])) return [];
  $removed = [];
  if (!empty($state['users'][$user]) && is_array($state['users'][$user])) {
    ensure_user_squad($state['users'][$user]);
    foreach ($season['training'][$user] as $key => $training) {
      if (!is_array($training) || (int)($training['remaining'] ?? 0) <= 0) continue;
      $playerId = (string)($training['playerId'] ?? $key);
      if ($playerId !== '' && find_squad_player($state['users'][$user]['squad'] ?? [], $playerId)) continue;
      $removed[] = [
        'key' => (string)$key,
        'playerId' => $playerId,
        'playerName' => (string)($training['playerName'] ?? 'Player'),
        'reason' => 'missing_from_squad',
      ];
      unset($season['training'][$user][$key]);
    }
  }
  return array_merge($removed, manager_prune_active_training_limit($season, $user, $max));
}

function manager_prune_active_training_limit(array &$season, string $user, int $max = MANAGER_MAX_TRAINING_PLAYERS): array {
  $user = clean_name($user);
  $max = max(0, $max);
  if ($user === '' || empty($season['training'][$user]) || !is_array($season['training'][$user])) return [];
  $entries = [];
  foreach ($season['training'][$user] as $key => $training) {
    if (!is_array($training) || (int)($training['remaining'] ?? 0) <= 0) continue;
    $entries[] = [
      'key' => (string)$key,
      'playerId' => (string)($training['playerId'] ?? $key),
      'playerName' => (string)($training['playerName'] ?? 'Player'),
      'startedAt' => (int)($training['startedAt'] ?? 0),
      'id' => (string)($training['id'] ?? ''),
    ];
  }
  if (count($entries) <= $max) return [];
  usort($entries, fn($a, $b) => [$a['startedAt'], $a['id'], $a['playerId']] <=> [$b['startedAt'], $b['id'], $b['playerId']]);
  $removed = array_slice($entries, $max);
  foreach ($removed as $entry) {
    unset($season['training'][$user][$entry['key']]);
  }
  return $removed;
}

function manager_unavailable_player_ids(array $season, string $user): array {
  return array_values(array_unique(array_merge(
    array_keys(manager_active_injuries($season, $user)),
    array_keys(manager_active_training($season, $user))
  )));
}

function manager_injury_player_candidates(array $squad, array $active): array {
  $blocked = array_fill_keys(array_map('strval', array_keys($active)), true);
  $groups = ['starters', 'bench'];
  foreach ($groups as $group) {
    $players = [];
    foreach (($squad[$group] ?? []) as $player) {
      $id = (string)($player['id'] ?? '');
      if ($id === '' || isset($blocked[$id])) continue;
      $players[] = $player;
    }
    if ($players) return $players;
  }
  return [];
}

function manager_trigger_due_injuries(array &$state, array &$season, int $roundIndex, int $now): void {
  manager_ensure_injury_plan($season);
  foreach (array_keys($season['injuryPlan'] ?? []) as $user) {
    if (empty($season['injuryPlan'][$user]) || !is_array($season['injuryPlan'][$user])) continue;
    foreach ($season['injuryPlan'][$user] as &$event) {
      if (($event['status'] ?? 'pending') !== 'pending') continue;
      if ((int)($event['roundIndex'] ?? 999999) > $roundIndex) continue;
      if (empty($state['users'][$user]) || !is_array($state['users'][$user])) {
        $event['status'] = 'skipped';
        continue;
      }
      ensure_user_squad($state['users'][$user]);
      $active = array_replace(manager_active_injuries($season, (string)$user), manager_active_training($season, (string)$user));
      $candidates = manager_injury_player_candidates($state['users'][$user]['squad'] ?? [], $active);
      if (!$candidates) {
        $event['status'] = 'skipped';
        continue;
      }
      $player = $candidates[mt_rand(0, count($candidates) - 1)];
      $playerId = (string)($player['id'] ?? '');
      if ($playerId === '') {
        $event['status'] = 'skipped';
        continue;
      }
      if (!isset($season['injuries']) || !is_array($season['injuries'])) $season['injuries'] = [];
      if (!isset($season['injuries'][$user]) || !is_array($season['injuries'][$user])) $season['injuries'][$user] = [];
      $injury = [
        'id' => (string)($event['id'] ?? ('inj' . bin2hex(random_bytes(3)))),
        'playerId' => $playerId,
        'playerName' => (string)($player['name'] ?? 'Player'),
        'reason' => (string)($event['reason'] ?? manager_injury_reasons()[0]),
        'remaining' => clamp_int((int)($event['duration'] ?? 1), 1, 3),
        'duration' => clamp_int((int)($event['duration'] ?? 1), 1, 3),
        'startedRound' => $roundIndex,
        'createdAt' => $now,
      ];
      $season['injuries'][$user][$playerId] = $injury;
      $event['status'] = 'active';
      $event['playerId'] = $playerId;
      $event['playerName'] = $injury['playerName'];
      manager_log($state, 'manager_injury', ['seasonId' => (string)($season['id'] ?? ''), 'user' => (string)$user, 'player' => $injury['playerName'], 'reason' => $injury['reason'], 'matches' => $injury['remaining']]);
    }
    unset($event);
  }
}

function manager_prepare_injuries_for_current_round(array &$state, array &$season, int $now): void {
  if (!in_array(($season['status'] ?? ''), ['active', 'playoff'], true)) return;
  $roundIndex = manager_pending_round_index($season);
  if ($roundIndex === null) return;
  manager_trigger_due_injuries($state, $season, $roundIndex, $now);
  manager_staff_auto_lineups_for_round($state, $season, $roundIndex, $now);
  foreach (array_values($season['humanManagers'] ?? []) as $user) {
    manager_scout_auto_train_bench($state, $season, (string)$user, $now);
  }
  manager_scout_auto_training_lineups_for_round($state, $season, $roundIndex, $now);
}

function manager_annotate_squad_injuries(array $squad, array $season, string $user): array {
  $active = manager_active_injuries($season, $user);
  if (!$active) return $squad;
  foreach (['starters', 'bench'] as $group) {
    if (empty($squad[$group]) || !is_array($squad[$group])) continue;
    foreach ($squad[$group] as &$player) {
      $id = (string)($player['id'] ?? '');
      if ($id === '' || empty($active[$id])) continue;
      $player['injured'] = true;
      $player['injury'] = [
        'reason' => (string)($active[$id]['reason'] ?? 'Injured'),
        'remaining' => (int)($active[$id]['remaining'] ?? 0),
        'duration' => (int)($active[$id]['duration'] ?? 0),
      ];
    }
    unset($player);
  }
  return $squad;
}

function manager_annotate_squad_training(array $squad, array $season, string $user): array {
  $active = manager_active_training($season, $user);
  if (!$active) return $squad;
  foreach (['starters', 'bench'] as $group) {
    if (empty($squad[$group]) || !is_array($squad[$group])) continue;
    foreach ($squad[$group] as &$player) {
      $id = (string)($player['id'] ?? '');
      if ($id === '' || empty($active[$id])) continue;
      $training = $active[$id];
      $player['trainingAway'] = true;
      $player['training'] = [
        'stat' => (string)($training['stat'] ?? 'passing'),
        'statLabel' => (string)($training['statLabel'] ?? manager_training_stat_label((string)($training['stat'] ?? 'passing'))),
        'gain' => (int)($training['gain'] ?? 2),
        'remaining' => (int)($training['remaining'] ?? 0),
        'duration' => (int)($training['duration'] ?? 5),
      ];
    }
    unset($player);
  }
  return $squad;
}

function manager_injured_starters(array $state, array $season, string $user): array {
  if (empty($state['users'][$user]) || !is_array($state['users'][$user])) return [];
  $active = manager_active_injuries($season, $user);
  $training = manager_active_training($season, $user);
  if (!$active && !$training) return [];
  $out = [];
  foreach (($state['users'][$user]['squad']['starters'] ?? []) as $player) {
    $id = (string)($player['id'] ?? '');
    if ($id === '') continue;
    if (!empty($active[$id])) {
      $injury = $active[$id];
      $out[] = [
        'playerId' => $id,
        'playerName' => (string)($player['name'] ?? $injury['playerName'] ?? 'Player'),
        'reason' => (string)($injury['reason'] ?? 'Injured'),
        'remaining' => (int)($injury['remaining'] ?? 0),
        'type' => 'injury',
      ];
      continue;
    }
    if (empty($training[$id])) continue;
    $session = $training[$id];
    $statLabel = (string)($session['statLabel'] ?? manager_training_stat_label((string)($session['stat'] ?? 'passing')));
    $gain = (int)($session['gain'] ?? 2);
    $out[] = [
      'playerId' => $id,
      'playerName' => (string)($player['name'] ?? $session['playerName'] ?? 'Player'),
      'reason' => 'Training ' . $statLabel . ' +' . $gain,
      'remaining' => (int)($session['remaining'] ?? 0),
      'type' => 'training',
      'stat' => (string)($session['stat'] ?? 'passing'),
      'statLabel' => $statLabel,
      'gain' => $gain,
    ];
  }
  return $out;
}

function manager_user_has_fixture_in_round(array $season, string $user, int $roundIndex): bool {
  foreach (($season['rounds'][$roundIndex]['fixtures'] ?? []) as $fixture) {
    if (!in_array(($fixture['status'] ?? ''), ['pending', 'watching'], true)) continue;
    if (($fixture['home'] ?? '') === $user || ($fixture['away'] ?? '') === $user) return true;
  }
  return false;
}

function public_manager_injury_rows(array $rows): array {
  return array_values(array_map(fn($row) => [
    'playerId' => (string)($row['playerId'] ?? ''),
    'playerName' => (string)($row['playerName'] ?? 'Player'),
    'reason' => (string)($row['reason'] ?? 'Injured'),
    'remaining' => (int)($row['remaining'] ?? 0),
    'type' => (string)($row['type'] ?? 'injury'),
    'stat' => (string)($row['stat'] ?? ''),
    'statLabel' => (string)($row['statLabel'] ?? ''),
    'gain' => (int)($row['gain'] ?? 0),
  ], $rows));
}

function public_manager_injury_report(array $state, array $season, string $viewer): array {
  $active = [];
  foreach (manager_active_injuries($season, $viewer) as $injury) {
    $active[] = [
      'playerId' => (string)($injury['playerId'] ?? ''),
      'playerName' => (string)($injury['playerName'] ?? 'Player'),
      'reason' => (string)($injury['reason'] ?? 'Injured'),
      'remaining' => (int)($injury['remaining'] ?? 0),
      'type' => 'injury',
    ];
  }
  foreach (manager_active_training($season, $viewer) as $training) {
    $statLabel = (string)($training['statLabel'] ?? manager_training_stat_label((string)($training['stat'] ?? 'passing')));
    $gain = (int)($training['gain'] ?? 2);
    $active[] = [
      'playerId' => (string)($training['playerId'] ?? ''),
      'playerName' => (string)($training['playerName'] ?? 'Player'),
      'reason' => 'Training ' . $statLabel . ' +' . $gain,
      'remaining' => (int)($training['remaining'] ?? 0),
      'type' => 'training',
      'stat' => (string)($training['stat'] ?? 'passing'),
      'statLabel' => $statLabel,
      'gain' => $gain,
    ];
  }
  $blocked = manager_injured_starters($state, $season, $viewer);
  return [
    'active' => public_manager_injury_rows($active),
    'blockedStarters' => public_manager_injury_rows($blocked),
    'blocked' => count($blocked) > 0,
  ];
}

function public_manager_injury_blocks(array $state, array $season, int $roundIndex): array {
  $out = [];
  foreach (($season['humanManagers'] ?? []) as $user) {
    $user = clean_name((string)$user);
    if ($user === '' || !manager_user_has_fixture_in_round($season, $user, $roundIndex)) continue;
    $blocked = manager_injured_starters($state, $season, $user);
    if (!$blocked) continue;
    $team = isset($state['users'][$user]) && has_team($state['users'][$user]) ? user_team($state['users'][$user]) : ['name' => strtoupper($user)];
    $out[] = [
      'user' => $user,
      'team' => $team,
      'players' => public_manager_injury_rows($blocked),
    ];
  }
  return $out;
}

function manager_injury_block_payload(array $state, array $season, string $user, int $roundIndex): array {
  return [
    'injuryBlocked' => true,
    'injuryBlocks' => public_manager_injury_blocks($state, $season, $roundIndex),
    'managerSeason' => public_manager_season($season, $state, $user),
    'managerFinance' => isset($state['users'][$user]) ? public_finance($state['users'][$user]) : null,
    'managerSquad' => isset($state['users'][$user]) ? public_manager_context_squad($state, $user, $season) : null,
  ];
}

function manager_decrement_injuries_for_team(array &$season, string $user): void {
  if (empty($season['injuries'][$user]) || !is_array($season['injuries'][$user])) return;
  foreach ($season['injuries'][$user] as $playerId => &$injury) {
    $left = max(0, (int)($injury['remaining'] ?? 0) - 1);
    $injury['remaining'] = $left;
    if ($left <= 0) {
      if (!isset($season['injuryHistory']) || !is_array($season['injuryHistory'])) $season['injuryHistory'] = [];
      $injury['healedAt'] = time();
      $season['injuryHistory'][] = $injury;
    }
  }
  unset($injury);
  foreach (array_keys($season['injuries'][$user]) as $playerId) {
    if ((int)($season['injuries'][$user][$playerId]['remaining'] ?? 0) <= 0) unset($season['injuries'][$user][$playerId]);
  }
}

function manager_apply_training_gain_to_player(array &$player, string $stat, int $gain): array {
  ensure_player_stats($player);
  if (!isset($player['stats']) || !is_array($player['stats'])) $player['stats'] = [];
  if (!array_key_exists($stat, $player['stats'])) $stat = manager_training_primary_stat($player);
  $before = clamp_int((int)($player['stats'][$stat] ?? player_stat($player, $stat)), 0, 100);
  $after = clamp_int($before + max(0, $gain), 0, 100);
  $player['stats'][$stat] = $after;
  $player['rating'] = average_stats($player['stats']);
  unset($player['baseRating'], $player['effectiveRating'], $player['matchStatMultiplier'], $player['hiredManagerBoostPct']);
  return ['stat' => $stat, 'before' => $before, 'after' => $after];
}

function manager_complete_player_training(array &$state, string $user, array $training): array {
  if (empty($state['users'][$user]) || !is_array($state['users'][$user])) return ['applied' => false];
  ensure_user_squad($state['users'][$user]);
  $playerId = (string)($training['playerId'] ?? '');
  if ($playerId === '') return ['applied' => false];
  $found = find_squad_player($state['users'][$user]['squad'] ?? [], $playerId);
  if (!$found) return ['applied' => false];
  $group = (string)$found['group'];
  $index = (int)$found['index'];
  $stat = (string)($training['stat'] ?? 'passing');
  $gain = (int)($training['gain'] ?? 2);
  $player =& $state['users'][$user]['squad'][$group][$index];
  $result = manager_apply_training_gain_to_player($player, $stat, $gain);
  $playerName = (string)($player['name'] ?? $training['playerName'] ?? 'Player');
  unset($player);
  ensure_squad_stats($state['users'][$user]['squad']);
  manager_sync_emergency_loan_bench($state['users'][$user]);
  return $result + ['applied' => true, 'playerName' => $playerName];
}

function manager_decrement_training_for_team(array &$state, array &$season, string $user): void {
  if (empty($season['training'][$user]) || !is_array($season['training'][$user])) return;
  foreach ($season['training'][$user] as $playerId => &$training) {
    $left = max(0, (int)($training['remaining'] ?? 0) - 1);
    $training['remaining'] = $left;
    if ($left > 0) continue;
    $completed = manager_complete_player_training($state, $user, $training);
    if (!isset($season['trainingHistory']) || !is_array($season['trainingHistory'])) $season['trainingHistory'] = [];
    $training['completedAt'] = time();
    $training['applied'] = !empty($completed['applied']);
    $training['before'] = (int)($completed['before'] ?? 0);
    $training['after'] = (int)($completed['after'] ?? 0);
    $training['playerName'] = (string)($completed['playerName'] ?? $training['playerName'] ?? 'Player');
    $season['trainingHistory'][] = $training;
    if (!empty($completed['applied'])) {
      manager_log($state, 'player_training_complete', [
        'seasonId' => (string)($season['id'] ?? ''),
        'user' => $user,
        'player' => (string)$training['playerName'],
        'playerId' => (string)($training['playerId'] ?? $playerId),
        'stat' => (string)($completed['stat'] ?? $training['stat'] ?? ''),
        'before' => (int)($completed['before'] ?? 0),
        'after' => (int)($completed['after'] ?? 0),
      ]);
    }
  }
  unset($training);
  foreach (array_keys($season['training'][$user]) as $playerId) {
    if ((int)($season['training'][$user][$playerId]['remaining'] ?? 0) <= 0) unset($season['training'][$user][$playerId]);
  }
}

function manager_start_player_training(array &$state, array &$season, string $user, string $playerId, int $now): array {
  $user = clean_name($user);
  $playerId = trim($playerId);
  if ($user === '' || $playerId === '' || empty($state['users'][$user]) || !is_array($state['users'][$user])) return ['ok' => false, 'error' => 'bad_player'];
  if (manager_participant($season, $user) === null) return ['ok' => false, 'error' => 'not_your_season'];
  ensure_user_squad($state['users'][$user]);
  $found = find_squad_player($state['users'][$user]['squad'] ?? [], $playerId);
  if (!$found) return ['ok' => false, 'error' => 'bad_player'];
  if ((string)$found['group'] !== 'bench') return ['ok' => false, 'error' => 'not_bench'];
  $player = $found['player'];
  if (manager_is_emergency_loan_player($player)) return ['ok' => false, 'error' => 'loan_player'];
  if (!empty(manager_active_injuries($season, $user)[$playerId])) return ['ok' => false, 'error' => 'injured'];
  $activeTraining = manager_active_training($season, $user);
  if (!empty($activeTraining[$playerId])) return ['ok' => false, 'error' => 'already_training'];
  manager_prune_invalid_training($state, $season, $user);
  $activeTraining = manager_active_training($season, $user);
  if (count($activeTraining) >= MANAGER_MAX_TRAINING_PLAYERS) {
    return ['ok' => false, 'error' => 'training_limit', 'max' => MANAGER_MAX_TRAINING_PLAYERS];
  }
  $stat = manager_training_primary_stat($player);
  $gain = 2;
  if (!isset($season['training']) || !is_array($season['training'])) $season['training'] = [];
  if (!isset($season['training'][$user]) || !is_array($season['training'][$user])) $season['training'][$user] = [];
  $training = [
    'id' => 'trn' . bin2hex(random_bytes(5)),
    'playerId' => $playerId,
    'playerName' => (string)($player['name'] ?? 'Player'),
    'stat' => $stat,
    'statLabel' => manager_training_stat_label($stat),
    'gain' => $gain,
    'remaining' => 5,
    'duration' => 5,
    'startedAt' => $now,
  ];
  $season['training'][$user][$playerId] = $training;
  $season['updated'] = $now;
  manager_log($state, 'player_training_start', [
    'seasonId' => (string)($season['id'] ?? ''),
    'user' => $user,
    'player' => $training['playerName'],
    'playerId' => $playerId,
    'stat' => $stat,
    'gain' => $gain,
    'matches' => 5,
  ]);
  return ['ok' => true, 'training' => $training];
}

function manager_scout_auto_train_bench(array &$state, array &$season, string $user, int $now): array {
  $user = clean_name($user);
  if ($user === '' || empty($state['users'][$user]) || !is_array($state['users'][$user])) return [];
  if (manager_participant($season, $user) === null) return [];
  ensure_user_squad($state['users'][$user]);
  ensure_manager_finance($state['users'][$user]);
  $finance = $state['users'][$user]['managerFinance'] ?? [];
  $scout = manager_public_hired_scout(is_array($finance) ? $finance : []);
  if ((int)($scout['level'] ?? 0) < 3) return [];
  $search = manager_public_scout_search(is_array($finance) ? $finance : []);
  if (empty($search['autoTrainBench'])) return [];
  manager_prune_invalid_training($state, $season, $user);
  $activeTraining = manager_active_training($season, $user);
  $activeInjuries = manager_active_injuries($season, $user);
  $bench = array_values(array_filter($state['users'][$user]['squad']['bench'] ?? [], 'is_array'));
  $activeCount = count($activeTraining);
  $maxActive = MANAGER_MAX_TRAINING_PLAYERS;
  $minAvailableBench = 4;
  if ($activeCount >= $maxActive) return [];
  $availableForMatch = 0;
  $candidates = [];
  foreach ($bench as $player) {
    $id = (string)($player['id'] ?? '');
    if ($id === '' || !empty($activeInjuries[$id]) || !empty($activeTraining[$id])) continue;
    $availableForMatch++;
    if (manager_is_emergency_loan_player($player)) continue;
    $candidates[] = $player;
  }
  if ($availableForMatch <= $minAvailableBench) return [];
  $rating = fn($p) => (int)($p['rating'] ?? average_stats(is_array($p['stats'] ?? null) ? $p['stats'] : []));
  usort($candidates, fn($a, $b) => $rating($a) <=> $rating($b));
  $started = [];
  foreach ($candidates as $player) {
    if ($activeCount >= $maxActive || $availableForMatch <= $minAvailableBench) break;
    $playerId = (string)($player['id'] ?? '');
    if ($playerId === '') continue;
    $result = manager_start_player_training($state, $season, $user, $playerId, $now);
    if (empty($result['ok'])) continue;
    $started[] = $result['training'];
    $activeCount++;
    $availableForMatch--;
  }
  if ($started) {
    manager_log($state, 'scout_auto_train_bench', [
      'seasonId' => (string)($season['id'] ?? ''),
      'user' => $user,
      'count' => count($started),
      'players' => array_values(array_map(fn($t) => (string)($t['playerName'] ?? ''), $started)),
    ]);
  }
  return $started;
}

function manager_scout_auto_lineup_training_for_user(array &$state, array &$season, string $user, int $roundIndex = -1, int $now = 0): bool {
  $user = clean_name($user);
  if ($user === '' || empty($state['users'][$user]) || !is_array($state['users'][$user])) return true;
  if (manager_participant($season, $user) === null) return true;
  ensure_user_squad($state['users'][$user]);
  ensure_manager_finance($state['users'][$user]);
  $finance = $state['users'][$user]['managerFinance'] ?? [];
  $scout = manager_public_hired_scout(is_array($finance) ? $finance : []);
  if ((int)($scout['level'] ?? 0) < 3) return true;
  $search = manager_public_scout_search(is_array($finance) ? $finance : []);
  if (empty($search['autoTrainBench'])) return true;
  $trainingBlocks = array_values(array_filter(manager_injured_starters($state, $season, $user), fn($row) => (string)($row['type'] ?? '') === 'training'));
  if (!$trainingBlocks) return true;
  $trainingIds = array_keys(manager_active_training($season, $user));
  if ($trainingIds && manager_fit_player_count($state['users'][$user], $trainingIds) < 11) return false;

  $sideData = manager_side_data($state, $season, $user);
  manager_lock_next_form_for_side($season, $user, $sideData);
  $sideData = manager_apply_next_form_to_side_data($season, $user, $sideData);
  $dailyForm = is_array($sideData['dailyForm'] ?? null) ? $sideData['dailyForm'] : [];
  $beforeIds = array_values(array_map(fn($player) => (string)($player['id'] ?? ''), $state['users'][$user]['squad']['starters'] ?? []));
  $formation = manager_user_formation($state['users'][$user]);
  if (!apply_squad_formation($state['users'][$user], $formation, $dailyForm, $trainingIds)) return false;
  $afterIds = array_values(array_map(fn($player) => (string)($player['id'] ?? ''), $state['users'][$user]['squad']['starters'] ?? []));
  $remaining = array_values(array_filter(manager_injured_starters($state, $season, $user), fn($row) => (string)($row['type'] ?? '') === 'training'));
  if ($beforeIds !== $afterIds || count($remaining) !== count($trainingBlocks)) {
    $season['updated'] = $now ?: time();
    manager_log($state, 'scout_auto_training_lineup', [
      'seasonId' => (string)($season['id'] ?? ''),
      'user' => $user,
      'roundIndex' => $roundIndex,
      'formation' => $formation,
      'trainingStartersFixed' => max(0, count($trainingBlocks) - count($remaining)),
      'changed' => $beforeIds !== $afterIds,
      'remainingBlocked' => count($remaining),
    ]);
  }
  return !$remaining;
}

function manager_scout_auto_training_lineups_for_round(array &$state, array &$season, int $roundIndex, int $now = 0): void {
  foreach (manager_round_human_users($season, $roundIndex) as $user) {
    manager_scout_auto_lineup_training_for_user($state, $season, $user, $roundIndex, $now);
  }
}

function squad_skill_rating(array $squad, int $limit = 16): int {
  $players = squad_top_players($squad, $limit);
  return $players ? average_rating($players) : 0;
}

function manager_display_skill(int $skillPoints): int {
  return clamp_int((int)round(($skillPoints / MANAGER_MAX_SKILL_POINTS) * 100), 0, 100);
}

function player_age_value_multiplier(array $player): float {
  $age = isset($player['age']) && is_numeric($player['age']) ? (int)$player['age'] : 0;
  return $age >= 44 ? 0.125 : ($age === 43 ? 0.25 : ($age === 42 ? 0.5 : 1.0));
}

function player_age_wage_multiplier(array $player): float {
  $valueMultiplier = player_age_value_multiplier($player);
  return 1.0 - ((1.0 - $valueMultiplier) * 0.5);
}

function player_value(array $player): int {
  if (manager_is_emergency_loan_player($player)) return 0;
  $rating = isset($player['stats']) && is_array($player['stats']) ? average_stats($player['stats']) : (int)($player['rating'] ?? 50);
  $premium = match (player_natural_role($player)) {
    'ST' => 1.18,
    'CM', 'RM', 'LM' => 1.08,
    'GK' => 1.06,
    default => 1.0,
  };
  $base = (max(1, $rating - 35) ** 2) * 140;
  $elite = (max(0, $rating - 72) ** 3) * 95;
  $division = (string)($player['sourceDivision'] ?? $player['purchasedFromDivision'] ?? $player['originDivisionName'] ?? '');
  $divisionPremium = match ($division) {
    'Premier League' => 20.0,
    'Championship' => 8.0,
    'League One' => 4.0,
    'League Two' => 2.0,
    default => 1.0,
  };
  $valueBoost = clamp_float((float)($player['valueBoostPct'] ?? 0), 0.0, (float)MANAGER_PLAYER_VALUE_BOOST_MAX);
  return max(25000, (int)(round((($base + $elite) * $premium * $divisionPremium * (1.0 + $valueBoost / 100.0) * player_age_value_multiplier($player)) / 25000) * 25000));
}

function player_wage(array $player): int {
  if (manager_is_emergency_loan_player($player)) return 0;
  $rating = (int)($player['rating'] ?? average_stats($player['stats'] ?? []));
  $elite = max(0, $rating - 70);
  $baseWage = 250 + $rating * 12 + $elite * $elite * 6;
  return max(100, (int)round($baseWage * player_age_wage_multiplier($player)));
}

function squad_value(array $squad, ?string $group = null): int {
  $players = $group ? ($squad[$group] ?? []) : array_merge($squad['starters'] ?? [], $squad['bench'] ?? []);
  $sum = 0;
  foreach ($players as $player) $sum += player_value($player);
  return $sum;
}

function squad_wages(array $squad): int {
  $sum = 0;
  foreach (array_merge($squad['starters'] ?? [], $squad['bench'] ?? []) as $player) {
    $sum += player_wage($player);
  }
  return $sum;
}

function manager_national_league_start_balance_rows(): array {
  static $rows = null;
  if ($rows !== null) return $rows;
  $rows = [];
  foreach (training_divisions() as $division) {
    if (($division['id'] ?? '') !== 'national-league') continue;
    foreach (($division['teams'] ?? []) as $team) {
      if (!is_array($team)) continue;
      $teamId = (string)($team['sourceId'] ?? $team['id'] ?? '');
      if ($teamId === '') continue;
      $name = (string)($team['name'] ?? $teamId);
      $baseRating = (int)($team['baseRating'] ?? team_base_rating($teamId));
      $squad = make_named_squad('home', $baseRating, $teamId, $name);
      $rows[] = [
        'teamId' => source_team_id($teamId),
        'name' => $name,
        'skill' => squad_skill_points($squad),
      ];
    }
    break;
  }
  usort($rows, fn($a, $b) => ((int)$b['skill'] <=> (int)$a['skill']) ?: strcmp((string)$a['name'], (string)$b['name']));
  return $rows;
}

function manager_national_league_start_balance(?array $user = null): int {
  $minBalance = 385000;
  $maxBalance = 425000;
  $displaySkill = null;
  if ($user && !empty($user['squad']) && is_array($user['squad'])) {
    $displaySkill = manager_display_skill(squad_skill_points($user['squad']));
  }
  $rows = manager_national_league_start_balance_rows();
  if (!$rows) return 405000;
  $teamId = $user && has_team($user) ? source_team_id((string)($user['teamId'] ?? '')) : '';
  foreach ($rows as $row) {
    if ($teamId === '' || (string)$row['teamId'] !== $teamId) continue;
    if ($displaySkill === null) $displaySkill = manager_display_skill((int)$row['skill']);
    break;
  }
  if ($displaySkill === null) return 405000;
  $rate = clamp_float(($displaySkill - 41) / 9, 0.0, 1.0);
  $balance = $minBalance + (($maxBalance - $minBalance) * $rate);
  return clamp_int((int)round($balance / 1000) * 1000, $minBalance, $maxBalance);
}

function manager_start_balance(int $level, ?array $user = null): int {
  $level = clamp_int($level, 1, 5);
  if ($level === 5) return manager_national_league_start_balance($user);
  return match (clamp_int($level, 1, 5)) {
    1 => 45000000,
    2 => 12000000,
    3 => 5500000,
    4 => 2500000,
    default => 405000,
  };
}

function manager_staff_manager_catalog(): array {
  $names = [
    5 => 'Booster Trainer I',
    10 => 'Booster Trainer II',
    15 => 'Booster Trainer III',
    20 => 'Performance Booster',
    25 => 'Squad Booster',
    30 => 'Promotion Booster',
    35 => 'Senior Booster Trainer',
    40 => 'Elite Booster Trainer',
    45 => 'Master Booster Trainer',
    50 => 'World Class Booster Trainer',
  ];
  $out = [];
  $level = 1;
  foreach ($names as $tenths => $name) {
    $boostPct = $tenths / 10;
    $out[] = [
      'id' => 'mgr-' . $tenths,
      'level' => $level++,
      'name' => $name,
      'boostPct' => $boostPct,
      'cost' => (int)round($boostPct * 1000000),
    ];
  }
  return $out;
}

function manager_staff_manager_option(string $id): ?array {
  foreach (manager_staff_manager_catalog() as $option) {
    if ((string)$option['id'] === $id) return $option;
  }
  return null;
}

function manager_staff_manager_level(?array $manager): int {
  if (!$manager) return 0;
  $id = (string)($manager['id'] ?? '');
  $boost = (float)($manager['boostPct'] ?? 0);
  foreach (manager_staff_manager_catalog() as $option) {
    if ($id !== '' && (string)$option['id'] === $id) return (int)$option['level'];
    if ($id === '' && abs((float)$option['boostPct'] - $boost) < 0.001) return (int)$option['level'];
  }
  return 0;
}

function manager_public_hired_manager(array $finance): ?array {
  $hired = is_array($finance['hiredManager'] ?? null) ? $finance['hiredManager'] : null;
  if (!$hired) return null;
  $id = (string)($hired['id'] ?? '');
  $option = $id !== '' ? manager_staff_manager_option($id) : null;
  $out = $option ?: [
    'id' => $id,
    'name' => (string)($hired['name'] ?? 'Hired Manager'),
    'boostPct' => (float)($hired['boostPct'] ?? 0),
    'cost' => (int)($hired['cost'] ?? 0),
  ];
  $out['hiredAt'] = (int)($hired['hiredAt'] ?? 0);
  return $out;
}

function manager_scout_catalog(): array {
  return [
    [
      'id' => 'scout-1',
      'name' => 'Local Scout',
      'level' => 1,
      'cost' => 250000,
      'feePct' => 7.5,
      'scope' => 'market',
      'canBrowse' => false,
      'canAutoBuy' => false,
      'description' => 'Searches the open transfer market.',
    ],
    [
      'id' => 'scout-2',
      'name' => 'League Scout',
      'level' => 2,
      'cost' => 750000,
      'feePct' => 5.0,
      'scope' => 'market_browse',
      'canBrowse' => true,
      'canAutoBuy' => false,
      'description' => 'Searches the market and Browse clubs.',
    ],
    [
      'id' => 'scout-3',
      'name' => 'Chief Scout',
      'level' => 3,
      'cost' => 1500000,
      'feePct' => 2.5,
      'scope' => 'market_browse_auto',
      'canBrowse' => true,
      'canAutoBuy' => true,
      'description' => 'Can auto-buy one matching player when you run scout.',
    ],
  ];
}

function manager_scout_option(string $id): ?array {
  foreach (manager_scout_catalog() as $option) {
    if ((string)$option['id'] === $id) return $option;
  }
  return null;
}

function manager_public_hired_scout(array $finance): ?array {
  $hired = is_array($finance['hiredScout'] ?? null) ? $finance['hiredScout'] : null;
  if (!$hired) return null;
  $id = (string)($hired['id'] ?? '');
  $option = $id !== '' ? manager_scout_option($id) : null;
  $out = $option ?: [
    'id' => $id,
    'name' => (string)($hired['name'] ?? 'Scout'),
    'level' => (int)($hired['level'] ?? 1),
    'cost' => (int)($hired['cost'] ?? 0),
    'feePct' => (float)($hired['feePct'] ?? 7.5),
    'canBrowse' => !empty($hired['canBrowse']),
    'canAutoBuy' => !empty($hired['canAutoBuy']),
  ];
  $out['hiredAt'] = (int)($hired['hiredAt'] ?? 0);
  return $out;
}

function manager_scout_stat_options(): array {
  return array_values(array_unique(array_merge(player_stat_keys(false), ['keeping'])));
}

function manager_scout_search_defaults(): array {
  return [
    'role' => 'ST',
    'stat' => 'shooting',
    'minStat' => 80,
    'maxPrice' => 0,
    'scope' => 'market',
    'autoBuy' => false,
    'autoTrainBench' => false,
  ];
}

function manager_normalize_scout_search(array $data, ?array $scout = null): array {
  $defaults = manager_scout_search_defaults();
  $roleRaw = strtoupper(trim((string)($data['role'] ?? $defaults['role'])));
  $role = $roleRaw === '' || $roleRaw === 'ALL' ? 'ALL' : canonical_player_role($roleRaw, 'ST');
  $statRaw = strtolower(preg_replace('/[^a-z]/', '', (string)($data['stat'] ?? $defaults['stat'])));
  $statOptions = manager_scout_stat_options();
  $stat = in_array($statRaw, $statOptions, true) ? $statRaw : $defaults['stat'];
  $level = clamp_int((int)($scout['level'] ?? 0), 0, 3);
  $scope = strtolower(trim((string)($data['scope'] ?? $defaults['scope'])));
  if (!in_array($scope, ['market', 'browse', 'all'], true)) $scope = 'market';
  if ($level < 2 && $scope !== 'market') $scope = 'market';
  $autoBuy = !empty($data['autoBuy']) && $level >= 3;
  $autoTrainBench = !empty($data['autoTrainBench']) && $level >= 3;
  return [
    'role' => $role,
    'stat' => $stat,
    'minStat' => clamp_int((int)($data['minStat'] ?? $defaults['minStat']), 1, 100),
    'maxPrice' => max(0, (int)round(((int)($data['maxPrice'] ?? $defaults['maxPrice'])) / 1000) * 1000),
    'scope' => $scope,
    'autoBuy' => $autoBuy,
    'autoTrainBench' => $autoTrainBench,
  ];
}

function manager_public_scout_search(array $finance): array {
  $scout = manager_public_hired_scout($finance);
  $saved = is_array($finance['scoutSearch'] ?? null) ? $finance['scoutSearch'] : [];
  return manager_normalize_scout_search($saved ?: manager_scout_search_defaults(), $scout);
}

function manager_scout_fee(int $price, array $scout): int {
  $pct = clamp_float((float)($scout['feePct'] ?? 7.5), 0.0, 25.0);
  return manager_round_money(max(5000, (int)round($price * $pct / 100)), 500);
}

function manager_scout_player_role_tokens(array $player): array {
  $tokens = [];
  foreach ([player_display_role($player), player_natural_role($player), (string)($player['role'] ?? ''), (string)($player['slotRole'] ?? '')] as $raw) {
    foreach (explode('/', strtoupper($raw)) as $part) {
      $role = canonical_player_role($part, '');
      if ($role !== '') $tokens[$role] = true;
    }
  }
  return array_keys($tokens);
}

function manager_scout_player_matches(array $player, array $search, int $price): bool {
  ensure_player_stats($player);
  $role = strtoupper((string)($search['role'] ?? 'ALL'));
  if ($role !== 'ALL' && !in_array($role, manager_scout_player_role_tokens($player), true)) return false;
  $stat = (string)($search['stat'] ?? 'shooting');
  $value = (int)($player['stats'][$stat] ?? 0);
  if ($value < (int)($search['minStat'] ?? 0)) return false;
  $maxPrice = (int)($search['maxPrice'] ?? 0);
  if ($maxPrice > 0 && $price > $maxPrice) return false;
  return true;
}

function manager_scout_candidate(array $player, string $source, int $price, string $teamUser, string $teamName, string $divisionName, int $marketIndex = -1): array {
  ensure_player_stats($player);
  return [
    'source' => $source,
    'marketIndex' => $marketIndex,
    'teamUser' => $teamUser,
    'teamName' => $teamName,
    'divisionName' => $divisionName,
    'price' => max(0, $price),
    'player' => $player,
  ];
}

function manager_public_scout_candidate(array $candidate, array $search): array {
  $player = public_player(is_array($candidate['player'] ?? null) ? $candidate['player'] : []);
  $price = max(0, (int)($candidate['price'] ?? $player['price'] ?? $player['value'] ?? player_value($player)));
  $player['value'] = player_value($player);
  $player['price'] = $price;
  $player['scoutSource'] = (string)($candidate['source'] ?? 'market');
  $player['sourceTeam'] = (string)($candidate['teamName'] ?? $player['sourceTeam'] ?? '');
  $player['sourceTeamId'] = (string)($candidate['teamUser'] ?? $player['sourceTeamId'] ?? '');
  $player['sourceDivision'] = (string)($candidate['divisionName'] ?? $player['sourceDivision'] ?? '');
  $stat = (string)($search['stat'] ?? 'shooting');
  $player['scoutStat'] = $stat;
  $player['scoutStatValue'] = (int)($player['stats'][$stat] ?? 0);
  return $player;
}

function manager_sort_scout_candidates(array &$candidates, array $search): void {
  $stat = (string)($search['stat'] ?? 'shooting');
  usort($candidates, function ($a, $b) use ($stat) {
    $ap = is_array($a['player'] ?? null) ? $a['player'] : [];
    $bp = is_array($b['player'] ?? null) ? $b['player'] : [];
    ensure_player_stats($ap);
    ensure_player_stats($bp);
    foreach ([
      ((int)($bp['stats'][$stat] ?? 0)) <=> ((int)($ap['stats'][$stat] ?? 0)),
      ((int)($bp['rating'] ?? average_stats($bp['stats'] ?? []))) <=> ((int)($ap['rating'] ?? average_stats($ap['stats'] ?? []))),
      ((int)($a['price'] ?? 0)) <=> ((int)($b['price'] ?? 0)),
    ] as $cmp) {
      if ($cmp !== 0) return $cmp;
    }
    return strcasecmp((string)($ap['name'] ?? ''), (string)($bp['name'] ?? ''));
  });
}

function manager_scout_market_candidates(array &$state, ?array $season, string $user, array $search): array {
  ensure_manager_market($state);
  $out = [];
  $buyerLevel = isset($state['users'][$user]) ? manager_user_division_level($state, $user) : 5;
  foreach (($state['managerMarket'] ?? []) as $index => $player) {
    if (!is_array($player) || manager_is_emergency_loan_player($player)) continue;
    if (!empty($player['marketListedByUser'])) continue;
    $sourceTeamId = (string)($player['sourceTeamId'] ?? '');
    if ($sourceTeamId !== '' && manager_team_controlled_by_human($state, $season, $sourceTeamId)) continue;
    $basePrice = max(0, (int)($player['price'] ?? player_value($player)));
    $rule = manager_market_division_rule($player, $buyerLevel, $basePrice);
    $price = (int)$rule['price'];
    if (!manager_scout_player_matches($player, $search, $price)) continue;
    $out[] = manager_scout_candidate($player, 'market', $price, $sourceTeamId ?: 'market', (string)($player['sourceTeam'] ?? 'TRANSFER MARKET'), (string)($player['sourceDivision'] ?? ''), (int)$index);
  }
  return $out;
}

function manager_scout_browse_candidates(array &$state, ?array $season, string $user, array $search): array {
  $out = [];
  $humanKeys = manager_human_team_keys($state, $season);
  foreach (manager_all_division_teams() as $team) {
    $teamKey = source_team_id((string)($team['sourceId'] ?? $team['id'] ?? ''));
    if ($teamKey !== '' && isset($humanKeys[$teamKey])) continue;
    $teamUser = manager_cpu_id((string)$team['id']);
    $details = manager_browse_team_details($state, $teamUser, $user);
    $divisionName = (string)($details['divisionName'] ?? manager_division_name((int)($team['managerDivisionLevel'] ?? 5)));
    $teamName = (string)($details['team']['name'] ?? $team['name'] ?? strtoupper($teamUser));
    foreach (['starters', 'bench'] as $group) {
      foreach (($details['squad'][$group] ?? []) as $player) {
        if (!is_array($player)) continue;
        $price = max(0, (int)($player['price'] ?? $player['value'] ?? player_value($player)));
        if (!manager_scout_player_matches($player, $search, $price)) continue;
        $out[] = manager_scout_candidate($player, 'browse', $price, $teamUser, $teamName, $divisionName);
      }
    }
  }
  return $out;
}

function manager_scout_candidates(array &$state, ?array $season, string $user, array $search, array $scout): array {
  $scope = (string)($search['scope'] ?? 'market');
  $candidates = [];
  if ($scope === 'market' || $scope === 'all') {
    $candidates = array_merge($candidates, manager_scout_market_candidates($state, $season, $user, $search));
  }
  if (((int)($scout['level'] ?? 0)) >= 2 && ($scope === 'browse' || $scope === 'all')) {
    $candidates = array_merge($candidates, manager_scout_browse_candidates($state, $season, $user, $search));
  }
  manager_sort_scout_candidates($candidates, $search);
  return $candidates;
}

function complete_manager_market_purchase(array &$state, ?array $season, string $user, int $marketIndex, array $player, int $price, int $now, int $scoutFee = 0): array {
  ensure_user_squad($state['users'][$user]);
  ensure_manager_finance($state['users'][$user]);
  if ($marketIndex < 0 || empty($state['managerMarket'][$marketIndex]) || !is_array($state['managerMarket'][$marketIndex])) reply(['ok' => false, 'error' => 'bad_player'], 404);
  if ((int)$state['users'][$user]['managerFinance']['balance'] < $price + $scoutFee) reply(['ok' => false, 'error' => 'no_money'], 409);
  $beforeSquad = $state['users'][$user]['squad'] ?? ['starters' => [], 'bench' => []];
  $fromTeam = (string)($player['sourceTeam'] ?? 'TRANSFER MARKET');
  $fromTeamId = (string)($player['sourceTeamId'] ?? 'market');
  $fromDivision = (string)($player['sourceDivision'] ?? '');
  $bought = manager_add_purchased_player_to_user($state['users'][$user], $player, $fromTeam, $fromTeamId, $fromDivision, $price, $now);
  $state['users'][$user]['managerFinance']['balance'] -= $price + $scoutFee;
  $state['users'][$user]['managerFinance']['transferSpent'] += $price;
  if ($scoutFee > 0) $state['users'][$user]['managerFinance']['scoutSpent'] = (int)($state['users'][$user]['managerFinance']['scoutSpent'] ?? 0) + $scoutFee;
  array_splice($state['managerMarket'], $marketIndex, 1);
  ensure_manager_market($state);
  $boughtPlayer = public_player($bought);
  $boughtPlayer['value'] = player_value($boughtPlayer);
  return [
    'managerSeason' => public_manager_season($season, $state, $user),
    'managerFinance' => public_finance($state['users'][$user]),
    'managerSquad' => public_manager_context_squad($state, $user, isset($season) && is_array($season) ? $season : current_manager_season($state, $user)),
	    'managerMarket' => public_manager_market($state, $season, $buyer),
    'managerTransferOffers' => public_manager_transfer_offers($state, $user),
    'boughtPlayer' => $boughtPlayer,
    'price' => $price,
    'cashPrice' => $price,
    'scoutFee' => $scoutFee,
    'skillChange' => manager_browse_skill_change($beforeSquad, $state['users'][$user]['squad'] ?? []),
  ];
}

function manager_staff_boost_pct(array $user): float {
  $finance = $user['managerFinance'] ?? [];
  $hired = manager_public_hired_manager(is_array($finance) ? $finance : []);
  return $hired ? clamp_float((float)($hired['boostPct'] ?? 0), 0.0, 5.0) : 0.0;
}

function manager_apply_staff_boost_to_squad(array $squad, array $user): array {
  $boostPct = manager_staff_boost_pct($user);
  if ($boostPct <= 0) return $squad;
  $multiplier = 1.0 + ($boostPct / 100.0);
  foreach (['starters', 'bench'] as $group) {
    if (empty($squad[$group]) || !is_array($squad[$group])) continue;
    foreach ($squad[$group] as &$player) {
      $player['hiredManagerBoostPct'] = $boostPct;
      $player['matchStatMultiplier'] = (float)($player['matchStatMultiplier'] ?? 1.0) * $multiplier;
    }
    unset($player);
  }
  return $squad;
}

function manager_fit_player_count(array $user, array $unavailablePlayerIds): int {
  $unavailableSet = array_fill_keys(array_map('strval', $unavailablePlayerIds), true);
  $count = 0;
  foreach (array_merge($user['squad']['starters'] ?? [], $user['squad']['bench'] ?? []) as $player) {
    $id = (string)($player['id'] ?? '');
    if ($id === '' || empty($unavailableSet[$id])) $count++;
  }
  return $count;
}

function manager_staff_auto_lineup_for_user(array &$state, array &$season, string $user, int $roundIndex = -1, int $now = 0, bool $onlyIfInjuredBlock = false): bool {
  $user = clean_name($user);
  if ($user === '' || empty($state['users'][$user]) || !is_array($state['users'][$user])) return true;
  if (!has_team($state['users'][$user])) return true;
  ensure_user_squad($state['users'][$user]);
  ensure_manager_finance($state['users'][$user]);
  if (manager_staff_boost_pct($state['users'][$user]) <= 0) return true;
  if (manager_participant($season, $user) === null) return true;

  $blockedBefore = manager_injured_starters($state, $season, $user);
  if ($onlyIfInjuredBlock && !$blockedBefore) return true;
  if (!empty($state['users'][$user]['manager']['manualLineupOverrideAt']) && !$blockedBefore) return true;

  $injuredPlayerIds = manager_unavailable_player_ids($season, $user);
  if ($injuredPlayerIds && manager_fit_player_count($state['users'][$user], $injuredPlayerIds) < 11) return false;

  $sideData = manager_side_data($state, $season, $user);
  manager_lock_next_form_for_side($season, $user, $sideData);
  $sideData = manager_apply_next_form_to_side_data($season, $user, $sideData);
  $dailyForm = is_array($sideData['dailyForm'] ?? null) ? $sideData['dailyForm'] : [];

  $beforeIds = array_values(array_map(fn($player) => (string)($player['id'] ?? ''), $state['users'][$user]['squad']['starters'] ?? []));
  $formation = manager_user_formation($state['users'][$user]);
  $ok = apply_squad_formation($state['users'][$user], $formation, $dailyForm, $injuredPlayerIds);
  if (!$ok) return false;
  $afterIds = array_values(array_map(fn($player) => (string)($player['id'] ?? ''), $state['users'][$user]['squad']['starters'] ?? []));
  if ($blockedBefore || $beforeIds !== $afterIds) {
    $season['updated'] = $now ?: time();
    manager_log($state, 'staff_manager_auto_lineup', [
      'seasonId' => (string)($season['id'] ?? ''),
      'user' => $user,
      'roundIndex' => $roundIndex,
      'formation' => $formation,
      'injuredStartersFixed' => count($blockedBefore),
      'changed' => $beforeIds !== $afterIds,
    ]);
  }
  return true;
}

function manager_staff_auto_lineups_for_round(array &$state, array &$season, int $roundIndex, int $now = 0, bool $onlyIfInjuredBlock = false): void {
  foreach (manager_round_human_users($season, $roundIndex) as $user) {
    manager_staff_auto_lineup_for_user($state, $season, $user, $roundIndex, $now, $onlyIfInjuredBlock);
  }
}

function manager_round_money(int $value, int $step = 500): int {
  return (int)round($value / max(1, $step)) * max(1, $step);
}

function manager_loan_payments_per_year(): int {
  return 46;
}

function manager_loan_rate_options(): array {
  return [
    ['years' => 1, 'ratePct' => 6.0],
    ['years' => 2, 'ratePct' => 8.5],
    ['years' => 3, 'ratePct' => 11.0],
    ['years' => 4, 'ratePct' => 13.5],
    ['years' => 5, 'ratePct' => 16.0],
  ];
}

function manager_loan_rate_pct(int $years): float {
  $years = clamp_int($years, 1, 5);
  foreach (manager_loan_rate_options() as $option) {
    if ((int)$option['years'] === $years) return (float)$option['ratePct'];
  }
  return 16.0;
}

function manager_public_loan(array $finance, int $level): array {
  $storedStart = (int)($finance['startBalance'] ?? 0);
  $startBalance = max(1, $storedStart > 0 ? $storedStart : manager_finance_estimated_start_balance($finance, $level));
  $maxAmount = $startBalance * 3;
  $loan = is_array($finance['loan'] ?? null) ? $finance['loan'] : null;
  if ($loan && (int)($loan['remaining'] ?? 0) <= 0) $loan = null;
  $current = null;
  if ($loan) {
    $current = [
      'principal' => (int)($loan['principal'] ?? 0),
      'cashBorrowed' => (int)($loan['cashBorrowed'] ?? $loan['principal'] ?? 0),
      'remaining' => max(0, (int)($loan['remaining'] ?? 0)),
      'totalDue' => (int)($loan['totalDue'] ?? 0),
      'interestTotal' => (int)($loan['interestTotal'] ?? 0),
      'paidTotal' => (int)($loan['paidTotal'] ?? 0),
      'termYears' => clamp_int((int)($loan['termYears'] ?? 1), 1, 5),
      'ratePct' => (float)($loan['ratePct'] ?? manager_loan_rate_pct((int)($loan['termYears'] ?? 1))),
      'paymentsTotal' => max(1, (int)($loan['paymentsTotal'] ?? manager_loan_payments_per_year())),
      'paymentsLeft' => max(0, (int)($loan['paymentsLeft'] ?? 0)),
      'paymentPerMatch' => max(0, (int)($loan['paymentPerMatch'] ?? 0)),
      'startedAt' => (int)($loan['startedAt'] ?? 0),
    ];
  }
  return [
    'active' => $current !== null,
    'startBalance' => $startBalance,
    'maxAmount' => $maxAmount,
    'available' => $current ? 0 : $maxAmount,
    'minAmount' => 50000,
    'step' => 50000,
    'paymentsPerYear' => manager_loan_payments_per_year(),
    'rateOptions' => manager_loan_rate_options(),
    'current' => $current,
  ];
}

function manager_apply_loan_payment(array &$finance): int {
  $loan = is_array($finance['loan'] ?? null) ? $finance['loan'] : null;
  if (!$loan) return 0;
  $remainingBefore = max(0, (int)($loan['remaining'] ?? 0));
  if ($remainingBefore <= 0) {
    $finance['lastLoan'] = $loan + ['paidAt' => time()];
    $finance['loan'] = null;
    return 0;
  }
  $payment = min($remainingBefore, max(1, (int)($loan['paymentPerMatch'] ?? 0)));
  $interestRemaining = max(0, (int)($loan['interestRemaining'] ?? 0));
  $interestPaid = $interestRemaining > 0 ? min($interestRemaining, (int)round($payment * ($interestRemaining / max(1, $remainingBefore)))) : 0;
  $principalPaid = max(0, $payment - $interestPaid);
  $loan['remaining'] = max(0, $remainingBefore - $payment);
  $loan['principalRemaining'] = max(0, (int)($loan['principalRemaining'] ?? $loan['principal'] ?? 0) - $principalPaid);
  $loan['interestRemaining'] = max(0, $interestRemaining - $interestPaid);
  $loan['paidTotal'] = (int)($loan['paidTotal'] ?? 0) + $payment;
  $loan['paymentsLeft'] = max(0, (int)($loan['paymentsLeft'] ?? 0) - 1);
  $finance['balance'] = (int)($finance['balance'] ?? 0) - $payment;
  $finance['loanRepaid'] = (int)($finance['loanRepaid'] ?? 0) + $payment;
  $finance['loanInterestPaid'] = (int)($finance['loanInterestPaid'] ?? 0) + $interestPaid;
  if ((int)$loan['remaining'] <= 0) {
    $loan['remaining'] = 0;
    $loan['paymentsLeft'] = 0;
    $loan['paidAt'] = time();
    $finance['lastLoan'] = $loan;
    $finance['loan'] = null;
  } else {
    $finance['loan'] = $loan;
  }
  return $payment;
}
