<?php
declare(strict_types=1);

function pick_goal_scorer(array $players): ?array {
  if (!$players) return null;
  $pool = [];
  foreach ($players as $player) {
    $role = (string)($player['role'] ?? 'CM');
    $weight = match ($role) {
      'ST' => 7,
      'CM', 'RM', 'LM' => 4,
      'RB', 'LB' => 2,
      'CB' => 1,
      default => 1,
    };
    $weight += max(0, (player_stat($player, 'shooting') - 45) / 12);
    for ($i = 0; $i < (int)round($weight); $i++) $pool[] = $player;
  }
  return $pool ? $pool[array_rand($pool)] : $players[array_rand($players)];
}

function add_scorer(array &$season, string $user, array $team, array $player, int $goals, int $penalties = 0, array $context = []): void {
  if ($goals <= 0) return;
  if (!isset($season['scorers']) || !is_array($season['scorers'])) $season['scorers'] = [];
  $context = $context ?: manager_competition_context_from_fixture(['phase' => 'league']);
  $competitionKey = (string)($context['key'] ?? 'league');
  $playerId = (string)($player['id'] ?? $player['playerId'] ?? '');
  $key = $competitionKey . ':' . $user . ':' . ($playerId !== '' ? $playerId : (string)($player['name'] ?? 'Player'));
  if (!isset($season['scorers'][$key])) {
    $season['scorers'][$key] = [
      'user' => $user,
      'team' => $team,
      'playerId' => $playerId,
      'player' => (string)($player['name'] ?? 'Player'),
      'competitionKey' => $competitionKey,
      'phase' => (string)($context['phase'] ?? 'league'),
      'cupKey' => (string)($context['cupKey'] ?? ''),
      'competitionName' => (string)($context['label'] ?? 'League'),
      'goals' => 0,
      'penalties' => 0,
    ];
  }
  $season['scorers'][$key]['goals'] += $goals;
  $season['scorers'][$key]['penalties'] = (int)($season['scorers'][$key]['penalties'] ?? 0) + max(0, min($goals, $penalties));
}

function add_assist(array &$season, string $user, array $team, array $player, int $assists = 1, array $context = []): void {
  if ($assists <= 0) return;
  if (!isset($season['assists']) || !is_array($season['assists'])) $season['assists'] = [];
  $context = $context ?: manager_competition_context_from_fixture(['phase' => 'league']);
  $competitionKey = (string)($context['key'] ?? 'league');
  $playerId = (string)($player['id'] ?? $player['playerId'] ?? '');
  $key = $competitionKey . ':' . $user . ':' . ($playerId !== '' ? $playerId : (string)($player['name'] ?? 'Player'));
  if (!isset($season['assists'][$key])) {
    $season['assists'][$key] = [
      'user' => $user,
      'team' => $team,
      'playerId' => $playerId,
      'player' => (string)($player['name'] ?? 'Player'),
      'competitionKey' => $competitionKey,
      'phase' => (string)($context['phase'] ?? 'league'),
      'cupKey' => (string)($context['cupKey'] ?? ''),
      'competitionName' => (string)($context['label'] ?? 'League'),
      'assists' => 0,
    ];
  }
  $season['assists'][$key]['assists'] += $assists;
}

function pick_assist_player(array $players, ?array $scorer = null): ?array {
  if (!$players) return null;
  $scorerName = (string)($scorer['name'] ?? '');
  $pool = [];
  foreach ($players as $player) {
    if ($scorerName !== '' && (string)($player['name'] ?? '') === $scorerName) continue;
    if (($player['role'] ?? '') === 'GK') continue;
    $role = (string)($player['role'] ?? 'CM');
    $weight = match ($role) {
      'CM' => 7,
      'LM', 'RM' => 6,
      'ST' => 4,
      'LB', 'RB' => 3,
      'CB' => 1,
      default => 2,
    };
    $weight += max(0, (player_stat($player, 'passing') + player_stat($player, 'vision') - 95) / 18);
    for ($i = 0; $i < (int)round($weight); $i++) $pool[] = $player;
  }
  return $pool ? $pool[array_rand($pool)] : null;
}

function manager_normalized_strength(int $strength): int {
  return max(1, $strength <= 120 ? $strength * 128 : $strength);
}

function manager_squad_match_profile(array $squad, int $fallbackStrength = 0): array {
  ensure_squad_stats($squad);
  $players = $squad['starters'] ?? [];
  if (!$players) $players = $squad['bench'] ?? [];
  $activeSquad = ['starters' => array_values($players), 'bench' => []];
  $activeStrength = $players ? squad_match_skill_points($activeSquad) : max(1, $fallbackStrength);
  $profile = ['attack' => 0.0, 'midfield' => 0.0, 'defense' => 0.0, 'keeper' => 0.0, 'overall' => max(1, $fallbackStrength)];
  $weights = ['attack' => 0.0, 'midfield' => 0.0, 'defense' => 0.0, 'keeper' => 0.0];
  foreach ($players as $player) {
    $role = strtoupper((string)($player['role'] ?? 'CM'));
    $attack = player_stat($player, 'shooting') * 1.25 + player_stat($player, 'shotcontrol') * 0.85 + player_stat($player, 'speed') * 0.45 + player_stat($player, 'heading') * 0.30 + player_stat($player, 'composure') * 0.35;
    $mid = player_stat($player, 'passing') * 1.10 + player_stat($player, 'passcontrol') * 0.85 + player_stat($player, 'vision') * 0.75 + player_stat($player, 'workrate') * 0.45 + player_stat($player, 'dribbling') * 0.35;
    $def = player_stat($player, 'tackling') * 1.15 + player_stat($player, 'positioning') * 0.80 + player_stat($player, 'heading') * 0.55 + player_stat($player, 'strength') * 0.45 + player_stat($player, 'discipline') * 0.30;
    $keep = player_stat($player, 'keeping') * 1.40 + player_stat($player, 'positioning') * 0.45 + player_stat($player, 'passcontrol') * 0.20;
    $roleWeights = match ($role) {
      'GK' => ['attack' => 0.02, 'midfield' => 0.08, 'defense' => 0.10, 'keeper' => 1.00],
      'CB', 'LB', 'RB', 'LWB', 'RWB' => ['attack' => 0.10, 'midfield' => 0.28, 'defense' => 1.00, 'keeper' => 0.00],
      'CM', 'DM', 'CDM', 'AM', 'CAM', 'LM', 'RM' => ['attack' => 0.36, 'midfield' => 1.00, 'defense' => 0.48, 'keeper' => 0.00],
      default => ['attack' => 1.00, 'midfield' => 0.38, 'defense' => 0.10, 'keeper' => 0.00],
    };
    foreach ($roleWeights as $key => $weight) {
      $value = match ($key) {
        'attack' => $attack,
        'midfield' => $mid,
        'defense' => $def,
        default => $keep,
      };
      $profile[$key] += $value * $weight;
      $weights[$key] += $weight;
    }
  }
  foreach ($weights as $key => $weight) {
    $profile[$key] = $weight > 0 ? $profile[$key] / $weight : max(1, $fallbackStrength / 320);
  }
  $profile['overall'] = max(1, $activeStrength);
  return $profile;
}

function manager_recent_form(array $season, string $user, int $limit = 3): float {
  return (float)(manager_league_form_state($season, $user, max(1, $limit))['modifier'] ?? 0.0);
}

function manager_league_form_state(array $season, string $user, int $limit = 3): array {
  $results = [];
  foreach (($season['rounds'] ?? []) as $round) {
    foreach (($round['fixtures'] ?? []) as $fixture) {
      $phase = strtolower((string)($fixture['phase'] ?? $round['phase'] ?? 'league'));
      if (($fixture['status'] ?? '') !== 'played' || $phase !== 'league') continue;
      if (($fixture['home'] ?? '') !== $user && ($fixture['away'] ?? '') !== $user) continue;
      $score = $fixture['score'] ?? null;
      if (!is_array($score)) continue;
      $home = (string)($fixture['home'] ?? '');
      $gf = $home === $user ? (int)$score[0] : (int)$score[1];
      $ga = $home === $user ? (int)$score[1] : (int)$score[0];
      $results[] = $gf > $ga ? 'W' : ($gf < $ga ? 'L' : 'D');
    }
  }
  $recent = array_slice($results, -$limit);
  $trend = '';
  $modifier = 0.0;
  if (count($recent) >= $limit) {
    if (count(array_filter($recent, fn($result) => $result === 'W')) === $limit) {
      $trend = 'up';
      $modifier = 0.03;
    } elseif (count(array_filter($recent, fn($result) => $result === 'L')) === $limit) {
      $trend = 'down';
      $modifier = -0.03;
    }
  }
  return [
    'trend' => $trend,
    'modifier' => $modifier,
    'pct' => (int)round($modifier * 100),
    'recent' => $recent,
  ];
}

function manager_apply_league_form_to_side_data(array $season, string $user, array $sideData): array {
  $form = manager_league_form_state($season, $user);
  $sideData['leagueForm'] = $form;
  $modifier = (float)($form['modifier'] ?? 0.0);
  if ($modifier === 0.0) return $sideData;
  foreach (['rating', 'matchStrength'] as $key) {
    if (!isset($sideData[$key]) || !is_numeric($sideData[$key])) continue;
    $sideData[$key] = max(1, (int)round((float)$sideData[$key] * (1.0 + $modifier)));
  }
  return $sideData;
}

function manager_match_profile(array $season, string $user, array $sideData, bool $home): array {
  $profile = manager_squad_match_profile($sideData['squad'] ?? [], (int)($sideData['matchStrength'] ?? $sideData['strength'] ?? 0));
  $form = manager_recent_form($season, $user);
  $homeBoost = $home ? 0.045 : 0.0;
  $multiplier = 1.0 + $form + $homeBoost;
  foreach (['attack', 'midfield', 'defense', 'keeper', 'overall'] as $key) $profile[$key] *= $multiplier;
  $profile['form'] = $form;
  $profile['homeBoost'] = $homeBoost;
  return $profile;
}

function manager_profile_result_power(array $profile): int {
  return (int)round(
    (float)$profile['attack'] * 86
    + (float)$profile['midfield'] * 76
    + (float)$profile['defense'] * 70
    + (float)$profile['keeper'] * 38
    + (float)$profile['overall'] * 0.20
  );
}

function manager_match_probabilities(int $homeStrength, int $awayStrength): array {
  $homeStrength = manager_normalized_strength($homeStrength);
  $awayStrength = manager_normalized_strength($awayStrength);
  $average = max(1.0, ($homeStrength + $awayStrength) / 2.0);
  $edge = clamp_float(($homeStrength - $awayStrength) / $average, -0.75, 0.75);
  $homeShare = clamp_float(0.535 + $edge * 0.34, 0.12, 0.88);
  $draw = clamp_float(0.30 - abs($homeShare - 0.50) * 0.24, 0.15, 0.30);
  $homeWin = (1.0 - $draw) * $homeShare;
  $awayWin = max(0.01, 1.0 - $draw - $homeWin);
  $homePct = clamp_int((int)round($homeWin * 100), 1, 98);
  $drawPct = clamp_int((int)round($draw * 100), 1, 98);
  $awayPct = max(1, 100 - $homePct - $drawPct);
  if ($homePct + $drawPct + $awayPct !== 100) $drawPct += 100 - $homePct - $drawPct - $awayPct;
  return ['home' => $homePct, 'draw' => $drawPct, 'away' => $awayPct];
}

function manager_fixture_probabilities(array $season, string $home, string $away, array $homeData, array $awayData): array {
  $homeProfile = manager_match_profile($season, $home, $homeData, true);
  $awayProfile = manager_match_profile($season, $away, $awayData, false);
  return manager_match_probabilities(manager_profile_result_power($homeProfile), manager_profile_result_power($awayProfile));
}

function manager_goal_roll(float $expected): int {
  $lambda = clamp_float($expected, 0.10, 2.85);
  $limit = exp(-$lambda);
  $goals = 0;
  $product = 1.0;
  do {
    $goals++;
    $product *= max(0.000001, rand_float());
  } while ($product > $limit && $goals < 11);
  return manager_apply_high_score_goal_brake(max(0, $goals - 1));
}

function manager_high_score_goal_factor(int $currentGoals): float {
  if ($currentGoals < 5) return 1.0;
  return 1.0 / (2 ** min(8, $currentGoals - 4));
}

function manager_high_score_goal_allowed(int $currentGoals): bool {
  return rand_float() <= manager_high_score_goal_factor($currentGoals);
}

function manager_apply_high_score_goal_brake(int $rawGoals): int {
  $rawGoals = clamp_int($rawGoals, 0, 10);
  $goals = 0;
  for ($i = 0; $i < $rawGoals; $i++) {
    if (!manager_high_score_goal_allowed($goals)) break;
    $goals++;
  }
  return $goals;
}

function match_side_goals(array $match, string $side): int {
  $index = $side === 'away' ? 1 : 0;
  return max(0, (int)($match['score'][$index] ?? 0));
}

function manager_fixture_score(array $season, string $home, string $away, array $homeData, array $awayData): array {
  $homeProfile = manager_match_profile($season, $home, $homeData, true);
  $awayProfile = manager_match_profile($season, $away, $awayData, false);
  $odds = manager_match_probabilities(manager_profile_result_power($homeProfile), manager_profile_result_power($awayProfile));
  $roll = mt_rand(1, 100);
  if ($roll <= $odds['home']) $outcome = 'home';
  elseif ($roll <= $odds['home'] + $odds['draw']) $outcome = 'draw';
  else $outcome = 'away';
  $homeAttackEdge = (($homeProfile['attack'] * 0.72 + $homeProfile['midfield'] * 0.28) - ($awayProfile['defense'] * 0.70 + $awayProfile['keeper'] * 0.30)) / 100.0;
  $awayAttackEdge = (($awayProfile['attack'] * 0.72 + $awayProfile['midfield'] * 0.28) - ($homeProfile['defense'] * 0.70 + $homeProfile['keeper'] * 0.30)) / 100.0;
  $hg = manager_goal_roll(1.22 + $homeAttackEdge * 0.62 + ($homeProfile['homeBoost'] ?? 0) * 1.25);
  $ag = manager_goal_roll(1.03 + $awayAttackEdge * 0.58);
  if ($outcome === 'draw') {
    $avg = clamp_int((int)round(($hg + $ag) / 2), 0, 3);
    return [$avg, $avg];
  }
  if ($outcome === 'home' && $hg <= $ag) {
    $ag = min($ag, mt_rand(1, 100) <= 78 ? 1 : 2);
    $hg = min(5, $ag + (mt_rand(1, 100) <= 82 ? 1 : 2));
  }
  if ($outcome === 'away' && $ag <= $hg) {
    $hg = min($hg, mt_rand(1, 100) <= 78 ? 1 : 2);
    $ag = min(5, $hg + (mt_rand(1, 100) <= 82 ? 1 : 2));
  }
  return [$hg, $ag];
}

function manager_score(int $homeStrength, int $awayStrength): array {
  $odds = manager_match_probabilities($homeStrength, $awayStrength);
  $roll = mt_rand(1, 100);
  if ($roll <= $odds['home']) $outcome = 'home';
  elseif ($roll <= $odds['home'] + $odds['draw']) $outcome = 'draw';
  else $outcome = 'away';
  $homeStrength = manager_normalized_strength($homeStrength);
  $awayStrength = manager_normalized_strength($awayStrength);
  $edge = clamp_float(abs($homeStrength - $awayStrength) / max(1.0, ($homeStrength + $awayStrength) / 2.0), 0.0, 0.75);
  if ($outcome === 'draw') {
    $score = mt_rand(1, 100) <= 45 ? 0 : (mt_rand(1, 100) <= 78 ? 1 : 2);
    return [$score, $score];
  }
  $loserGoals = mt_rand(1, 100) <= 52 ? 0 : (mt_rand(1, 100) <= 82 ? 1 : 2);
  $margin = 1 + (mt_rand(1, 100) <= 18 + (int)round($edge * 35) ? 1 : 0) + (mt_rand(1, 100) <= 5 + (int)round($edge * 12) ? 1 : 0);
  $winnerGoals = min(7, $loserGoals + $margin);
  return $outcome === 'home' ? [$winnerGoals, $loserGoals] : [$loserGoals, $winnerGoals];
}

function playoff_winner(string $home, string $away, int $hg, int $ag, int $homeStrength, int $awayStrength): string {
  if ($hg > $ag) return $home;
  if ($ag > $hg) return $away;
  $odds = manager_match_probabilities($homeStrength, $awayStrength);
  $homeChance = clamp_float($odds['home'] / max(1, $odds['home'] + $odds['away']), 0.25, 0.75);
  return rand_float() <= $homeChance ? $home : $away;
}

function playoff_penalty_score(string $winner, string $home, string $away): array {
  $loserPens = mt_rand(2, 5);
  $winnerPens = $loserPens + 1;
  return $winner === $home ? [$winnerPens, $loserPens] : [$loserPens, $winnerPens];
}

function apply_manager_result(array &$season, string $home, string $away, int $hg, int $ag): void {
  foreach ([[$home, $hg, $ag], [$away, $ag, $hg]] as $row) {
    [$user, $gf, $ga] = $row;
    if (!isset($season['table'][$user])) continue;
    $season['table'][$user]['played']++;
    $season['table'][$user]['gf'] += $gf;
    $season['table'][$user]['ga'] += $ga;
    $season['table'][$user]['gd'] = $season['table'][$user]['gf'] - $season['table'][$user]['ga'];
    if ($gf > $ga) {
      $season['table'][$user]['won']++;
      $season['table'][$user]['points'] += 3;
    } elseif ($gf === $ga) {
      $season['table'][$user]['drawn']++;
      $season['table'][$user]['points'] += 1;
    } else {
      $season['table'][$user]['lost']++;
    }
  }
}

function sort_manager_table(array &$season): void {
  uasort($season['table'], function ($a, $b) {
    return [$b['points'], $b['gd'], $b['gf'], $a['team']['name'] ?? ''] <=> [$a['points'], $a['gd'], $a['gf'], $b['team']['name'] ?? ''];
  });
}

function manager_ranked_rows(array $season): array {
  $rows = array_values($season['table'] ?? []);
  usort($rows, function ($a, $b) {
    return [$b['points'], $b['gd'], $b['gf'], $a['team']['name'] ?? ''] <=> [$a['points'], $a['gd'], $a['gf'], $b['team']['name'] ?? ''];
  });
  return $rows;
}

function manager_round_seq(array $season): int {
  return count($season['rounds'] ?? []) + 1;
}

function manager_add_playoff_round(array &$season, string $label, array $fixtures): void {
  $roundFixtures = [];
  foreach ($fixtures as $fixture) {
    $fixture['status'] = 'pending';
    $fixture['score'] = null;
    $fixture['phase'] = 'playoff';
    $roundFixtures[] = $fixture;
  }
  $season['rounds'][] = [
    'round' => $label,
    'phase' => 'playoff',
    'fixtures' => $roundFixtures,
  ];
}

function start_manager_playoffs(array &$season): void {
  if (!empty($season['leagueResolved'])) return;
  if (!empty($season['playoffStarted'])) return;
  sort_manager_table($season);
  $rows = manager_ranked_rows($season);
  $rules = manager_promotion_rules((int)($season['divisionLevel'] ?? 5));
  $directCount = (int)$rules['direct'];
  $playoffStart = (int)$rules['playoffStart'];
  $playoffEnd = (int)$rules['playoffEnd'];
  $direct = [];
  for ($i = 0; $i < $directCount && isset($rows[$i]); $i++) $direct[] = (string)$rows[$i]['user'];
  $season['promotion'] = [
    'direct' => $direct,
    'playoff' => null,
  ];
  if ($playoffStart <= 0 || $playoffEnd <= 0 || count($rows) < $playoffEnd) {
    $season['status'] = 'finished';
    finalize_manager_season($season);
    return;
  }
  $season['status'] = 'playoff';
  $season['playoffStarted'] = true;
  $season['playoff'] = [
    'stage' => $playoffEnd - $playoffStart + 1 === 6 ? 'qf' : 'sf',
    'direct' => $direct,
    'ranked' => array_map(fn($row) => ['user' => $row['user'], 'team' => $row['team']], array_slice($rows, 0, $playoffEnd)),
    'rules' => $rules,
    'winners' => [],
  ];
  if ($playoffEnd - $playoffStart + 1 === 6) {
    manager_add_playoff_round($season, 'PLAYOFF QF', [
      ['home' => (string)$rows[4]['user'], 'away' => (string)$rows[5]['user'], 'playoffKey' => 'qf_5_6'],
      ['home' => (string)$rows[3]['user'], 'away' => (string)$rows[6]['user'], 'playoffKey' => 'qf_4_7'],
    ]);
    return;
  }
  manager_add_playoff_round($season, 'PLAYOFF SF', [
    ['home' => (string)$rows[$playoffStart - 1]['user'], 'away' => (string)$rows[$playoffEnd - 1]['user'], 'playoffKey' => 'sf_a'],
    ['home' => (string)$rows[$playoffStart]['user'], 'away' => (string)$rows[$playoffEnd - 2]['user'], 'playoffKey' => 'sf_b'],
  ]);
}

function manager_has_unplayed_phase(array $season, string $phase): bool {
  foreach (($season['rounds'] ?? []) as $round) {
    foreach (($round['fixtures'] ?? []) as $fixture) {
      $fixturePhase = (string)($fixture['phase'] ?? 'league');
      if ($fixturePhase !== $phase) continue;
      if (in_array(($fixture['status'] ?? ''), ['pending', 'watching'], true)) return true;
    }
  }
  return false;
}

function manager_playoff_winners(array $season): array {
  $winners = [];
  foreach (($season['rounds'] ?? []) as $round) {
    foreach (($round['fixtures'] ?? []) as $fixture) {
      if (($fixture['phase'] ?? '') !== 'playoff') continue;
      if (($fixture['status'] ?? '') !== 'played') continue;
      $key = (string)($fixture['playoffKey'] ?? '');
      if ($key !== '') $winners[$key] = (string)($fixture['winner'] ?? '');
    }
  }
  return $winners;
}

function advance_manager_playoffs(array &$season): void {
  if (empty($season['playoffStarted'])) return;
  if (manager_has_unplayed_phase($season, 'playoff')) return;
  $rows = manager_ranked_rows($season);
  $winners = manager_playoff_winners($season);
  $stage = (string)($season['playoff']['stage'] ?? 'qf');
  if ($stage === 'qf' && !empty($winners['qf_5_6']) && !empty($winners['qf_4_7'])) {
    $season['playoff']['stage'] = 'sf';
    manager_add_playoff_round($season, 'PLAYOFF SF', [
      ['home' => (string)$rows[1]['user'], 'away' => $winners['qf_5_6'], 'playoffKey' => 'sf_a'],
      ['home' => (string)$rows[2]['user'], 'away' => $winners['qf_4_7'], 'playoffKey' => 'sf_b'],
    ]);
    return;
  }
  if ($stage === 'sf' && !empty($winners['sf_a']) && !empty($winners['sf_b'])) {
    $season['playoff']['stage'] = 'final';
    manager_add_playoff_round($season, 'PLAYOFF FINAL', [
      ['home' => $winners['sf_a'], 'away' => $winners['sf_b'], 'playoffKey' => 'final'],
    ]);
    return;
  }
  if ($stage === 'final' && !empty($winners['final'])) {
    $season['promotion']['playoff'] = $winners['final'];
    $season['playoff']['winner'] = $winners['final'];
    finalize_manager_season($season);
  }
}

function finalize_manager_season(array &$season): void {
  if (!empty($season['leagueResolved']) && ($season['status'] ?? '') === 'finished') return;
  sort_manager_table($season);
  $rows = array_values($season['table']);
  $level = clamp_int((int)($season['divisionLevel'] ?? 5), 1, 5);
  if (empty($season['shadowRelegationFromAbove'])) {
    $season['shadowRelegationFromAbove'] = manager_shadow_relegation_report($level, (string)($season['id'] ?? ''));
  }
  $directRaw = $season['promotion']['direct'] ?? ($rows[0]['user'] ?? '');
  $direct = is_array($directRaw) ? array_map('strval', $directRaw) : [(string)$directRaw];
  $playoff = (string)($season['promotion']['playoff'] ?? '');
  $rules = manager_promotion_rules($level);
  $relegationCount = max(0, (int)($rules['relegation'] ?? 0));
  $relegated = [];
  if ($relegationCount > 0) {
    foreach (array_slice($rows, -$relegationCount) as $row) {
      $id = (string)($row['user'] ?? '');
      if ($id !== '') $relegated[$id] = true;
    }
  }
  $result = [];
  foreach ($rows as $row) {
    $id = (string)$row['user'];
    $result[$id] = (in_array($id, $direct, true) || ($playoff !== '' && $id === $playoff)) ? 'promoted' : (isset($relegated[$id]) ? 'relegated' : 'stayed');
  }
  $season['leagueResolved'] = true;
  $season['status'] = manager_has_unplayed_phase($season, 'cup') ? 'active' : 'finished';
  $season['outcome'] = $result;
}

function manager_award_season_trophies(array &$state, array &$season): void {
  $year = manager_season_year($season, $state);
  $seasonId = (string)($season['id'] ?? '');
  $division = (string)($season['divisionName'] ?? manager_division_name((int)($season['divisionLevel'] ?? 5)));
  if (empty($season['trophiesApplied'])) {
    $rows = manager_ranked_rows($season);
    if (!empty($rows[0])) {
      $championId = (string)$rows[0]['user'];
      $championTeam = manager_side_preview($state, $season, $championId)['team'];
      add_team_trophy($state, $championTeam, [
        'seasonId' => $seasonId,
        'seasonYear' => $year,
        'kind' => 'league_win',
        'title' => $division . ' Champions',
        'detail' => 'League winners',
      ]);
    }
    $level = clamp_int((int)($season['divisionLevel'] ?? 5), 1, 5);
    $toDivision = $level > 1 ? manager_division_name($level - 1) : '';
    foreach (($season['outcome'] ?? []) as $id => $outcome) {
      if ($outcome !== 'promoted' || $toDivision === '') continue;
      $team = manager_side_preview($state, $season, (string)$id)['team'];
      $method = ((string)($season['promotion']['playoff'] ?? '') === (string)$id) ? 'playoff' : 'direct';
      add_team_trophy($state, $team, [
        'seasonId' => $seasonId,
        'seasonYear' => $year,
        'kind' => 'promotion',
        'title' => 'Promoted to ' . $toDivision,
        'detail' => $method === 'playoff' ? 'Promotion via playoff' : 'Direct promotion',
      ]);
    }
    $cups = $season['cups'] ?? [];
    if (!$cups && !empty($season['cup'])) $cups = ['legacy' => $season['cup']];
    foreach ($cups as $cupKey => $cup) {
      $cupName = (string)($cup['name'] ?? 'Cup');
      $winner = (string)($cup['winner'] ?? '');
      $runnerUp = (string)($cup['runnerUp'] ?? '');
      if ($winner !== '') {
        $team = manager_side_preview($state, $season, $winner)['team'];
        add_team_trophy($state, $team, [
          'seasonId' => $seasonId,
          'seasonYear' => $year,
          'kind' => 'cup_win_' . (string)$cupKey,
          'title' => $cupName . ' Winners',
          'detail' => 'Cup winners',
        ]);
      }
      if ($runnerUp !== '') {
        $team = manager_side_preview($state, $season, $runnerUp)['team'];
        add_team_trophy($state, $team, [
          'seasonId' => $seasonId,
          'seasonYear' => $year,
          'kind' => 'cup_runner_up_' . (string)$cupKey,
          'title' => $cupName . ' Runners-up',
          'detail' => 'Cup final runners-up',
        ]);
      }
    }
    $season['trophiesApplied'] = true;
  }
  if (empty($season['statTrophiesApplied'])) {
    $scorers = manager_stat_rows_for_phase($season, 'scorers', 'league', 'goals');
    manager_sort_scorer_rows($scorers);
    $topGoals = !empty($scorers[0]) ? (int)($scorers[0]['goals'] ?? 0) : 0;
    if ($topGoals > 0) {
      foreach ($scorers as $scorer) {
        if ((int)($scorer['goals'] ?? 0) !== $topGoals) break;
        $player = trim((string)($scorer['player'] ?? 'Player')) ?: 'Player';
        $team = is_array($scorer['team'] ?? null) ? $scorer['team'] : [];
        if (team_trophy_key($team) === '') $team = manager_side_preview($state, $season, (string)($scorer['user'] ?? ''))['team'];
        add_team_trophy($state, $team, [
          'seasonId' => $seasonId,
          'seasonYear' => $year,
          'kind' => 'league_top_scorer',
          'visual' => 'medal',
          'title' => 'Golden Boot - ' . $player,
          'detail' => $division . ' top scorer | ' . $topGoals . ' goals',
        ]);
      }
    }
    $season['statTrophiesApplied'] = true;
  }
}

function manager_job_offer_roll(string $seed, int $max = 100): int {
  $max = max(1, $max);
  return (int)(hexdec(substr(hash('sha256', $seed), 0, 8)) % $max);
}

function manager_job_offer_plan(array $season, string $user, string $outcome, int $rank, int $teams): ?array {
  $level = clamp_int((int)($season['divisionLevel'] ?? 5), 1, 5);
  $playoffWinner = (string)($season['promotion']['playoff'] ?? '') === $user;
  $directRaw = $season['promotion']['direct'] ?? [];
  $direct = is_array($directRaw) ? array_map('strval', $directRaw) : [(string)$directRaw];
  if ($outcome === 'promoted' && $level > 1) {
    if ($rank === 1) return ['targetLevel' => $level - 1, 'chance' => 60, 'reason' => 'league_title', 'offerCount' => 2];
    if ($playoffWinner) return ['targetLevel' => $level - 1, 'chance' => 24, 'reason' => 'playoff_promotion', 'offerCount' => 1];
    if (in_array($user, $direct, true)) return ['targetLevel' => $level - 1, 'chance' => 36, 'reason' => 'direct_promotion', 'offerCount' => 1];
    return ['targetLevel' => $level - 1, 'chance' => 20, 'reason' => 'promotion', 'offerCount' => 1];
  }
  if ($outcome === 'stayed') {
    $rules = manager_promotion_rules($level);
    $playoffEnd = (int)($rules['playoffEnd'] ?? 0);
    $topHalf = $rank <= (int)ceil(max(1, $teams) / 2);
    $nearPlayoff = $playoffEnd > 0 && $rank <= $playoffEnd + 2;
    $chance = $nearPlayoff ? 18 : ($topHalf ? 12 : 7);
    return ['targetLevel' => $level, 'chance' => $chance, 'reason' => 'same_division', 'offerCount' => 1];
  }
  if ($outcome === 'promoted' && $level === 1 && $rank === 1) {
    return ['targetLevel' => 1, 'chance' => 14, 'reason' => 'top_flight_title', 'offerCount' => 1];
  }
  return null;
}

function manager_job_offer_reason_label(string $reason): string {
  return match ($reason) {
    'league_title' => 'League title',
    'direct_promotion' => 'Direct promotion',
    'playoff_promotion' => 'Playoff promotion',
    'promotion' => 'Promotion',
    'top_flight_title' => 'Title-winning season',
    default => 'Solid season',
  };
}

function manager_job_offer_candidates(array $state, array $season, int $targetLevel, string $user, string $seed): array {
  $gameId = manager_game_id($season);
  $blocked = manager_game_human_team_keys($state, $gameId, $season['humanManagers'] ?? [], $season);
  $candidates = [];
  foreach (manager_division_teams($targetLevel) as $team) {
    if (manager_team_key_blocked($team, $blocked)) continue;
    $team['managerDivisionLevel'] = $targetLevel;
    $team['managerDivisionName'] = manager_division_name($targetLevel);
    $candidates[] = $team;
  }
  usort($candidates, function ($a, $b) use ($seed) {
    $ak = manager_job_offer_roll($seed . '|team|' . manager_team_unique_key($a), 1000000000);
    $bk = manager_job_offer_roll($seed . '|team|' . manager_team_unique_key($b), 1000000000);
    return $ak <=> $bk;
  });
  return $candidates;
}

function manager_ensure_job_offers(array &$state, array &$season): void {
  if (($season['status'] ?? '') !== 'finished' || !empty($season['jobOffersGenerated'])) return;
  $seasonId = (string)($season['id'] ?? '');
  $seasonYear = manager_season_year($season, $state);
  $fromLevel = clamp_int((int)($season['divisionLevel'] ?? 5), 1, 5);
  $teams = count($season['table'] ?? []) ?: manager_division_target_count($fromLevel);
  $season['jobOffers'] = is_array($season['jobOffers'] ?? null) ? $season['jobOffers'] : [];
  foreach (($season['humanManagers'] ?? []) as $rawName) {
    $user = clean_name((string)$rawName);
    if ($user === '' || !isset($state['users'][$user])) continue;
    $outcome = (string)($season['outcome'][$user] ?? '');
    $rank = manager_user_rank_in_season($season, $user);
    $plan = manager_job_offer_plan($season, $user, $outcome, $rank, $teams);
    if (!$plan) continue;
    $chance = clamp_int((int)($plan['chance'] ?? 0), 0, 100);
    $seed = $seasonId . '|' . $seasonYear . '|' . $user . '|job_offer|' . (string)($plan['reason'] ?? '');
    if (manager_job_offer_roll($seed, 100) >= $chance) continue;
    $targetLevel = clamp_int((int)($plan['targetLevel'] ?? $fromLevel), 1, 5);
    $candidates = manager_job_offer_candidates($state, $season, $targetLevel, $user, $seed);
    $count = min(max(1, (int)($plan['offerCount'] ?? 1)), count($candidates));
    for ($i = 0; $i < $count; $i++) {
      $team = $candidates[$i];
      $offerId = 'job_' . substr(hash('sha1', $seed . '|' . manager_team_unique_key($team)), 0, 12);
      $season['jobOffers'][$user][] = [
        'id' => $offerId,
        'user' => $user,
        'status' => 'pending',
        'reason' => (string)($plan['reason'] ?? 'same_division'),
        'reasonLabel' => manager_job_offer_reason_label((string)($plan['reason'] ?? 'same_division')),
        'chancePct' => $chance,
        'rank' => $rank,
        'fromLevel' => $fromLevel,
        'fromDivision' => manager_division_name($fromLevel),
        'targetLevel' => $targetLevel,
        'targetDivision' => manager_division_name($targetLevel),
        'team' => $team,
        'createdAt' => time(),
      ];
    }
  }
  $season['jobOffersGenerated'] = true;
}

function public_manager_job_offers(array $season, string $viewer): array {
  $viewer = clean_name($viewer);
  if ($viewer === '') return [];
  $offers = is_array($season['jobOffers'][$viewer] ?? null) ? $season['jobOffers'][$viewer] : [];
  $out = [];
  foreach ($offers as $offer) {
    if (!is_array($offer)) continue;
    $status = (string)($offer['status'] ?? 'pending');
    if ($status === 'rejected' || $status === 'superseded') continue;
    $team = is_array($offer['team'] ?? null) ? $offer['team'] : [];
    $out[] = [
      'id' => (string)($offer['id'] ?? ''),
      'status' => $status,
      'reason' => (string)($offer['reason'] ?? ''),
      'reasonLabel' => (string)($offer['reasonLabel'] ?? manager_job_offer_reason_label((string)($offer['reason'] ?? ''))),
      'rank' => (int)($offer['rank'] ?? 0),
      'fromDivision' => (string)($offer['fromDivision'] ?? ''),
      'targetLevel' => (int)($offer['targetLevel'] ?? 5),
      'targetDivision' => (string)($offer['targetDivision'] ?? ''),
      'teamUser' => manager_cpu_id((string)($team['id'] ?? $team['sourceId'] ?? '')),
      'team' => [
        'id' => (string)($team['id'] ?? ''),
        'sourceId' => (string)($team['sourceId'] ?? $team['id'] ?? ''),
        'name' => (string)($team['name'] ?? 'Team'),
        'shirt' => (string)($team['shirt'] ?? '#ffffff'),
        'trim' => (string)($team['trim'] ?? '#111111'),
        'baseRating' => (int)($team['baseRating'] ?? team_base_rating((string)($team['id'] ?? ''))),
      ],
      'createdAt' => (int)($offer['createdAt'] ?? 0),
      'decidedAt' => (int)($offer['decidedAt'] ?? 0),
    ];
  }
  return $out;
}

function manager_apply_job_offer_to_user(array &$state, string $user, array $offer): void {
  if (!isset($state['users'][$user]) || !is_array($state['users'][$user])) return;
  $team = is_array($offer['team'] ?? null) ? $offer['team'] : [];
  $teamId = (string)($team['id'] ?? $team['sourceId'] ?? '');
  if ($teamId === '') return;
  $level = clamp_int((int)($offer['targetLevel'] ?? $state['users'][$user]['managerDivisionLevel'] ?? 5), 1, 5);
  reset_user_manager_transfer_state($state, $user);
  $state['users'][$user]['teamId'] = $teamId;
  $state['users'][$user]['teamName'] = (string)($team['name'] ?? strtoupper($user));
  $state['users'][$user]['shirt'] = (string)($team['shirt'] ?? '#ffffff');
  $state['users'][$user]['trim'] = (string)($team['trim'] ?? '#111111');
  $state['users'][$user]['managerDivisionLevel'] = $level;
  ensure_user_squad($state['users'][$user]);
  ensure_manager_finance($state['users'][$user]);
}

function apply_manager_progression(array &$state, array &$season): void {
  manager_award_season_trophies($state, $season);
  if (!empty($season['progressionApplied'])) {
    manager_ensure_job_offers($state, $season);
    return;
  }
  $growthAwards = manager_apply_player_growth_awards($state, $season);
  $changes = [];
  $seasonLevel = clamp_int((int)($season['divisionLevel'] ?? 5), 1, 5);
  foreach (($season['outcome'] ?? []) as $name => $outcome) {
    if (!isset($state['users'][$name])) continue;
    ensure_manager_finance($state['users'][$name]);
    $before = $seasonLevel;
    $level = $before;
    $rank = manager_user_rank_in_season($season, (string)$name);
    $teams = count($season['table'] ?? []);
    $award = manager_season_finance_award($before, $rank, $teams, (string)$outcome);
    $state['users'][$name]['managerFinance']['seasonAwards'] += $award;
    $state['users'][$name]['managerFinance']['balance'] += $award;
    $debtStatus = manager_update_debt_status_after_season($state, $season, (string)$name);
    $ageing = manager_age_user_squad_after_season($state, $season, (string)$name);
    if ($outcome === 'promoted') $level = max(1, $level - 1);
    elseif ($outcome === 'relegated') $level = min(5, $level + 1);
    $state['users'][$name]['managerDivisionLevel'] = $level;
    $changes[$name] = ['outcome' => $outcome, 'fromLevel' => $before, 'toLevel' => $level, 'seasonAward' => $award, 'debtStatus' => $debtStatus, 'ageing' => $ageing];
  }
  manager_ensure_job_offers($state, $season);
  $season['progressionApplied'] = true;
  manager_log($state, 'apply_progression', ['seasonId' => (string)($season['id'] ?? ''), 'changes' => $changes, 'playerGrowth' => $growthAwards]);
}

function manager_award_finished_season_trophies(array &$state): void {
  if (empty($state['managerSeasons']) || !is_array($state['managerSeasons'])) return;
  foreach ($state['managerSeasons'] as &$season) {
    if (!is_array($season) || ($season['status'] ?? '') !== 'finished') continue;
    manager_award_season_trophies($state, $season);
  }
  unset($season);
}

function manager_has_unplayed(array $season): bool {
  foreach (($season['rounds'] ?? []) as $round) {
    foreach (($round['fixtures'] ?? []) as $fixture) {
      if (in_array(($fixture['status'] ?? ''), ['pending', 'watching'], true)) return true;
    }
  }
  return false;
}

function manager_has_watching(array $season): bool {
  foreach (($season['rounds'] ?? []) as $round) {
    foreach (($round['fixtures'] ?? []) as $fixture) {
      if (($fixture['status'] ?? '') === 'watching') return true;
    }
  }
  return false;
}

function manager_next_user_fixture(array $season, string $user): ?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'] ?? '') === $user || ($fixture['away'] ?? '') === $user) {
        return ['roundIndex' => $roundIndex, 'fixtureIndex' => $fixtureIndex, 'fixture' => $fixture];
      }
    }
  }
  return null;
}

function add_manager_finance_for_result(array &$state, string $name, int $gf, int $ga, string $phase = 'league', bool $homeGame = true, array $season = [], bool $cupWinner = false): void {
  if (!isset($state['users'][$name])) return;
  ensure_user_squad($state['users'][$name]);
  ensure_manager_finance($state['users'][$name]);
  $level = clamp_int((int)($state['users'][$name]['managerDivisionLevel'] ?? 5), 1, 5);
  if ($phase === 'league' && !empty($season)) {
    $level = clamp_int((int)($season['divisionLevel'] ?? $level), 1, 5);
  }
  $finance = $state['users'][$name]['managerFinance'] ?? [];
  $rank = $season ? manager_user_rank_in_season($season, $name) : 12;
  $teams = count($season['table'] ?? []) ?: 24;
  $breakdown = manager_match_income_breakdown($level, $gf, $ga, $phase, (int)($finance['arenaCapacity'] ?? manager_base_arena_capacity($level)), $rank, $teams, $homeGame, $cupWinner, (int)($finance['merchLevel'] ?? 0));
	  $income = (int)($breakdown['gateIncome'] ?? 0) + (int)($breakdown['merchIncome'] ?? 0) + (int)($breakdown['bonusIncome'] ?? 0) + (int)($breakdown['cupIncome'] ?? 0);
	  $wages = squad_wages($state['users'][$name]['squad'] ?? []);
	  $balanceBeforeMatch = (int)($state['users'][$name]['managerFinance']['balance'] ?? 0);
	  $loanInterestBefore = (int)($state['users'][$name]['managerFinance']['loanInterestPaid'] ?? 0);
	  $financeTopUp = 0;
	  foreach ($breakdown as $key => $amount) {
	    if ($key === 'attendance') continue;
	    $state['users'][$name]['managerFinance'][$key] = (int)($state['users'][$name]['managerFinance'][$key] ?? 0) + (int)$amount;
  }
  if ($homeGame) {
    $attendance = (int)($breakdown['attendance'] ?? 0);
    $state['users'][$name]['managerFinance']['arenaLastAttendance'] = $attendance;
    $state['users'][$name]['managerFinance']['arenaTotalAttendance'] = (int)($state['users'][$name]['managerFinance']['arenaTotalAttendance'] ?? 0) + $attendance;
    $state['users'][$name]['managerFinance']['arenaHomeMatches'] = (int)($state['users'][$name]['managerFinance']['arenaHomeMatches'] ?? 0) + 1;
  }
  $state['users'][$name]['managerFinance']['income'] += $income;
  $state['users'][$name]['managerFinance']['wages'] += $wages;
  $state['users'][$name]['managerFinance']['balance'] += $income - $wages;
  $hadLoan = is_array($state['users'][$name]['managerFinance']['loan'] ?? null);
  $loanPayment = manager_apply_loan_payment($state['users'][$name]['managerFinance']);
  if ($phase === 'league' && ($hadLoan || $loanPayment > 0)) {
    $netAfterLoan = (int)($state['users'][$name]['managerFinance']['balance'] ?? 0) - $balanceBeforeMatch;
    $targetNet = manager_league_net_target_with_loan($gf, $ga);
    if ($netAfterLoan < $targetNet) {
      $topUp = (int)ceil(($targetNet - $netAfterLoan) / 500) * 500;
	      $state['users'][$name]['managerFinance']['bonusIncome'] = (int)($state['users'][$name]['managerFinance']['bonusIncome'] ?? 0) + $topUp;
	      $state['users'][$name]['managerFinance']['income'] = (int)($state['users'][$name]['managerFinance']['income'] ?? 0) + $topUp;
	      $state['users'][$name]['managerFinance']['balance'] = (int)($state['users'][$name]['managerFinance']['balance'] ?? 0) + $topUp;
	      $financeTopUp += $topUp;
	      manager_log($state, 'league_loan_net_floor', [
	        'user' => $name,
	        'level' => $level,
        'score' => [$gf, $ga],
        'incomeBeforeFloor' => $income,
        'wages' => $wages,
        'loanPayment' => $loanPayment,
        'netBeforeFloor' => $netAfterLoan,
        'targetNet' => $targetNet,
        'topUp' => $topUp,
      ]);
    }
  } elseif ($phase === 'league' && $gf > $ga) {
    $netAfterLoan = (int)($state['users'][$name]['managerFinance']['balance'] ?? 0) - $balanceBeforeMatch;
    if ($netAfterLoan <= 0) {
      $topUp = max(500, (int)ceil((1 - $netAfterLoan) / 500) * 500);
	      $state['users'][$name]['managerFinance']['bonusIncome'] = (int)($state['users'][$name]['managerFinance']['bonusIncome'] ?? 0) + $topUp;
	      $state['users'][$name]['managerFinance']['income'] = (int)($state['users'][$name]['managerFinance']['income'] ?? 0) + $topUp;
	      $state['users'][$name]['managerFinance']['balance'] = (int)($state['users'][$name]['managerFinance']['balance'] ?? 0) + $topUp;
	      $financeTopUp += $topUp;
	      manager_log($state, 'league_win_income_floor', [
	        'user' => $name,
	        'level' => $level,
        'score' => [$gf, $ga],
        'incomeBeforeFloor' => $income,
        'wages' => $wages,
        'loanPayment' => $loanPayment,
        'netBeforeFloor' => $netAfterLoan,
        'topUp' => $topUp,
      ]);
	    }
	  }
	  $balanceAfterMatch = (int)($state['users'][$name]['managerFinance']['balance'] ?? 0);
	  $state['users'][$name]['managerFinance']['lastMatchFinance'] = [
	    'phase' => $phase,
	    'level' => $level,
	    'homeGame' => $homeGame,
	    'score' => [$gf, $ga],
	    'result' => $gf > $ga ? 'win' : ($gf === $ga ? 'draw' : 'loss'),
	    'attendance' => (int)($breakdown['attendance'] ?? 0),
	    'gateIncome' => (int)($breakdown['gateIncome'] ?? 0),
	    'merchIncome' => (int)($breakdown['merchIncome'] ?? 0),
	    'bonusIncome' => (int)($breakdown['bonusIncome'] ?? 0) + $financeTopUp,
	    'cupIncome' => (int)($breakdown['cupIncome'] ?? 0),
	    'floorTopUp' => $financeTopUp,
	    'income' => $income + $financeTopUp,
	    'wages' => $wages,
	    'loanPayment' => $loanPayment,
	    'loanInterestPaid' => max(0, (int)($state['users'][$name]['managerFinance']['loanInterestPaid'] ?? 0) - $loanInterestBefore),
	    'loanDebtAfter' => is_array($state['users'][$name]['managerFinance']['loan'] ?? null) ? max(0, (int)($state['users'][$name]['managerFinance']['loan']['remaining'] ?? 0)) : 0,
	    'expenses' => $wages + $loanPayment,
	    'net' => $balanceAfterMatch - $balanceBeforeMatch,
	    'balanceBefore' => $balanceBeforeMatch,
	    'balanceAfter' => $balanceAfterMatch,
	    'createdAt' => time(),
	  ];
	  if ($hadLoan && $loanPayment > 0 && !is_array($state['users'][$name]['managerFinance']['loan'] ?? null)) {
	    manager_log($state, 'loan_paid_off', ['user' => $name, 'payment' => $loanPayment, 'balance' => (int)($state['users'][$name]['managerFinance']['balance'] ?? 0)]);
	  }
}

function manager_tick_arena_builds_for_round(array &$state, array &$season, int $roundIndex): void {
  if (!isset($season['rounds'][$roundIndex]) || !is_array($season['rounds'][$roundIndex])) return;
  if (!empty($season['rounds'][$roundIndex]['arenaBuildTicked'])) return;
  foreach (($season['rounds'][$roundIndex]['fixtures'] ?? []) as $fixture) {
    if (($fixture['status'] ?? '') !== 'played') return;
  }
  foreach (($season['humanManagers'] ?? []) as $name) {
    $name = clean_name((string)$name);
    if ($name === '' || !isset($state['users'][$name])) continue;
    ensure_manager_finance($state['users'][$name]);
    $build = $state['users'][$name]['managerFinance']['arenaBuild'] ?? null;
    if (!is_array($build)) continue;
    $left = max(0, (int)($build['roundsLeft'] ?? 0) - 1);
    $build['roundsLeft'] = $left;
    if ($left <= 0) {
      $state['users'][$name]['managerFinance']['arenaLevel'] = clamp_int((int)($build['level'] ?? 0), 0, 10);
      $state['users'][$name]['managerFinance']['arenaCapacity'] = max(
        manager_base_arena_capacity((int)($state['users'][$name]['managerDivisionLevel'] ?? 5)),
        (int)($state['users'][$name]['managerFinance']['arenaCapacity'] ?? 0) + max(0, (int)($build['seats'] ?? 0))
      );
      $state['users'][$name]['managerFinance']['arenaBuild'] = null;
      manager_log($state, 'arena_expansion_complete', ['seasonId' => (string)($season['id'] ?? ''), 'user' => $name, 'level' => (int)($state['users'][$name]['managerFinance']['arenaLevel'] ?? 0), 'capacity' => (int)($state['users'][$name]['managerFinance']['arenaCapacity'] ?? 0)]);
    } else {
      $state['users'][$name]['managerFinance']['arenaBuild'] = $build;
    }
  }
  $season['rounds'][$roundIndex]['arenaBuildTicked'] = true;
}

function add_manager_scorers(array &$season, string $sideId, array $sideData, int $goals, ?array $match, string $matchSide, array $fixture = []): void {
  $context = manager_competition_context_from_fixture($fixture);
  $added = 0;
  $matchGoals = is_array($match) ? ($match['engine']['goalScorers'] ?? []) : [];
  foreach ($matchGoals as $goal) {
    if (($goal['side'] ?? '') !== $matchSide) continue;
    add_scorer($season, $sideId, $sideData['team'], ['id' => (string)($goal['scorerId'] ?? ''), 'name' => (string)($goal['scorer'] ?? 'Unknown')], 1, !empty($goal['penalty']) ? 1 : 0, $context);
    if (empty($goal['penalty']) && is_string($goal['assist'] ?? null) && (string)$goal['assist'] !== '') {
      add_assist($season, $sideId, $sideData['team'], ['name' => (string)$goal['assist']], 1, $context);
    }
    $added++;
  }
  for ($i = $added; $i < $goals; $i++) {
    $scorer = pick_goal_scorer($sideData['squad']['starters'] ?? []);
    if ($scorer) {
      $isPenalty = rand_float() < 0.11;
      add_scorer($season, $sideId, $sideData['team'], $scorer, 1, $isPenalty ? 1 : 0, $context);
      if (!$isPenalty && rand_float() < 0.82) {
        $assist = pick_assist_player($sideData['squad']['starters'] ?? [], $scorer);
        if ($assist) add_assist($season, $sideId, $sideData['team'], $assist, 1, $context);
      }
    }
  }
}

function complete_manager_fixture(array &$state, array &$season, int $roundIndex, int $fixtureIndex, array $score, ?array $match = null): void {
  if (empty($season['rounds'][$roundIndex]['fixtures'][$fixtureIndex])) return;
  $fixture =& $season['rounds'][$roundIndex]['fixtures'][$fixtureIndex];
  if (($fixture['status'] ?? '') === 'played') return;
  $home = (string)($fixture['home'] ?? '');
  $away = (string)($fixture['away'] ?? '');
  $hg = max(0, (int)($score[0] ?? 0));
  $ag = max(0, (int)($score[1] ?? 0));
  $homeData = manager_side_data($state, $season, $home);
  $awayData = manager_side_data($state, $season, $away);
  manager_lock_fixture_form_for_side($season, $fixture, $home, $homeData, $roundIndex, $fixtureIndex);
  manager_lock_fixture_form_for_side($season, $fixture, $away, $awayData, $roundIndex, $fixtureIndex);
  $homeData = manager_apply_fixture_form_to_side_data($season, $fixture, $home, $homeData, $roundIndex, $fixtureIndex);
  $awayData = manager_apply_fixture_form_to_side_data($season, $fixture, $away, $awayData, $roundIndex, $fixtureIndex);
  $homeData = manager_apply_league_form_to_side_data($season, $home, $homeData);
  $awayData = manager_apply_league_form_to_side_data($season, $away, $awayData);
  $phase = (string)($fixture['phase'] ?? 'league');
  $cupStage = (string)($fixture['cupStage'] ?? 'knockout');
  $cupTableStage = $phase === 'cup' && in_array($cupStage, ['group', 'league'], true);
  $homeMatchStrength = (int)($homeData['matchStrength'] ?? $homeData['strength'] ?? 0);
  $awayMatchStrength = (int)($awayData['matchStrength'] ?? $awayData['strength'] ?? 0);
  $cupBonusWinner = '';
  if ($phase === 'playoff' || ($phase === 'cup' && !$cupTableStage)) {
    $winner = playoff_winner($home, $away, $hg, $ag, $homeMatchStrength, $awayMatchStrength);
    $fixture['winner'] = $winner;
    if ($hg === $ag) {
      $fixture['decidedBy'] = 'PEN';
      $fixture['penaltyScore'] = playoff_penalty_score($winner, $home, $away);
    }
  } elseif ($cupTableStage && $cupStage === 'group' && $hg === $ag && !empty($season['cups'][(string)($fixture['cupKey'] ?? '')]['groupPenaltyBonus'])) {
    $cupBonusWinner = playoff_winner($home, $away, $hg, $ag, $homeMatchStrength, $awayMatchStrength);
    $fixture['winner'] = $cupBonusWinner;
    $fixture['decidedBy'] = 'PEN BONUS';
    $fixture['penaltyScore'] = playoff_penalty_score($cupBonusWinner, $home, $away);
  }
  $fixture['status'] = 'played';
  $fixture['score'] = [$hg, $ag];
  $fixture['playedAt'] = time();
  unset($fixture['matchId']);
  if ($phase === 'cup') {
    manager_log($state, 'cup_fixture_played', [
      'score' => [$hg, $ag],
      'winner' => (string)($fixture['winner'] ?? ''),
      'decidedBy' => (string)($fixture['decidedBy'] ?? ''),
    ] + manager_round_log_context($season, $roundIndex, $fixtureIndex, $fixture));
  }
  if ($phase === 'league') {
    apply_manager_result($season, $home, $away, $hg, $ag);
    manager_clear_future_daily_forms_for_side($season, $home, $roundIndex, $fixtureIndex);
    manager_clear_future_daily_forms_for_side($season, $away, $roundIndex, $fixtureIndex);
  }
  if ($cupTableStage) {
    manager_cup_apply_result($season, (string)($fixture['cupKey'] ?? 'fa-cup'), $cupStage, (string)($fixture['cupGroup'] ?? 'LEAGUE'), $home, $away, $hg, $ag, $cupBonusWinner);
  }
  add_manager_scorers($season, $home, $homeData, $hg, $match, 'home', $fixture);
  add_manager_scorers($season, $away, $awayData, $ag, $match, 'away', $fixture);
  if (is_array($match) || !empty($homeData['human']) || !empty($awayData['human'])) {
    $report = is_array($match)
      ? manager_build_match_report($season, $fixture, $roundIndex, $fixtureIndex, [$hg, $ag], $match)
      : manager_build_simulated_match_report($season, $fixture, $roundIndex, $fixtureIndex, [$hg, $ag], $homeData, $awayData);
    $fixture['reportId'] = $report['id'];
    manager_store_last_match_cards($season, $home, $away, $report);
    manager_store_match_report($season, $report);
  }
  $financeCupMatch = $phase === 'cup' || $phase === 'playoff';
  $financeWinner = (string)($fixture['winner'] ?? '');
  $homeCupWinner = $financeCupMatch && (($financeWinner !== '' && $financeWinner === $home) || ($financeWinner === '' && $hg > $ag));
  $awayCupWinner = $financeCupMatch && (($financeWinner !== '' && $financeWinner === $away) || ($financeWinner === '' && $ag > $hg));
  if (!empty($homeData['human'])) add_manager_finance_for_result($state, $home, $hg, $ag, $phase, true, $season, $homeCupWinner);
  if (!empty($awayData['human'])) add_manager_finance_for_result($state, $away, $ag, $hg, $phase, false, $season, $awayCupWinner);
  manager_decrement_injuries_for_team($season, $home);
  manager_decrement_injuries_for_team($season, $away);
  manager_decrement_training_for_team($state, $season, $home);
  manager_decrement_training_for_team($state, $season, $away);
  manager_scout_auto_train_bench($state, $season, $home, time());
  manager_scout_auto_train_bench($state, $season, $away, time());
  sort_manager_table($season);
  manager_tick_arena_builds_for_round($state, $season, $roundIndex);
  if ($phase === 'league' && !manager_has_unplayed_phase($season, 'league')) {
    start_manager_playoffs($season);
  } elseif ($phase === 'playoff') {
    advance_manager_playoffs($season);
  } elseif ($phase === 'cup') {
    advance_manager_cup($season, $roundIndex);
  }
  if (!empty($season['leagueResolved']) && !manager_has_unplayed_phase($season, 'cup')) {
    $season['status'] = 'finished';
  }
  if (($season['status'] ?? '') === 'finished') {
    apply_manager_progression($state, $season);
  }
  manager_generate_cpu_market_offers($state, $season, time());
  unset($fixture);
}

function simulate_manager_fixture_headless(array &$state, array &$season, int $roundIndex, int $fixtureIndex): void {
  if (empty($season['rounds'][$roundIndex]['fixtures'][$fixtureIndex])) return;
  $fixture = $season['rounds'][$roundIndex]['fixtures'][$fixtureIndex];
  if (($fixture['status'] ?? '') !== 'pending') return;
  $seasonId = (string)($season['id'] ?? '');
  if ($seasonId === '') return;
  $match = new_manager_watch_match($state, '', $seasonId, $roundIndex, $fixtureIndex, false);
  $match['status'] = 'active';
  begin_kickoff($match, 'home', 0.45);
  $guard = 0;
  while (($match['status'] ?? '') === 'active' && $guard < 260) {
    tick_match_fixed_dt($match, 1.0, false);
    $guard++;
  }
  if (($match['status'] ?? '') !== 'finished') {
    $match['elapsed'] = MATCH_GAME_MINUTES;
    $match['status'] = 'finished';
  }
  complete_manager_fixture($state, $season, $roundIndex, $fixtureIndex, $match['score'] ?? [0, 0], $match);
}

function simulate_manager_round_fixtures(array &$state, array &$season, int $roundIndex, bool $includeHumanFixtures = true): int {
  if (empty($season['rounds'][$roundIndex]['fixtures']) || !is_array($season['rounds'][$roundIndex]['fixtures'])) return 0;
  $simulated = 0;
  $humans = array_flip(array_map('strval', $season['humanManagers'] ?? []));
  foreach (array_keys($season['rounds'][$roundIndex]['fixtures']) as $fixtureIndex) {
    if (empty($season['rounds'][$roundIndex]['fixtures'][$fixtureIndex])) continue;
    $fixture = $season['rounds'][$roundIndex]['fixtures'][$fixtureIndex];
    if (($fixture['status'] ?? '') !== 'pending') continue;
    $home = (string)($fixture['home'] ?? '');
    $away = (string)($fixture['away'] ?? '');
    $hasHuman = isset($humans[$home]) || isset($humans[$away]);
    if (!$includeHumanFixtures && $hasHuman) continue;
    if ($hasHuman) {
      simulate_manager_fixture_headless($state, $season, $roundIndex, (int)$fixtureIndex);
      $simulated++;
      continue;
    }
    $homeData = manager_side_data($state, $season, $home);
    $awayData = manager_side_data($state, $season, $away);
    manager_lock_fixture_form_for_side($season, $fixture, $home, $homeData, $roundIndex, (int)$fixtureIndex);
    manager_lock_fixture_form_for_side($season, $fixture, $away, $awayData, $roundIndex, (int)$fixtureIndex);
    $homeData = manager_apply_fixture_form_to_side_data($season, $fixture, $home, $homeData, $roundIndex, (int)$fixtureIndex);
    $awayData = manager_apply_fixture_form_to_side_data($season, $fixture, $away, $awayData, $roundIndex, (int)$fixtureIndex);
    [$hg, $ag] = manager_fixture_score($season, $home, $away, $homeData, $awayData);
    complete_manager_fixture($state, $season, $roundIndex, (int)$fixtureIndex, [$hg, $ag], null);
    $simulated++;
  }
  sort_manager_table($season);
  return $simulated;
}

function sync_manager_match(array &$state, string $matchId): void {
  if (empty($state['matches'][$matchId]) || empty($state['matches'][$matchId]['managerFixture'])) return;
  $match =& $state['matches'][$matchId];
  if (($match['status'] ?? '') !== 'finished' || !empty($match['managerSynced'])) return;
  $meta = $match['managerFixture'];
  $seasonId = (string)($meta['seasonId'] ?? '');
  if ($seasonId === '' || empty($state['managerSeasons'][$seasonId])) return;
  $roundIndex = (int)($meta['roundIndex'] ?? -1);
  $fixtureIndex = (int)($meta['fixtureIndex'] ?? -1);
  $season =& $state['managerSeasons'][$seasonId];
  $fixtureBefore = $season['rounds'][$roundIndex]['fixtures'][$fixtureIndex] ?? [];
  $hasHumanFixture = is_array($fixtureBefore) && count(manager_fixture_human_users($season, $fixtureBefore)) > 0;
  complete_manager_fixture(
    $state,
    $season,
    $roundIndex,
    $fixtureIndex,
    $match['score'] ?? [0, 0],
    $match
  );
  if ($hasHumanFixture) manager_require_round_continue($season, $roundIndex);
  $simulated = simulate_manager_round_fixtures($state, $season, $roundIndex, count($season['humanManagers'] ?? []) <= 1);
  $season['updated'] = time();
  $fixture = $season['rounds'][$roundIndex]['fixtures'][$fixtureIndex] ?? null;
  $saveUsers = manager_save_users($season);
  $savedBy = clean_name((string)($match['players']['home'] ?? ''));
  if ($savedBy === '' || !in_array($savedBy, $saveUsers, true)) $savedBy = clean_name((string)($match['players']['away'] ?? ''));
  if (is_array($fixture)) manager_autosave_season($state, $season, $fixture, $savedBy);
  manager_log($state, 'watch_match_sync', ['seasonId' => $seasonId, 'roundIndex' => $roundIndex, 'fixtureIndex' => $fixtureIndex, 'simulatedOtherFixtures' => $simulated]);
  $match['managerSynced'] = true;
  unset($season);
}

function sync_manager_matches(array &$state): void {
  foreach (array_keys($state['matches'] ?? []) as $matchId) {
    sync_manager_match($state, (string)$matchId);
  }
}

function tick_state_match(array &$state, string $matchId): void {
  if (empty($state['matches'][$matchId])) return;
  tick_match($state['matches'][$matchId]);
  sync_manager_match($state, $matchId);
}
