<?php
declare(strict_types=1);

function manager_viewer_start_balance(array $season, ?array $state, string $viewer): ?int {
  if ($viewer === '') return null;
  if (isset($season['startingBalances'][$viewer])) return (int)$season['startingBalances'][$viewer];
  if ($state !== null && isset($state['users'][$viewer])) return manager_estimated_start_balance($state['users'][$viewer]);
  return null;
}

function manager_viewer_finance_start(array $season, string $viewer): ?array {
  if ($viewer === '') return null;
  $snapshots = is_array($season['startingFinance'] ?? null) ? $season['startingFinance'] : [];
  return is_array($snapshots[$viewer] ?? null) ? manager_finance_public_start_snapshot($snapshots[$viewer]) : null;
}

function manager_apply_finance_point_deductions(array &$state, array &$season, int $seasonYear): array {
  $applied = [];
  foreach (array_values($season['humanManagers'] ?? []) as $name) {
    $name = clean_name((string)$name);
    if ($name === '' || empty($state['users'][$name]) || !is_array($state['users'][$name])) continue;
    if (empty($season['table'][$name]) || !is_array($season['table'][$name])) continue;
    ensure_manager_finance($state['users'][$name]);
    $finance =& $state['users'][$name]['managerFinance'];
    $pending = is_array($finance['pointDeductionPending'] ?? null) ? $finance['pointDeductionPending'] : null;
    if (!$pending) continue;
    $pendingYear = (int)($pending['seasonYear'] ?? 0);
    if ($pendingYear <= 0 || $pendingYear > $seasonYear) continue;
    $points = max(1, (int)($pending['points'] ?? 10));
    $season['table'][$name]['points'] = (int)($season['table'][$name]['points'] ?? 0) - $points;
    $season['table'][$name]['pointDeduction'] = (int)($season['table'][$name]['pointDeduction'] ?? 0) + $points;
    $season['table'][$name]['pointDeductionReason'] = 'negative_balance';
    $entry = [
      'user' => $name,
      'reason' => 'negative_balance',
      'seasonId' => (string)($season['id'] ?? ''),
      'seasonYear' => $seasonYear,
      'points' => $points,
      'balance' => (int)($pending['balance'] ?? $finance['balance'] ?? 0),
      'fromSeasonId' => (string)($pending['fromSeasonId'] ?? ''),
      'appliedAt' => time(),
    ];
    if (!isset($season['pointDeductions']) || !is_array($season['pointDeductions'])) $season['pointDeductions'] = [];
    $season['pointDeductions'][$name] = $entry;
    $finance['pointDeductionApplied'] = $entry;
    $finance['pointDeductionPending'] = null;
    $applied[$name] = $entry;
  }
  if ($applied) {
    sort_manager_table($season);
    manager_log($state, 'finance_point_deduction', ['seasonId' => (string)($season['id'] ?? ''), 'seasonYear' => $seasonYear, 'applied' => $applied]);
  }
  return $applied;
}

function manager_update_debt_status_after_season(array &$state, array $season, string $name): ?array {
  $name = clean_name($name);
  if ($name === '' || empty($state['users'][$name]) || !is_array($state['users'][$name])) return null;
  ensure_manager_finance($state['users'][$name]);
  $finance =& $state['users'][$name]['managerFinance'];
  $finishedYear = manager_season_year($season, $state);
  $nextYear = $finishedYear + 1;
  $balance = (int)($finance['balance'] ?? 0);
  $warning = is_array($finance['debtWarning'] ?? null) ? $finance['debtWarning'] : null;
  $warningYear = $warning ? (int)($warning['deadlineSeasonYear'] ?? $warning['seasonYear'] ?? 0) : 0;
  $warningCoveredThisSeason = $warning && $warningYear > 0 && $warningYear <= $finishedYear;
  if ($balance < 0) {
    $status = 'warning';
    if ($warningCoveredThisSeason) {
      $finance['pointDeductionPending'] = manager_debt_penalty_payload($nextYear, $balance, (string)($season['id'] ?? ''));
      $status = 'penalty_pending';
    }
    $finance['debtWarning'] = manager_debt_warning_payload($nextYear, $balance, (string)($season['id'] ?? ''));
    return [
      'status' => $status,
      'balance' => $balance,
      'warningSeason' => $nextYear,
      'penaltySeason' => $status === 'penalty_pending' ? $nextYear : null,
      'points' => 10,
    ];
  }
  $hadDebtState = $warning || is_array($finance['pointDeductionPending'] ?? null);
  $finance['debtWarning'] = null;
  $finance['pointDeductionPending'] = null;
  return $hadDebtState ? ['status' => 'cleared', 'balance' => $balance] : null;
}

function create_manager_season_array(array &$state, array $selected, string $createdBy, int $now, int $seasonYear = 0, ?array $previousSeason = null): array {
  $selected = array_values(array_unique(array_map(fn($name) => clean_name((string)$name), $selected)));
  if (!$selected) reply(['ok' => false, 'error' => 'bad_users'], 400);
  if ($duplicate = manager_duplicate_human_team($state, $selected)) {
    reply(['ok' => false, 'error' => 'duplicate_human_team', 'team' => $duplicate['team'], 'users' => $duplicate['users']], 409);
  }
  $seasonYear = $seasonYear > 0 ? max(manager_base_season_year(), $seasonYear) : manager_state_year($state);
  $state['managerYear'] = $seasonYear;
  $managerGameId = '';
  if ($previousSeason !== null) {
    $managerGameId = manager_game_id($previousSeason);
    if ($managerGameId === '') $managerGameId = (string)($previousSeason['id'] ?? '');
  }
  if ($managerGameId === '') $managerGameId = 'g' . bin2hex(random_bytes(5));
  $divisionLevel = 5;
  if ($previousSeason !== null) {
    $divisionLevel = manager_continue_target_level($state, $previousSeason, $selected[0]);
  } elseif (isset($state['users'][$selected[0]])) {
    ensure_manager_finance($state['users'][$selected[0]]);
    $divisionLevel = clamp_int((int)($state['users'][$selected[0]]['managerDivisionLevel'] ?? 5), 1, 5);
  }
  $participants = [];
  $managedTeamIds = manager_game_human_team_keys($state, $managerGameId, $selected, $previousSeason);
  $startingBalances = [];
  $startingFinance = [];
  foreach ($selected as $name) {
    if (!isset($state['users'][$name])) reply(['ok' => false, 'error' => 'bad_user'], 400);
    if (!has_team($state['users'][$name])) reply(['ok' => false, 'error' => 'user_without_team'], 409);
    ensure_user_squad($state['users'][$name]);
    ensure_manager_finance($state['users'][$name]);
    $startingBalances[$name] = (int)($state['users'][$name]['managerFinance']['balance'] ?? manager_start_balance($divisionLevel));
    $startingFinance[$name] = manager_finance_counter_snapshot($state['users'][$name]['managerFinance'] ?? []);
    $team = user_team($state['users'][$name]);
    manager_mark_team_ids($managedTeamIds, $team);
    $participantFields = manager_human_participant_fields_from_user($name, $state['users'][$name]);
    $participantFields['divisionLevel'] = $divisionLevel;
    $participantFields['divisionName'] = manager_division_name($divisionLevel);
    if (is_array($participantFields['userSnapshot'] ?? null)) $participantFields['userSnapshot']['managerDivisionLevel'] = $divisionLevel;
    $participants[] = array_merge([
      'user' => $name,
      'managerType' => 'human',
    ], $participantFields);
  }
  $addCpuTeam = function (array $team) use (&$participants, &$managedTeamIds, $divisionLevel): bool {
    if (count($participants) >= manager_division_target_count($divisionLevel)) return false;
    $teamId = (string)($team['id'] ?? '');
    if ($teamId === '') return false;
    $teamKeys = [];
    manager_mark_team_ids($teamKeys, $team);
    foreach (array_keys($teamKeys) as $key) {
      if ($key !== '' && isset($managedTeamIds[$key])) return false;
    }
    $team['divisionId'] = manager_division_id($divisionLevel);
    $team['divisionName'] = manager_division_name($divisionLevel);
    $participants[] = cpu_manager_participant($team, $divisionLevel);
    manager_mark_team_ids($managedTeamIds, $team);
    return true;
  };
  if ($previousSeason !== null) {
    foreach (manager_relegated_from_above_teams($divisionLevel, (string)($previousSeason['id'] ?? ''), $previousSeason) as $team) {
      $addCpuTeam($team);
    }
  }
  foreach (manager_division_teams($divisionLevel) as $team) {
    if (count($participants) >= manager_division_target_count($divisionLevel)) break;
    $addCpuTeam($team);
  }
  $id = 's' . bin2hex(random_bytes(5));
  $rounds = manager_fixtures($participants);
  $season = [
    'id' => $id,
    'managerGameId' => $managerGameId,
    'status' => 'active',
    'created' => $now,
    'updated' => $now,
    'seasonYear' => $seasonYear,
    'startingBalances' => $startingBalances,
    'startingFinance' => $startingFinance,
    'createdBy' => $createdBy,
    'divisionLevel' => $divisionLevel,
    'divisionName' => manager_division_name($divisionLevel),
    'humanManagers' => $selected,
    'participants' => $participants,
    'table' => manager_table($participants),
    'rounds' => $rounds,
  ];
  manager_apply_finance_point_deductions($state, $season, $seasonYear);
  manager_start_all_cups($state, $season, $selected, $previousSeason);
  manager_ensure_injury_plan($season);
  return $season;
}

function manager_season_has_started(array $season): bool {
  foreach (($season['table'] ?? []) as $row) {
    if ((int)($row['played'] ?? 0) > 0) return true;
  }
  foreach (($season['rounds'] ?? []) as $round) {
    foreach (($round['fixtures'] ?? []) as $fixture) {
      if (($fixture['status'] ?? 'pending') !== 'pending') return true;
    }
  }
  return false;
}

function manager_season_has_cup_round(array $season): bool {
  foreach (($season['rounds'] ?? []) as $round) {
    if (($round['phase'] ?? '') === 'cup') return true;
  }
  return false;
}

function manager_repair_relegated_carry(array &$state): void {
  if (empty($state['managerSeasons']) || !is_array($state['managerSeasons'])) return;
  $repaired = [];
  foreach ($state['managerSeasons'] as $seasonId => &$season) {
    if (!is_array($season) || ($season['status'] ?? '') !== 'active') continue;
    $previousId = (string)($season['continuedFrom'] ?? '');
    if ($previousId === '' || empty($state['managerSeasons'][$previousId]) || manager_season_has_started($season)) continue;
    $level = clamp_int((int)($season['divisionLevel'] ?? 5), 1, 5);
    $previousSeason = $state['managerSeasons'][$previousId] ?? null;
    $expectedTeams = manager_relegated_from_above_teams($level, $previousId, is_array($previousSeason) ? $previousSeason : null);
    if (!$expectedTeams) continue;
    $targetCount = manager_division_target_count($level);
    $participants = array_values($season['participants'] ?? []);
    $newParticipants = [];
    $seen = [];
    $addParticipant = function (array $participant) use (&$newParticipants, &$seen, $targetCount): bool {
      if (count($newParticipants) >= $targetCount) return false;
      $user = (string)($participant['user'] ?? '');
      $team = is_array($participant['team'] ?? null) ? $participant['team'] : [];
      $teamId = (string)($team['id'] ?? '');
      $sourceId = (string)($team['sourceId'] ?? $teamId);
      $keys = array_filter([$user, $teamId, $sourceId], fn($key) => $key !== '');
      foreach ($keys as $key) {
        if (isset($seen[$key])) return false;
      }
      $newParticipants[] = $participant;
      foreach ($keys as $key) $seen[$key] = true;
      return true;
    };
    foreach ($participants as $participant) {
      if (($participant['managerType'] ?? '') !== 'human' && is_cpu_manager((string)($participant['user'] ?? ''))) continue;
      $addParticipant($participant);
    }
    foreach ($expectedTeams as $team) {
      $team['divisionId'] = manager_division_id($level);
      $team['divisionName'] = manager_division_name($level);
      $addParticipant(cpu_manager_participant($team, $level));
    }
    foreach ($participants as $participant) {
      if (($participant['managerType'] ?? '') === 'human' && !is_cpu_manager((string)($participant['user'] ?? ''))) continue;
      $addParticipant($participant);
    }
    $before = array_map(fn($participant) => (string)($participant['user'] ?? ''), $participants);
    $after = array_map(fn($participant) => (string)($participant['user'] ?? ''), $newParticipants);
    $needsParticipantRepair = $before !== $after && count($newParticipants) === $targetCount;
    $needsCupRepair = !manager_season_has_cup_round($season);
    if (!$needsParticipantRepair && !$needsCupRepair) continue;
    $season['participants'] = $newParticipants;
    $season['participantCount'] = count($newParticipants);
    $season['table'] = manager_table($newParticipants);
    $season['rounds'] = manager_fixtures($newParticipants);
    $season['updated'] = time();
    unset($season['simVotes'], $season['watchVotes'], $season['simSeasonVotes'], $season['playoff'], $season['playoffStarted'], $season['promotion'], $season['outcome'], $season['cup'], $season['cups'], $season['cupParticipants']);
    manager_start_all_cups($state, $season, array_values($season['humanManagers'] ?? []), is_array($previousSeason) ? $previousSeason : null);
    $repaired[] = [
      'seasonId' => (string)$seasonId,
      'fromSeasonId' => $previousId,
      'division' => manager_division_name($level),
      'cupRebuilt' => $needsCupRepair,
      'added' => array_values(array_diff($after, $before)),
      'removed' => array_values(array_diff($before, $after)),
    ];
  }
  unset($season);
  foreach ($repaired as $entry) manager_log($state, 'repair_relegated_carry', $entry);
}

function manager_expected_cup_keys_for_season(array &$state, array $season, ?array $previousSeason): array {
  $selected = array_values($season['humanManagers'] ?? []);
  if (!$selected) return [];
  $keys = [];
  foreach (manager_cup_definitions() as $cup) {
    $participants = manager_cup_participants_for_definition($state, $selected, $cup, $previousSeason);
    if (count($participants) >= 2) $keys[] = (string)$cup['key'];
  }
  return $keys;
}

function manager_repair_unstarted_cups(array &$state): void {
  if (empty($state['managerSeasons']) || !is_array($state['managerSeasons'])) return;
  $repaired = [];
  foreach ($state['managerSeasons'] as $seasonId => &$season) {
    if (!is_array($season) || ($season['status'] ?? '') !== 'active' || manager_season_has_started($season)) continue;
    $selected = array_values($season['humanManagers'] ?? []);
    if (!$selected) continue;
    $previousId = (string)($season['continuedFrom'] ?? '');
    $previousSeason = $previousId !== '' && is_array($state['managerSeasons'][$previousId] ?? null) ? $state['managerSeasons'][$previousId] : null;
    $expected = manager_expected_cup_keys_for_season($state, $season, $previousSeason);
    $existing = array_keys($season['cups'] ?? []);
    $staleCup = false;
    foreach (($season['cups'] ?? []) as $cup) {
      if ((int)($cup['version'] ?? 0) < MANAGER_CUP_VERSION) {
        $staleCup = true;
        break;
      }
    }
    if (!$expected || (!array_diff($expected, $existing) && !$staleCup)) continue;
    $season['rounds'] = array_values(array_filter($season['rounds'] ?? [], fn($round) => ($round['phase'] ?? '') !== 'cup'));
    unset($season['cup'], $season['cups'], $season['cupParticipants']);
    manager_start_all_cups($state, $season, $selected, $previousSeason);
    $season['updated'] = time();
    $repaired[] = ['seasonId' => (string)$seasonId, 'addedCupKeys' => array_values(array_diff($expected, $existing))];
  }
  unset($season);
  foreach ($repaired as $entry) manager_log($state, 'repair_unstarted_cups', $entry);
}

function manager_result_hidden_for_viewer(int $hiddenUntil, string $visibleTo, string $viewer, int $now = 0): bool {
  if ($hiddenUntil <= 0) return false;
  if ($now <= 0) $now = time();
  if ($now >= $hiddenUntil) return false;
  $visible = array_values(array_filter(array_map('trim', explode(',', $visibleTo)), fn($name) => $name !== ''));
  if (!$visible) return $viewer === '' || $viewer !== $visibleTo;
  return $viewer === '' || !in_array($viewer, $visible, true);
}

function manager_playback_user_list(string $visibleTo): array {
  $users = [];
  foreach (array_map('trim', explode(',', $visibleTo)) as $name) {
    if ($name !== '') $users[$name] = true;
  }
  return array_keys($users);
}

function manager_fixture_playback_required_users(array $fixture): array {
  $required = $fixture['playbackRequired'] ?? [];
  if (!is_array($required)) return [];
  $users = [];
  foreach ($required as $key => $value) {
    $name = is_int($key) ? (string)$value : (string)$key;
    if ($name !== '' && !empty($value)) $users[$name] = true;
  }
  return array_keys($users);
}

function manager_fixture_playback_done_users(array $fixture): array {
  $done = $fixture['playbackDone'] ?? [];
  if (!is_array($done)) return [];
  $users = [];
  foreach ($done as $key => $value) {
    $name = is_int($key) ? (string)$value : (string)$key;
    if ($name !== '' && !empty($value)) $users[$name] = true;
  }
  return array_keys($users);
}

function manager_fixture_playback_has_requirement(array $fixture): bool {
  return count(manager_fixture_playback_required_users($fixture)) > 0;
}

function manager_fixture_playback_pending_users(array $fixture): array {
  $done = array_fill_keys(manager_fixture_playback_done_users($fixture), true);
  return array_values(array_filter(manager_fixture_playback_required_users($fixture), fn($name) => empty($done[$name])));
}

function manager_fixture_playback_any_pending(array $fixture): bool {
  return count(manager_fixture_playback_pending_users($fixture)) > 0;
}

function manager_fixture_playback_active_window(array $fixture, int $now = 0): bool {
  if ($now <= 0) $now = time();
  $started = (int)($fixture['playbackStartedAt'] ?? 0);
  if ($started <= 0) return (int)($fixture['resultHiddenUntil'] ?? 0) > $now;
  $speed = manager_playback_speed_pct($fixture['playbackSpeedPct'] ?? 100);
  $duration = (int)ceil(MANAGER_LIVE_PLAYBACK_SECONDS / max(0.25, $speed / 100)) + 15;
  return $now <= $started + max(20, $duration);
}

function manager_fixture_playback_pending_for_viewer(array $fixture, string $viewer): bool {
  $required = manager_fixture_playback_required_users($fixture);
  if (!$required) return false;
  $done = array_fill_keys(manager_fixture_playback_done_users($fixture), true);
  if ($viewer !== '' && in_array($viewer, $required, true)) return empty($done[$viewer]) && manager_fixture_playback_active_window($fixture);
  return manager_fixture_playback_active_window($fixture);
}

function manager_fixture_result_hidden_for_viewer(array $fixture, string $viewer, int $now = 0): bool {
  if (manager_fixture_playback_has_requirement($fixture)) return manager_fixture_playback_pending_for_viewer($fixture, $viewer);
  return manager_result_hidden_for_viewer((int)($fixture['resultHiddenUntil'] ?? 0), (string)($fixture['resultVisibleTo'] ?? ''), $viewer, $now);
}

function manager_fixture_live_progress(array $fixture): float {
  $started = (int)($fixture['playbackStartedAt'] ?? 0);
  if ($started > 0) {
    $speed = manager_playback_speed_pct($fixture['playbackSpeedPct'] ?? 100);
    $duration = MANAGER_LIVE_PLAYBACK_SECONDS / max(0.25, $speed / 100);
    return max(0.0, min(1.0, (time() - $started) / max(1.0, $duration)));
  }
  $hiddenUntil = (int)($fixture['resultHiddenUntil'] ?? 0);
  $remaining = max(0, $hiddenUntil - time());
  $duration = max(1, MANAGER_SIM_RESULT_REVEAL_SECONDS);
  return max(0.0, min(1.0, ($duration - $remaining) / $duration));
}

function manager_report_live_score(array $fixture, array $report, float $progress): ?array {
  $goals = array_values(array_filter($report['goalScorers'] ?? [], 'is_array'));
  if (!$goals) return null;
  usort($goals, fn($a, $b) => ((float)($a['at'] ?? $a['minute'] ?? 0) <=> (float)($b['at'] ?? $b['minute'] ?? 0)) ?: ((int)($a['seq'] ?? 0) <=> (int)($b['seq'] ?? 0)));
  $visibleUntil = max(0.0, min(90.0, $progress * 90.0));
  $home = 0;
  $away = 0;
  foreach ($goals as $goal) {
    $at = (float)($goal['at'] ?? $goal['minute'] ?? 0);
    if ($at > $visibleUntil) continue;
    if (($goal['side'] ?? '') === 'home') $home++;
    elseif (($goal['side'] ?? '') === 'away') $away++;
  }
  $total = count($goals);
  if (manager_fixture_playback_any_pending($fixture) && $home + $away >= $total && $total > 0) {
    $last = $goals[$total - 1] ?? null;
    if (($last['side'] ?? '') === 'home') $home = max(0, $home - 1);
    elseif (($last['side'] ?? '') === 'away') $away = max(0, $away - 1);
  }
  return [$home, $away];
}

function manager_masked_live_score(array $fixture, ?array $report = null): ?array {
  $score = $fixture['score'] ?? null;
  if (!is_array($score)) return null;
  $homeGoals = max(0, (int)($score[0] ?? 0));
  $awayGoals = max(0, (int)($score[1] ?? 0));
  $totalGoals = $homeGoals + $awayGoals;
  if ($totalGoals <= 0) return [0, 0];
  $progress = manager_fixture_live_progress($fixture);
  if ($report) {
    $reportScore = manager_report_live_score($fixture, $report, $progress);
    if ($reportScore !== null) return $reportScore;
  }
  $visibleGoals = (int)floor($progress * ($totalGoals + 1));
  if ($visibleGoals >= $totalGoals && $progress < 0.70) $visibleGoals = max(0, $totalGoals - 1);
  $visibleGoals = max(0, min($totalGoals, $visibleGoals));
  if (manager_fixture_playback_any_pending($fixture)) $visibleGoals = min($visibleGoals, max(0, $totalGoals - 1));
  if ($visibleGoals <= 0) return [0, 0];

  $seed = (string)($fixture['home'] ?? '') . '|' . (string)($fixture['away'] ?? '') . '|' . (string)($fixture['playedAt'] ?? '') . '|' . $homeGoals . '-' . $awayGoals;
  $events = [];
  for ($i = 0; $i < $homeGoals; $i++) $events[] = ['side' => 'home', 'order' => crc32($seed . '|home|' . $i)];
  for ($i = 0; $i < $awayGoals; $i++) $events[] = ['side' => 'away', 'order' => crc32($seed . '|away|' . $i)];
  usort($events, fn($a, $b) => ((int)$a['order']) <=> ((int)$b['order']));

  $live = [0, 0];
  foreach (array_slice($events, 0, $visibleGoals) as $event) {
    if (($event['side'] ?? '') === 'home') $live[0]++;
    else $live[1]++;
  }
  return $live;
}

function manager_report_result_hidden_for_viewer(array $season, array $report, string $viewer, int $now = 0): bool {
  $roundIndex = (int)($report['roundIndex'] ?? -1);
  $fixtureIndex = (int)($report['fixtureIndex'] ?? -1);
  $fixture = $season['rounds'][$roundIndex]['fixtures'][$fixtureIndex] ?? null;
  if (is_array($fixture) && manager_fixture_playback_has_requirement($fixture)) return manager_fixture_result_hidden_for_viewer($fixture, $viewer, $now);
  if (manager_result_hidden_for_viewer((int)($report['resultHiddenUntil'] ?? 0), (string)($report['resultVisibleTo'] ?? ''), $viewer, $now)) return true;
  return is_array($fixture) && manager_fixture_result_hidden_for_viewer($fixture, $viewer, $now);
}

function manager_mask_fixture_result(array $fixture, string $viewer = '', ?array $report = null): array {
  $fixture['status'] = 'simulating';
  $fixture['score'] = manager_masked_live_score($fixture, $report);
  $fixture['resultMasked'] = true;
  $fixture['resultRevealIn'] = max(0, (int)($fixture['resultHiddenUntil'] ?? 0) - time());
  $fixture['playbackPending'] = manager_fixture_playback_pending_for_viewer($fixture, $viewer);
  unset($fixture['winner'], $fixture['decidedBy'], $fixture['penaltyScore'], $fixture['reportId']);
  return $fixture;
}

function manager_mark_fixture_result_reveal_delay(array &$season, int $roundIndex, int $fixtureIndex, string $visibleTo, int $now): void {
  if (empty($season['rounds'][$roundIndex]['fixtures'][$fixtureIndex])) return;
  $hiddenUntil = $now + MANAGER_SIM_RESULT_REVEAL_SECONDS;
  $fixture =& $season['rounds'][$roundIndex]['fixtures'][$fixtureIndex];
  $fixture['resultHiddenUntil'] = $hiddenUntil;
  $fixture['resultVisibleTo'] = $visibleTo;
  if (!isset($fixture['playbackRequired']) || !is_array($fixture['playbackRequired'])) $fixture['playbackRequired'] = [];
  if (!isset($fixture['playbackDone']) || !is_array($fixture['playbackDone'])) $fixture['playbackDone'] = [];
  foreach (manager_playback_user_list($visibleTo) as $name) $fixture['playbackRequired'][$name] = $now;
  $fixture['playbackStartedAt'] = $fixture['playbackStartedAt'] ?? $now;
  $reportId = (string)($fixture['reportId'] ?? '');
  if (isset($season['matchReports']) && is_array($season['matchReports'])) {
    foreach ($season['matchReports'] as &$report) {
      if (!is_array($report)) continue;
      $sameReport = $reportId !== '' && (string)($report['id'] ?? '') === $reportId;
      $sameFixture = (int)($report['roundIndex'] ?? -1) === $roundIndex && (int)($report['fixtureIndex'] ?? -1) === $fixtureIndex;
      if (!$sameReport && !$sameFixture) continue;
      $report['resultHiddenUntil'] = $hiddenUntil;
      $report['resultVisibleTo'] = $visibleTo;
    }
    unset($report);
  }
  unset($fixture);
}

function manager_mark_fixture_playback_complete(array &$season, int $roundIndex, int $fixtureIndex, string $user, int $now): bool {
  if ($user === '' || empty($season['rounds'][$roundIndex]['fixtures'][$fixtureIndex])) return false;
  $fixture =& $season['rounds'][$roundIndex]['fixtures'][$fixtureIndex];
  if (!isset($fixture['playbackDone']) || !is_array($fixture['playbackDone'])) $fixture['playbackDone'] = [];
  $fixture['playbackDone'][$user] = $now;
  $allDone = !manager_fixture_playback_any_pending($fixture);
  if ($allDone) {
    $fixture['resultHiddenUntil'] = 0;
    $fixture['resultVisibleTo'] = '';
  }
  $reportId = (string)($fixture['reportId'] ?? '');
  if ($allDone && isset($season['matchReports']) && is_array($season['matchReports'])) {
    foreach ($season['matchReports'] as &$report) {
      if (!is_array($report)) continue;
      $sameReport = $reportId !== '' && (string)($report['id'] ?? '') === $reportId;
      $sameFixture = (int)($report['roundIndex'] ?? -1) === $roundIndex && (int)($report['fixtureIndex'] ?? -1) === $fixtureIndex;
      if (!$sameReport && !$sameFixture) continue;
      $report['resultHiddenUntil'] = 0;
      $report['resultVisibleTo'] = '';
    }
    unset($report);
  }
  unset($fixture);
  return true;
}

function manager_playback_speed_pct(mixed $value): int {
  $pct = (int)$value;
  $allowed = [25, 50, 75, 100, 125, 150, 175, 200, 225, 250, 275, 300, 325, 350, 375, 400];
  return in_array($pct, $allowed, true) ? $pct : 100;
}

function manager_set_fixture_playback_speed(array &$season, int $roundIndex, int $fixtureIndex, int $speedPct, string $user, int $now): bool {
  if (empty($season['rounds'][$roundIndex]['fixtures'][$fixtureIndex])) return false;
  $fixture =& $season['rounds'][$roundIndex]['fixtures'][$fixtureIndex];
  if (($fixture['home'] ?? '') !== $user && ($fixture['away'] ?? '') !== $user) {
    unset($fixture);
    return false;
  }
  $newSpeed = manager_playback_speed_pct($speedPct);
  $oldSpeed = manager_playback_speed_pct($fixture['playbackSpeedPct'] ?? 100);
  $started = (int)($fixture['playbackStartedAt'] ?? 0);
  if ($started > 0 && $newSpeed !== $oldSpeed) {
    $oldDuration = MANAGER_LIVE_PLAYBACK_SECONDS / max(0.25, $oldSpeed / 100);
    $progress = max(0.0, min(0.985, ($now - $started) / max(1.0, $oldDuration)));
    $newDuration = MANAGER_LIVE_PLAYBACK_SECONDS / max(0.25, $newSpeed / 100);
    $fixture['playbackStartedAt'] = (int)round($now - ($progress * $newDuration));
  }
  $fixture['playbackSpeedPct'] = $newSpeed;
  $fixture['playbackSpeedBy'] = $user;
  $fixture['playbackSpeedAt'] = $now;
  unset($fixture);
  return true;
}

function manager_user_pending_playback_fixture(array $season, string $user): ?array {
  if ($user === '') return null;
  foreach (($season['rounds'] ?? []) as $roundIndex => $round) {
    foreach (($round['fixtures'] ?? []) as $fixtureIndex => $fixture) {
      if (!is_array($fixture) || ($fixture['status'] ?? '') !== 'played') continue;
      if ((string)($fixture['home'] ?? '') !== $user && (string)($fixture['away'] ?? '') !== $user) continue;
      if (!manager_fixture_playback_pending_for_viewer($fixture, $user)) continue;
      return ['roundIndex' => (int)$roundIndex, 'fixtureIndex' => (int)$fixtureIndex, 'fixture' => $fixture];
    }
  }
  return null;
}

function public_manager_fixture(array $fixture, array $names, ?array $state, array $season, string $viewer = '', int $roundIndex = -1, int $fixtureIndex = -1, array $previews = []): array {
  $home = (string)($fixture['home'] ?? '');
  $away = (string)($fixture['away'] ?? '');
  $homePreview = $previews[$home] ?? manager_side_preview($state, $season, $home);
  $awayPreview = $previews[$away] ?? manager_side_preview($state, $season, $away);
  $homeStrength = (int)($names[$home]['rawStrength'] ?? $names[$home]['strength'] ?? $homePreview['strength']);
  $awayStrength = (int)($names[$away]['rawStrength'] ?? $names[$away]['strength'] ?? $awayPreview['strength']);
  $homePreview = manager_apply_fixture_form_to_side_data($season, $fixture, $home, $homePreview, $roundIndex, $fixtureIndex);
  $awayPreview = manager_apply_fixture_form_to_side_data($season, $fixture, $away, $awayPreview, $roundIndex, $fixtureIndex);
  $homeMatchStrength = (int)($homePreview['matchStrength'] ?? $homeStrength);
  $awayMatchStrength = (int)($awayPreview['matchStrength'] ?? $awayStrength);
  $fixture['seasonId'] = (string)($season['id'] ?? '');
  if ($roundIndex >= 0) $fixture['roundIndex'] = $roundIndex;
  if ($fixtureIndex >= 0) $fixture['fixtureIndex'] = $fixtureIndex;
  $fixture['homeTeam'] = is_array($homePreview['team'] ?? null) ? $homePreview['team'] : ($names[$home]['team'] ?? null);
  $fixture['awayTeam'] = is_array($awayPreview['team'] ?? null) ? $awayPreview['team'] : ($names[$away]['team'] ?? null);
  $fixture['divisionName'] = (string)($season['divisionName'] ?? manager_division_name((int)($season['divisionLevel'] ?? 5)));
  $fixture['rawStrength'] = ['home' => $homeStrength, 'away' => $awayStrength];
  $fixture['strength'] = ['home' => manager_display_skill($homeStrength), 'away' => manager_display_skill($awayStrength)];
  $fixture['odds'] = manager_fixture_probabilities(
    $season,
    $home,
    $away,
    ['squad' => $homePreview['squad'] ?? [], 'strength' => $homeStrength, 'matchStrength' => $homeMatchStrength],
    ['squad' => $awayPreview['squad'] ?? [], 'strength' => $awayStrength, 'matchStrength' => $awayMatchStrength]
  );
  $fixture['mine'] = $viewer !== '' && ($home === $viewer || $away === $viewer);
  if (manager_fixture_result_hidden_for_viewer($fixture, $viewer)) {
    $report = ($roundIndex >= 0 && $fixtureIndex >= 0) ? public_manager_live_match_report($season, $fixture, $roundIndex, $fixtureIndex) : null;
    $fixture = manager_mask_fixture_result($fixture, $viewer, $report);
  }
  return $fixture;
}

function public_manager_match_reports(array $season, string $viewer = '', int $limit = 12): array {
  $reports = array_values(array_filter($season['matchReports'] ?? [], 'is_array'));
  if ($viewer !== '') {
    $reports = array_values(array_filter($reports, fn($report) => (string)($report['home'] ?? '') === $viewer || (string)($report['away'] ?? '') === $viewer));
  }
  $reports = array_values(array_filter($reports, fn($report) => !manager_report_result_hidden_for_viewer($season, $report, $viewer)));
  $reports = array_map('manager_normalize_match_report_cards', $reports);
  usort($reports, fn($a, $b) => ((int)($b['playedAt'] ?? 0)) <=> ((int)($a['playedAt'] ?? 0)));
  return array_slice($reports, 0, $limit);
}

function public_manager_live_match_report(array $season, array $fixture, int $roundIndex, int $fixtureIndex): ?array {
  $reportId = (string)($fixture['reportId'] ?? '');
  $reports = array_reverse(array_values(array_filter($season['matchReports'] ?? [], 'is_array')));
  foreach ($reports as $report) {
    $sameReport = $reportId !== '' && (string)($report['id'] ?? '') === $reportId;
    $sameFixture = (int)($report['roundIndex'] ?? -1) === $roundIndex && (int)($report['fixtureIndex'] ?? -1) === $fixtureIndex;
    if (!$sameReport && !$sameFixture) continue;
    $out = manager_normalize_match_report_cards($report);
    unset($out['resultHiddenUntil'], $out['resultVisibleTo']);
    return $out;
  }
  return null;
}

function manager_public_fixture_maps(?array $state, array $season): array {
  $names = [];
  $previews = [];
  foreach (array_merge($season['participants'] ?? [], $season['cupParticipants'] ?? []) as $participant) {
    if (!is_array($participant)) continue;
    $id = (string)($participant['user'] ?? '');
    if ($id === '' || isset($names[$id])) continue;
    $preview = manager_side_preview($state, $season, $id);
    $previews[$id] = $preview;
    $names[$id] = [
      'user' => $id,
      'managerType' => (string)($participant['managerType'] ?? (is_cpu_manager($id) ? 'cpu' : 'human')),
      'team' => $preview['team'],
      'rating' => $preview['rating'],
      'rawStrength' => $preview['strength'],
      'matchRawStrength' => $preview['matchStrength'] ?? $preview['strength'],
      'strength' => manager_display_skill((int)$preview['strength']),
      'divisionLevel' => (int)($participant['divisionLevel'] ?? ($season['divisionLevel'] ?? 5)),
      'divisionName' => (string)($participant['divisionName'] ?? ($season['divisionName'] ?? manager_division_name((int)($season['divisionLevel'] ?? 5)))),
    ];
  }
  return [$names, $previews];
}

function public_manager_live_simulation_rows_from_related(array $state, array $season, string $viewer, array $seen = []): array {
  $rows = [];
  $currentId = (string)($season['id'] ?? '');
  foreach (manager_related_current_season_ids($state, $season) as $seasonId) {
    if ($seasonId === $currentId) continue;
    $source = $state['managerSeasons'][$seasonId] ?? null;
    if (!is_array($source)) continue;
    [$names, $previews] = manager_public_fixture_maps($state, $source);
    foreach (($source['rounds'] ?? []) as $roundIndex => $round) {
      if (!is_array($round)) continue;
      foreach (($round['fixtures'] ?? []) as $fixtureIndex => $fixture) {
        if (!is_array($fixture)) continue;
        $humans = manager_fixture_human_users($source, $fixture);
        if (!$humans) continue;
        $fixtureOut = public_manager_fixture($fixture, $names, $state, $source, $viewer, (int)$roundIndex, (int)$fixtureIndex, $previews);
        if (empty($fixtureOut['resultMasked'])) continue;
        $key = (string)($source['id'] ?? $seasonId) . ':' . (int)$roundIndex . ':' . (int)$fixtureIndex;
        if (isset($seen[$key])) continue;
        $report = public_manager_live_match_report($source, $fixture, (int)$roundIndex, (int)$fixtureIndex);
        $rows[] = [
          'sourceSeasonId' => (string)($source['id'] ?? $seasonId),
          'divisionName' => (string)($source['divisionName'] ?? manager_division_name((int)($source['divisionLevel'] ?? 5))),
          'round' => $round['round'] ?? '',
          'phase' => (string)($round['phase'] ?? $fixtureOut['phase'] ?? 'league'),
          'playoff' => !empty($round['playoff']) || (string)($round['phase'] ?? '') === 'playoff',
          'cupName' => (string)($round['cupName'] ?? $fixtureOut['cupName'] ?? ''),
          'humans' => $humans,
          'fixture' => $fixtureOut,
          'revealIn' => (int)($fixtureOut['resultRevealIn'] ?? 0),
          'duration' => MANAGER_SIM_RESULT_REVEAL_SECONDS,
          'report' => $report,
        ];
        $seen[$key] = true;
      }
    }
  }
  return $rows;
}

function manager_live_simulation_row_time(array $row): int {
  $fixture = is_array($row['fixture'] ?? null) ? $row['fixture'] : [];
  $started = (int)($fixture['playbackStartedAt'] ?? 0);
  if ($started > 0) return $started;
  $hiddenUntil = (int)($fixture['resultHiddenUntil'] ?? 0);
  if ($hiddenUntil > 0) return max(0, $hiddenUntil - MANAGER_SIM_RESULT_REVEAL_SECONDS);
  return (int)($fixture['playedAt'] ?? 0);
}

function manager_live_simulation_row_key(array $row): string {
  $fixture = is_array($row['fixture'] ?? null) ? $row['fixture'] : [];
  return (string)($row['sourceSeasonId'] ?? $fixture['seasonId'] ?? '') . ':' . (int)($fixture['roundIndex'] ?? -1) . ':' . (int)($fixture['fixtureIndex'] ?? -1);
}

function manager_filter_public_live_simulations(array $rows): array {
  $latestByHuman = [];
  foreach ($rows as $index => $row) {
    if (!is_array($row) || !is_array($row['fixture'] ?? null)) continue;
    $humans = array_values(array_filter(array_map('strval', $row['humans'] ?? []), fn($name) => $name !== ''));
    if (!$humans) {
      foreach (['home', 'away'] as $side) {
        $name = (string)($row['fixture'][$side] ?? '');
        if ($name !== '' && !is_cpu_manager($name)) $humans[] = $name;
      }
    }
    if (!$humans) continue;
    $time = manager_live_simulation_row_time($row);
    foreach (array_unique($humans) as $human) {
      if (!isset($latestByHuman[$human]) || $time >= (int)$latestByHuman[$human]['time']) {
        $latestByHuman[$human] = ['index' => $index, 'time' => $time];
      }
    }
  }
  $keep = [];
  foreach ($latestByHuman as $entry) $keep[(int)$entry['index']] = true;
  $out = [];
  $seen = [];
  foreach ($rows as $index => $row) {
    if (empty($keep[$index]) || !is_array($row)) continue;
    $key = manager_live_simulation_row_key($row);
    if (isset($seen[$key])) continue;
    $seen[$key] = true;
    $out[] = $row;
  }
  usort($out, fn($a, $b) => manager_live_simulation_row_time($b) <=> manager_live_simulation_row_time($a));
  return $out;
}

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

function public_manager_table(array $season, string $viewer = ''): array {
  $table = [];
  foreach (($season['table'] ?? []) as $key => $row) {
    if (!is_array($row)) continue;
    $user = (string)($row['user'] ?? $key);
    if ($user === '') continue;
    $row['user'] = $user;
    $table[$user] = $row;
  }
  foreach (($season['rounds'] ?? []) as $roundIndex => $round) {
    foreach (($round['fixtures'] ?? []) as $fixture) {
      if (($fixture['phase'] ?? $round['phase'] ?? 'league') !== 'league') continue;
      if (($fixture['status'] ?? '') !== 'played' || !is_array($fixture['score'] ?? null)) continue;
      if (!manager_fixture_result_hidden_for_viewer($fixture, $viewer)) continue;
      manager_revert_public_league_result($table, (string)($fixture['home'] ?? ''), (string)($fixture['away'] ?? ''), (int)($fixture['score'][0] ?? 0), (int)($fixture['score'][1] ?? 0));
    }
  }
  $rows = array_values($table);
  $rows = manager_apply_table_debug_overrides($rows);
  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_table_debug_overrides(): array {
  static $overrides = null;
  if ($overrides !== null) return $overrides;
  $overrides = [];
  $root = defined('YSP_RUNTIME_ROOT') ? YSP_RUNTIME_ROOT : dirname(__DIR__, 2);
  $file = $root . '/manager_table_debug.json';
  if (!is_file($file)) return $overrides;
  $data = json_decode((string)@file_get_contents($file), true);
  if (!is_array($data)) return $overrides;
  foreach ($data as $key => $values) {
    if (!is_array($values)) continue;
    $key = strtolower(trim((string)$key));
    if ($key !== '') $overrides[$key] = $values;
  }
  return $overrides;
}

function manager_apply_table_debug_overrides(array $rows): array {
  $overrides = manager_table_debug_overrides();
  if (!$overrides) return $rows;
  $fields = ['played', 'won', 'drawn', 'lost', 'gf', 'ga', 'points'];
  foreach ($rows as &$row) {
    $keys = array_filter([
      strtolower(trim((string)($row['user'] ?? ''))),
      strtolower(trim((string)($row['team']['name'] ?? ''))),
      strtolower(trim((string)($row['team']['id'] ?? ''))),
      strtolower(trim((string)($row['team']['sourceId'] ?? ''))),
    ]);
    $override = null;
    foreach ($keys as $key) {
      if (isset($overrides[$key])) {
        $override = $overrides[$key];
        break;
      }
    }
    if (!$override) continue;
    foreach ($fields as $field) {
      if (array_key_exists($field, $override)) $row[$field] = (int)$override[$field];
    }
    $row['gd'] = (int)($row['gf'] ?? 0) - (int)($row['ga'] ?? 0);
  }
  unset($row);
  return $rows;
}

function manager_competition_context_from_fixture(array $fixture = []): array {
  $phase = strtolower((string)($fixture['phase'] ?? 'league'));
  if ($phase === 'cup') {
    $cupKey = (string)($fixture['cupKey'] ?? 'cup');
    $cupName = (string)($fixture['cupName'] ?? 'Cup');
    return ['key' => 'cup:' . $cupKey, 'phase' => 'cup', 'cupKey' => $cupKey, 'label' => $cupName];
  }
  if ($phase === 'playoff') return ['key' => 'playoff', 'phase' => 'playoff', 'cupKey' => '', 'label' => 'Playoffs'];
  return ['key' => 'league', 'phase' => 'league', 'cupKey' => '', 'label' => 'League'];
}

function manager_viewer_stat_context(array $season, string $viewer = ''): array {
  if ($viewer !== '') {
    foreach (($season['rounds'] ?? []) as $round) {
      foreach (($round['fixtures'] ?? []) as $fixture) {
        if (!in_array(($fixture['status'] ?? ''), ['pending', 'watching'], true)) continue;
        if ((string)($fixture['home'] ?? '') !== $viewer && (string)($fixture['away'] ?? '') !== $viewer) continue;
        if ((string)($fixture['phase'] ?? 'league') === 'cup') return manager_competition_context_from_fixture($fixture);
      }
    }
  }
  return manager_competition_context_from_fixture(['phase' => 'league']);
}

function manager_league_participant_map(array $season): array {
  $ids = [];
  foreach (($season['participants'] ?? []) as $participant) {
    $id = (string)($participant['user'] ?? '');
    if ($id !== '') $ids[$id] = true;
  }
  return $ids;
}

function manager_cup_participant_map(array $season, string $cupKey): array {
  $ids = [];
  foreach (($season['rounds'] ?? []) as $round) {
    if (($round['phase'] ?? '') !== 'cup' || (string)($round['cupKey'] ?? '') !== $cupKey) continue;
    foreach (($round['fixtures'] ?? []) as $fixture) {
      foreach (['home', 'away'] as $side) {
        $id = (string)($fixture[$side] ?? '');
        if ($id !== '') $ids[$id] = true;
      }
    }
  }
  return $ids;
}

function manager_stat_competition_key(array $row): string {
  $key = (string)($row['competitionKey'] ?? '');
  if ($key !== '') return $key;
  $phase = strtolower((string)($row['phase'] ?? ''));
  if ($phase === 'cup') return 'cup:' . (string)($row['cupKey'] ?? 'cup');
  if ($phase === 'playoff') return 'playoff';
  return '';
}

function manager_stat_matches_context(array $season, array $row, array $context): bool {
  $key = manager_stat_competition_key($row);
  if ($key !== '') return $key === (string)($context['key'] ?? 'league');
  $contextKey = (string)($context['key'] ?? 'league');
  if (str_starts_with($contextKey, 'cup:')) {
    $cupTeams = manager_cup_participant_map($season, (string)($context['cupKey'] ?? substr($contextKey, 4)));
    return isset($cupTeams[(string)($row['user'] ?? '')]);
  }
  if ($contextKey !== 'league') return false;
  $league = manager_league_participant_map($season);
  return isset($league[(string)($row['user'] ?? '')]);
}

function manager_stat_player_group_key(array $row): string {
  $user = (string)($row['user'] ?? '');
  $name = strtolower(trim((string)($row['player'] ?? $row['name'] ?? '')));
  $name = preg_replace('/\s+/', ' ', $name) ?? $name;
  if ($name !== '') return $user . ':name:' . $name;
  $playerId = (string)($row['playerId'] ?? '');
  return $user . ':id:' . ($playerId !== '' ? $playerId : 'player');
}

function manager_group_stat_rows(array $rows, string $valueKey): array {
  $grouped = [];
  foreach ($rows as $row) {
    if (!is_array($row)) continue;
    $key = manager_stat_player_group_key($row);
    if (!isset($grouped[$key])) {
      $grouped[$key] = $row;
      $grouped[$key][$valueKey] = 0;
      if ($valueKey === 'goals') $grouped[$key]['penalties'] = 0;
    }
    if ((string)($grouped[$key]['playerId'] ?? '') === '' && (string)($row['playerId'] ?? '') !== '') $grouped[$key]['playerId'] = (string)$row['playerId'];
    if ((string)($grouped[$key]['player'] ?? '') === '' && (string)($row['player'] ?? '') !== '') $grouped[$key]['player'] = (string)$row['player'];
    if (empty($grouped[$key]['team']) && !empty($row['team'])) $grouped[$key]['team'] = $row['team'];
    $grouped[$key][$valueKey] = (int)($grouped[$key][$valueKey] ?? 0) + (int)($row[$valueKey] ?? 0);
    if ($valueKey === 'goals') {
      $grouped[$key]['penalties'] = (int)($grouped[$key]['penalties'] ?? 0) + (int)($row['penalties'] ?? 0);
    }
  }
  return array_values($grouped);
}

function manager_stat_rows_for_context(array $season, string $sourceKey, array $context, string $valueKey): array {
  $rows = array_values(array_filter($season[$sourceKey] ?? [], fn($row) => is_array($row) && manager_stat_matches_context($season, $row, $context)));
  return manager_group_stat_rows($rows, $valueKey);
}

function manager_stat_rows_for_phase(array $season, string $sourceKey, string $phase, string $valueKey): array {
  $phase = strtolower($phase);
  if ($phase === 'league') return manager_stat_rows_for_context($season, $sourceKey, manager_competition_context_from_fixture(['phase' => 'league']), $valueKey);
  $rows = array_values(array_filter($season[$sourceKey] ?? [], function ($row) use ($phase) {
    if (!is_array($row)) return false;
    $key = manager_stat_competition_key($row);
    if ($phase === 'cup') return str_starts_with($key, 'cup:');
    return $key === $phase;
  }));
  return manager_group_stat_rows($rows, $valueKey);
}

function manager_sort_scorer_rows(array &$rows): void {
  foreach ($rows as &$row) $row['penalties'] = (int)($row['penalties'] ?? 0);
  unset($row);
  usort($rows, fn($a, $b) => (((int)($b['goals'] ?? 0)) <=> ((int)($a['goals'] ?? 0))) ?: (((int)($b['penalties'] ?? 0)) <=> ((int)($a['penalties'] ?? 0))));
}

function manager_sort_assist_rows(array &$rows): void {
  usort($rows, fn($a, $b) => ((int)($b['assists'] ?? 0)) <=> ((int)($a['assists'] ?? 0)));
}

function public_manager_league_stat_tables(array $season, int $limit = 10, string $label = 'League'): array {
  $scorers = manager_stat_rows_for_phase($season, 'scorers', 'league', 'goals');
  manager_sort_scorer_rows($scorers);
  $assists = manager_stat_rows_for_phase($season, 'assists', 'league', 'assists');
  manager_sort_assist_rows($assists);
  $context = manager_competition_context_from_fixture(['phase' => 'league']);
  $context['label'] = $label;
  return [
    'statContext' => $context,
    'topScorers' => array_slice($scorers, 0, $limit),
    'topAssists' => array_slice($assists, 0, $limit),
  ];
}

function manager_public_own_player_stats(array $season, string $viewer, string $phase): array {
  if ($viewer === '') return [];
  $rows = [];
  foreach (manager_stat_rows_for_phase($season, 'scorers', $phase, 'goals') as $row) {
    if ((string)($row['user'] ?? '') !== $viewer) continue;
    $player = (string)($row['player'] ?? 'Player');
    if (!isset($rows[$player])) $rows[$player] = ['user' => $viewer, 'team' => $row['team'] ?? null, 'player' => $player, 'goals' => 0, 'assists' => 0, 'penalties' => 0];
    $rows[$player]['goals'] += (int)($row['goals'] ?? 0);
    $rows[$player]['penalties'] += (int)($row['penalties'] ?? 0);
    if (empty($rows[$player]['team']) && isset($row['team'])) $rows[$player]['team'] = $row['team'];
  }
  foreach (manager_stat_rows_for_phase($season, 'assists', $phase, 'assists') as $row) {
    if ((string)($row['user'] ?? '') !== $viewer) continue;
    $player = (string)($row['player'] ?? 'Player');
    if (!isset($rows[$player])) $rows[$player] = ['user' => $viewer, 'team' => $row['team'] ?? null, 'player' => $player, 'goals' => 0, 'assists' => 0, 'penalties' => 0];
    $rows[$player]['assists'] += (int)($row['assists'] ?? 0);
    if (empty($rows[$player]['team']) && isset($row['team'])) $rows[$player]['team'] = $row['team'];
  }
  $out = array_values(array_filter($rows, fn($row) => ((int)($row['goals'] ?? 0) + (int)($row['assists'] ?? 0)) > 0));
  usort($out, fn($a, $b) => (((int)($b['goals'] ?? 0)) <=> ((int)($a['goals'] ?? 0))) ?: (((int)($b['assists'] ?? 0)) <=> ((int)($a['assists'] ?? 0))) ?: strcasecmp((string)($a['player'] ?? ''), (string)($b['player'] ?? '')));
  return $out;
}

function manager_chat_key(array $state, array $season): string {
  return manager_season_chain_key($state, $season);
}

function manager_chat_participants(array $state, array $season): array {
  $users = [];
  $add = function (array $names) use (&$users): void {
    foreach ($names as $name) {
      $name = (string)$name;
      if ($name !== '') $users[$name] = true;
    }
  };
  $add($season['humanManagers'] ?? []);
  foreach (manager_related_current_season_ids($state, $season) as $id) {
    $candidate = $state['managerSeasons'][$id] ?? null;
    if (is_array($candidate)) $add($candidate['humanManagers'] ?? []);
  }
  $key = manager_chat_key($state, $season);
  $thread = $state['managerChats'][$key] ?? null;
  if (is_array($thread) && is_array($thread['participants'] ?? null)) $add($thread['participants']);
  return array_keys($users);
}

function manager_chat_normalize_messages(array $messages, int $limit): array {
  $seen = [];
  $out = [];
  foreach ($messages as $message) {
    if (!is_array($message)) continue;
    $id = (string)($message['id'] ?? '');
    if ($id === '') {
      $id = 'legacy:' . sha1((string)($message['user'] ?? '') . '|' . (string)($message['text'] ?? '') . '|' . (string)($message['sentAt'] ?? ''));
      $message['id'] = $id;
    }
    if (isset($seen[$id])) continue;
    $seen[$id] = true;
    $out[] = $message;
  }
  usort($out, fn($a, $b) => (((int)($a['sentAt'] ?? 0)) <=> ((int)($b['sentAt'] ?? 0))) ?: strcmp((string)($a['id'] ?? ''), (string)($b['id'] ?? '')));
  return array_slice($out, -max(1, $limit));
}

function manager_chat_source_messages(array $state, array $season, int $limit = 80): array {
  $messages = [];
  $key = manager_chat_key($state, $season);
  $thread = $state['managerChats'][$key] ?? null;
  if (is_array($thread) && is_array($thread['messages'] ?? null)) $messages = array_merge($messages, $thread['messages']);
  $ids = manager_related_current_season_ids($state, $season);
  $currentId = (string)($season['id'] ?? '');
  if ($currentId !== '' && !in_array($currentId, $ids, true)) $ids[] = $currentId;
  foreach ($ids as $id) {
    $candidate = $state['managerSeasons'][$id] ?? null;
    if (is_array($candidate) && is_array($candidate['chatMessages'] ?? null)) $messages = array_merge($messages, $candidate['chatMessages']);
  }
  if (is_array($season['chatMessages'] ?? null)) $messages = array_merge($messages, $season['chatMessages']);
  return manager_chat_normalize_messages($messages, $limit);
}

function manager_chat_store_message(array &$state, array &$season, array $entry, int $now): void {
  if (!isset($state['managerChats']) || !is_array($state['managerChats'])) $state['managerChats'] = [];
  $key = manager_chat_key($state, $season);
  $messages = manager_chat_source_messages($state, $season, 220);
  $messages[] = $entry;
  $state['managerChats'][$key] = [
    'key' => $key,
    'participants' => manager_chat_participants($state, $season),
    'messages' => manager_chat_normalize_messages($messages, 220),
    'updated' => $now,
  ];
  if (!isset($season['chatMessages']) || !is_array($season['chatMessages'])) $season['chatMessages'] = [];
  $season['chatMessages'][] = $entry;
  if (count($season['chatMessages']) > 80) $season['chatMessages'] = array_slice($season['chatMessages'], -80);
  $season['updated'] = $now;
}

function public_manager_chat_message(array $message): array {
  return [
    'id' => (string)($message['id'] ?? ''),
    'user' => (string)($message['user'] ?? ''),
    'team' => is_array($message['team'] ?? null) ? $message['team'] : null,
    'text' => (string)($message['text'] ?? ''),
    'image' => is_array($message['image'] ?? null) ? [
      'url' => (string)($message['image']['url'] ?? ''),
      'name' => (string)($message['image']['name'] ?? ''),
      'mime' => (string)($message['image']['mime'] ?? ''),
      'size' => (int)($message['image']['size'] ?? 0),
      'width' => (int)($message['image']['width'] ?? 0),
      'height' => (int)($message['image']['height'] ?? 0),
    ] : null,
    'sentAt' => (int)($message['sentAt'] ?? 0),
  ];
}

function public_manager_chat_messages(array $season, string $viewer, int $limit = 80, ?array $state = null): array {
  if ($viewer === '') return [];
  if ($state !== null) {
    if (!in_array($viewer, manager_chat_participants($state, $season), true)) return [];
    $messages = manager_chat_source_messages($state, $season, $limit);
  } else {
    if (!in_array($viewer, $season['humanManagers'] ?? [], true)) return [];
    $messages = is_array($season['chatMessages'] ?? null) ? array_slice($season['chatMessages'], -max(1, $limit)) : [];
  }
  return array_values(array_map('public_manager_chat_message', array_filter($messages, 'is_array')));
}

function manager_private_chat_key(string $a, string $b): string {
  $users = [clean_name($a), clean_name($b)];
  sort($users, SORT_STRING);
  return 'pm:' . implode(':', $users);
}

function manager_private_chat_target(array $state, string $viewer, string $target): string {
  $viewer = clean_name($viewer);
  $target = clean_name($target);
  if ($viewer === '' || !isset($state['users'][$viewer])) reply(['ok' => false, 'error' => 'not_logged_in'], 401);
  if ($target === '' || !isset($state['users'][$target])) reply(['ok' => false, 'error' => 'bad_user'], 404);
  if ($target === $viewer) reply(['ok' => false, 'error' => 'self_message'], 400);
  return $target;
}

function manager_private_chat_messages_for(array $state, string $viewer, string $target, int $limit = 80): array {
  $target = manager_private_chat_target($state, $viewer, $target);
  $key = manager_private_chat_key($viewer, $target);
  $thread = is_array($state['managerPrivateChats'][$key] ?? null) ? $state['managerPrivateChats'][$key] : [];
  $participants = array_values(array_map('clean_name', array_map('strval', $thread['participants'] ?? [$viewer, $target])));
  if (!in_array($viewer, $participants, true) || !in_array($target, $participants, true)) return [];
  $messages = manager_chat_normalize_messages(is_array($thread['messages'] ?? null) ? $thread['messages'] : [], $limit);
  return array_values(array_map('public_manager_chat_message', array_filter($messages, 'is_array')));
}

function manager_private_chat_mark_seen(array &$state, string $viewer, string $target, int $now): void {
  $target = manager_private_chat_target($state, $viewer, $target);
  $key = manager_private_chat_key($viewer, $target);
  if (!isset($state['managerPrivateChats'][$key]) || !is_array($state['managerPrivateChats'][$key])) return;
  $messages = is_array($state['managerPrivateChats'][$key]['messages'] ?? null) ? $state['managerPrivateChats'][$key]['messages'] : [];
  $latest = 0;
  foreach ($messages as $message) {
    if (!is_array($message)) continue;
    $latest = max($latest, (int)($message['sentAt'] ?? 0));
  }
  if (!isset($state['managerPrivateChats'][$key]['seen']) || !is_array($state['managerPrivateChats'][$key]['seen'])) $state['managerPrivateChats'][$key]['seen'] = [];
  $state['managerPrivateChats'][$key]['seen'][$viewer] = $latest;
}

function manager_private_chat_store_message(array &$state, string $from, string $target, array $entry, int $now): void {
  $target = manager_private_chat_target($state, $from, $target);
  if (!isset($state['managerPrivateChats']) || !is_array($state['managerPrivateChats'])) $state['managerPrivateChats'] = [];
  $key = manager_private_chat_key($from, $target);
  $thread = is_array($state['managerPrivateChats'][$key] ?? null) ? $state['managerPrivateChats'][$key] : [];
  $messages = manager_chat_normalize_messages(is_array($thread['messages'] ?? null) ? $thread['messages'] : [], 220);
  $messages[] = $entry;
  $seen = is_array($thread['seen'] ?? null) ? $thread['seen'] : [];
  $seen[$from] = $now;
  $state['managerPrivateChats'][$key] = [
    'key' => $key,
    'participants' => array_values(array_unique([$from, $target])),
    'messages' => manager_chat_normalize_messages($messages, 220),
    'seen' => $seen,
    'updated' => $now,
  ];
}

function public_manager_private_chat_alerts(array $state, string $viewer): array {
  $viewer = clean_name($viewer);
  if ($viewer === '') return [];
  $alerts = [];
  foreach (($state['managerPrivateChats'] ?? []) as $thread) {
    if (!is_array($thread)) continue;
    $participants = array_values(array_filter(array_map('clean_name', array_map('strval', $thread['participants'] ?? [])), fn($name) => $name !== ''));
    if (!in_array($viewer, $participants, true)) continue;
    $target = '';
    foreach ($participants as $name) {
      if ($name !== $viewer) {
        $target = $name;
        break;
      }
    }
    if ($target === '') continue;
    $seenAt = (int)($thread['seen'][$viewer] ?? 0);
    $latest = 0;
    $count = 0;
    foreach (($thread['messages'] ?? []) as $message) {
      if (!is_array($message)) continue;
      $sentAt = (int)($message['sentAt'] ?? 0);
      $latest = max($latest, $sentAt);
      if ((string)($message['user'] ?? '') !== $viewer && $sentAt > $seenAt) $count++;
    }
    if ($count <= 0) continue;
    $account = is_array($state['users'][$target] ?? null) ? $state['users'][$target] : [];
    $alerts[] = [
      'user' => $target,
      'count' => $count,
      'lastAt' => $latest,
      'team' => has_team($account) ? user_team($account) : null,
    ];
  }
  usort($alerts, fn($a, $b) => ((int)($b['lastAt'] ?? 0) <=> (int)($a['lastAt'] ?? 0)) ?: strcmp((string)($a['user'] ?? ''), (string)($b['user'] ?? '')));
  return $alerts;
}

function manager_chat_text(string $text): string {
  $text = trim(strip_tags($text));
  $text = preg_replace('/\s+/', ' ', $text) ?? '';
  if (function_exists('mb_substr')) return mb_substr($text, 0, 500);
  return substr($text, 0, 500);
}

function manager_chat_upload_dir(): string {
  return YSP_CODE_ROOT . '/uploads/manager-chat';
}

function manager_chat_upload_url(string $file): string {
  return 'uploads/manager-chat/' . rawurlencode($file);
}

function manager_chat_image_name(string $name): string {
  $name = trim(basename($name));
  $name = preg_replace('/[^a-zA-Z0-9._ -]+/', '', $name) ?? '';
  if (function_exists('mb_substr')) $name = mb_substr($name, 0, 90);
  else $name = substr($name, 0, 90);
  return $name !== '' ? $name : 'chat image';
}

function manager_chat_decode_image(array $data, int $now): array {
  $dataUrl = (string)($data['dataUrl'] ?? '');
  if (!preg_match('/^data:(image\/(?:png|jpe?g|webp|gif));base64,([A-Za-z0-9+\/=\r\n]+)$/', $dataUrl, $m)) reply(['ok' => false, 'error' => 'bad_image'], 400);
  $mime = strtolower($m[1]);
  if ($mime === 'image/jpg') $mime = 'image/jpeg';
  $raw = base64_decode(preg_replace('/\s+/', '', $m[2]) ?? '', true);
  if (!is_string($raw) || $raw === '') reply(['ok' => false, 'error' => 'bad_image'], 400);
  $max = 5 * 1024 * 1024;
  if (strlen($raw) > $max) reply(['ok' => false, 'error' => 'image_too_large', 'maxBytes' => $max], 413);
  $info = @getimagesizefromstring($raw);
  if (!is_array($info) || empty($info['mime'])) reply(['ok' => false, 'error' => 'bad_image'], 400);
  $detected = strtolower((string)$info['mime']);
  if ($detected === 'image/jpg') $detected = 'image/jpeg';
  $extByMime = ['image/png' => 'png', 'image/jpeg' => 'jpg', 'image/webp' => 'webp', 'image/gif' => 'gif'];
  if (!isset($extByMime[$detected]) || $detected !== $mime) reply(['ok' => false, 'error' => 'bad_image_type'], 400);
  $dir = manager_chat_upload_dir();
  if (!is_dir($dir) && !@mkdir($dir, 0775, true) && !is_dir($dir)) reply(['ok' => false, 'error' => 'upload_failed'], 500);
  $file = 'chat_' . $now . '_' . bin2hex(random_bytes(8)) . '.' . $extByMime[$detected];
  $path = $dir . '/' . $file;
  if (@file_put_contents($path, $raw, LOCK_EX) === false) reply(['ok' => false, 'error' => 'upload_failed'], 500);
  @chmod($path, 0664);
  return [
    'url' => manager_chat_upload_url($file),
    'name' => manager_chat_image_name((string)($data['name'] ?? 'chat image')),
    'mime' => $detected,
    'size' => strlen($raw),
    'width' => (int)($info[0] ?? 0),
    'height' => (int)($info[1] ?? 0),
  ];
}

function public_manager_season(?array $season, ?array $state = null, string $viewer = ''): ?array {
  if (!$season) return null;
  $participants = $season['participants'] ?? [];
  $names = [];
  $previews = [];
  foreach (array_merge($participants, $season['cupParticipants'] ?? []) as $participant) {
    $id = (string)$participant['user'];
    if (isset($names[$id])) continue;
    $preview = manager_side_preview($state, $season, $id);
    $previews[$id] = $preview;
    $names[$id] = [
      'user' => $id,
      'managerType' => (string)($participant['managerType'] ?? (is_cpu_manager($id) ? 'cpu' : 'human')),
      'team' => $preview['team'],
      'rating' => $preview['rating'],
      'rawStrength' => $preview['strength'],
      'matchRawStrength' => $preview['matchStrength'] ?? $preview['strength'],
      'strength' => manager_display_skill((int)$preview['strength']),
      'divisionLevel' => (int)($participant['divisionLevel'] ?? 5),
      'divisionName' => (string)($participant['divisionName'] ?? manager_division_name((int)($participant['divisionLevel'] ?? 5))),
    ];
  }
  $publicParticipants = [];
  foreach ($participants as $participant) {
    $id = (string)($participant['user'] ?? '');
    if ($id === '' || !isset($names[$id])) continue;
    $publicParticipants[] = [
      'user' => $id,
      'managerType' => $names[$id]['managerType'],
      'team' => $names[$id]['team'],
      'rating' => $names[$id]['rating'],
      'strength' => $names[$id]['strength'],
      'rawStrength' => $names[$id]['rawStrength'] ?? null,
      'divisionLevel' => $names[$id]['divisionLevel'],
      'divisionName' => $names[$id]['divisionName'],
    ];
  }
  $statContext = manager_competition_context_from_fixture(['phase' => 'league']);
  $scorers = manager_stat_rows_for_phase($season, 'scorers', 'league', 'goals');
  manager_sort_scorer_rows($scorers);
  $cupScorers = manager_stat_rows_for_phase($season, 'scorers', 'cup', 'goals');
  manager_sort_scorer_rows($cupScorers);
  $assists = manager_stat_rows_for_phase($season, 'assists', 'league', 'assists');
  manager_sort_assist_rows($assists);
  $cupAssists = manager_stat_rows_for_phase($season, 'assists', 'cup', 'assists');
  manager_sort_assist_rows($cupAssists);
  $nextRound = null;
  foreach (($season['rounds'] ?? []) as $round) {
    foreach (($round['fixtures'] ?? []) as $fixture) {
      if (($fixture['status'] ?? '') === 'pending') {
        $nextRound = $round['round'] ?? 0;
        break 2;
      }
    }
  }
  $rounds = $season['rounds'] ?? [];
  $start = 0;
  if ($nextRound !== null) {
    foreach ($rounds as $i => $round) {
      if (($round['round'] ?? 0) === $nextRound) {
        $start = max(0, $i - 1);
        break;
      }
    }
  }
  $publicRounds = [];
  $lastMatch = null;
  $humanLastMatches = [];
  $liveSimulations = [];
  $liveSimulationKeys = [];
  $humanManagers = array_values(array_filter(array_map('strval', $season['humanManagers'] ?? []), fn($name) => $name !== ''));
  foreach ($rounds as $roundIndex => $round) {
    $roundOut = $round;
    $roundOut['current'] = $roundIndex >= $start && $roundIndex < $start + 8;
    $roundOut['playoff'] = (string)($round['phase'] ?? '') === 'playoff';
    $roundOut['fixtures'] = [];
    foreach (($round['fixtures'] ?? []) as $fixtureIndex => $fixture) {
      $fixtureOut = public_manager_fixture($fixture, $names, $state, $season, $viewer, (int)$roundIndex, (int)$fixtureIndex, $previews);
      $fixtureOut['roundIndex'] = $roundIndex;
      $fixtureOut['fixtureIndex'] = $fixtureIndex;
      $roundOut['fixtures'][] = $fixtureOut;
      if (!empty($fixtureOut['resultMasked'])) {
        $live = ['round' => $round['round'] ?? '', 'fixture' => $fixtureOut, 'revealIn' => (int)($fixtureOut['resultRevealIn'] ?? 0)];
        $report = public_manager_live_match_report($season, $fixture, (int)$roundIndex, (int)$fixtureIndex);
        if ($report !== null) $live['report'] = $report;
        $live['sourceSeasonId'] = (string)($season['id'] ?? '');
        $live['divisionName'] = (string)($season['divisionName'] ?? manager_division_name((int)($season['divisionLevel'] ?? 5)));
        $live['phase'] = (string)($round['phase'] ?? $fixtureOut['phase'] ?? 'league');
        $live['playoff'] = !empty($round['playoff']) || (string)($round['phase'] ?? '') === 'playoff';
        $live['cupName'] = (string)($round['cupName'] ?? $fixtureOut['cupName'] ?? '');
        $live['humans'] = manager_fixture_human_users($season, $fixture);
        $live['duration'] = MANAGER_SIM_RESULT_REVEAL_SECONDS;
        $liveSimulations[] = $live;
        $liveSimulationKeys[(string)($season['id'] ?? '') . ':' . (int)$roundIndex . ':' . (int)$fixtureIndex] = true;
      }
      if (($fixtureOut['status'] ?? '') === 'played' && ($viewer === '' || !empty($fixtureOut['mine']))) {
        $lastMatch = ['round' => $round['round'] ?? '', 'fixture' => $fixtureOut];
      }
      if (($fixtureOut['status'] ?? '') === 'played') {
        foreach ($humanManagers as $human) {
          if ((string)($fixtureOut['home'] ?? '') !== $human && (string)($fixtureOut['away'] ?? '') !== $human) continue;
          $humanLastMatches[$human] = ['user' => $human, 'round' => $round['round'] ?? '', 'fixture' => $fixtureOut];
        }
      }
    }
    $publicRounds[] = $roundOut;
  }
  if ($state !== null) {
    $liveSimulations = array_merge($liveSimulations, public_manager_live_simulation_rows_from_related($state, $season, $viewer, $liveSimulationKeys));
  }
  $liveSimulations = manager_filter_public_live_simulations($liveSimulations);
  $rules = manager_promotion_rules((int)($season['divisionLevel'] ?? 5));
  $table = public_manager_table($season, $viewer);
  foreach ($table as &$row) {
    $id = (string)($row['user'] ?? '');
    $row['mine'] = $viewer !== '' && $id === $viewer;
    $leagueForm = manager_league_form_state($season, $id);
    $row['formTrend'] = $leagueForm['trend'] ?? '';
    $row['formBonusPct'] = (int)($leagueForm['pct'] ?? 0);
    $row['formRecent'] = $leagueForm['recent'] ?? [];
    if (isset($names[$id])) {
      $row['team'] = $names[$id]['team'];
      $row['rating'] = $names[$id]['rating'];
      $row['strength'] = $names[$id]['strength'];
      $row['rawStrength'] = $names[$id]['rawStrength'] ?? null;
    }
  }
  unset($row);
  $seasonYear = manager_season_year($season, $state);
  $saveUsers = $state !== null ? manager_current_save_users($state, $viewer) : array_values($season['humanManagers'] ?? []);
  $publicCups = manager_public_cups($season, $state);
  $publicCup = $season['cup'] ?? null;
  if (is_array($publicCup)) {
    $publicCupKey = (string)($publicCup['key'] ?? '');
    foreach ($publicCups as $cupRow) {
      if ($publicCupKey !== '' && (string)($cupRow['key'] ?? '') === $publicCupKey) {
        $publicCup = $cupRow;
        break;
      }
    }
  }
  return [
    'id' => $season['id'] ?? '',
    'managerGameId' => (string)($season['managerGameId'] ?? $season['saveGameId'] ?? ''),
    'status' => $season['status'] ?? 'active',
    'started' => manager_season_has_started($season),
    'seasonYear' => $seasonYear,
    'startBalance' => manager_viewer_start_balance($season, $state, $viewer),
    'financeStart' => manager_viewer_finance_start($season, $viewer),
    'createdBy' => $season['createdBy'] ?? '',
    'divisionLevel' => $season['divisionLevel'] ?? 5,
    'divisionName' => $season['divisionName'] ?? 'National League',
    'participantCount' => count($participants),
    'humanManagers' => array_values($season['humanManagers'] ?? []),
    'saveUsers' => $saveUsers,
    'participants' => $publicParticipants,
    'table' => $table,
    'rounds' => $publicRounds,
    'cup' => $publicCup,
    'cups' => $publicCups,
    'lastMatch' => $lastMatch,
    'humanLastMatches' => $humanLastMatches,
    'liveSimulations' => $liveSimulations,
    'promotionRules' => $rules,
    'pointDeductions' => $season['pointDeductions'] ?? [],
    'playoffActive' => ($season['status'] ?? '') === 'playoff',
    'statContext' => $statContext,
    'topScorers' => array_slice($scorers, 0, 10),
    'topCupScorers' => array_slice($cupScorers, 0, 10),
    'topAssists' => array_slice($assists, 0, 10),
    'topCupAssists' => array_slice($cupAssists, 0, 10),
    'ownLeaguePlayerStats' => manager_public_own_player_stats($season, $viewer, 'league'),
    'ownCupPlayerStats' => manager_public_own_player_stats($season, $viewer, 'cup'),
    'chatMessages' => public_manager_chat_messages($season, $viewer, 80, $state),
    'matchReports' => public_manager_match_reports($season, $viewer),
    'nextRound' => $nextRound,
    'outcome' => $season['outcome'] ?? null,
    'seasonReport' => ($season['status'] ?? '') === 'finished' ? manager_season_report($season, $names, $state) : null,
    'jobOffers' => public_manager_job_offers($season, $viewer),
    'teamsByUser' => $names,
    'simVote' => public_manager_sim_vote($season, $viewer),
    'watchVote' => public_manager_watch_vote($season, $viewer),
    'simSeasonVote' => public_manager_sim_season_vote($season, $viewer),
    'injuryBlock' => ($state !== null && $viewer !== '') ? public_manager_injury_report($state, $season, $viewer) : null,
    'continueWait' => (($season['status'] ?? '') === 'finished' && $state !== null && $viewer !== '') ? manager_continue_waiting_users($state, $season, $viewer) : [],
  ];
}

function manager_turn_alert_vote_reason(?array $vote, string $user): ?string {
  if (!$vote || $user === '') return null;
  $waiting = array_values(array_map('clean_name', array_map('strval', $vote['waiting'] ?? [])));
  $readyCount = (int)($vote['readyCount'] ?? count($vote['ready'] ?? []));
  if ($readyCount > 0 && in_array($user, $waiting, true)) return (string)($vote['mode'] ?? 'ready');
  return null;
}

function manager_turn_alert_reason(array $state, array $season, string $user): ?string {
  $user = clean_name($user);
  if ($user === '' || !manager_season_visible_to_user($season, $user)) return null;
  if (($season['status'] ?? '') === 'aborted') return null;
  if (count(array_values($season['humanManagers'] ?? [])) <= 1) return null;
  if (($season['status'] ?? '') === 'finished') {
    $seasonId = (string)($season['id'] ?? '');
    $targetLevel = manager_continue_target_level($state, $season, $user);
    if ($seasonId !== '' && continued_manager_season_id($state, $seasonId, $user, $targetLevel) !== null) return null;
    return manager_continue_waiting_users($state, $season, $user) ? null : 'nextSeason';
  }
  return manager_turn_alert_vote_reason(public_manager_sim_vote($season, $user), $user)
    ?? manager_turn_alert_vote_reason(public_manager_watch_vote($season, $user), $user)
    ?? manager_turn_alert_vote_reason(public_manager_sim_season_vote($season, $user), $user);
}

function manager_turn_alert_save_meta(string $user, string $gameId): ?array {
  if ($user === '' || $gameId === '') return null;
  $branches = manager_latest_game_save_metas_by_user([$user], $gameId);
  $meta = $branches[$user] ?? null;
  return is_array($meta) ? manager_enrich_online_save_meta($meta) : null;
}

function manager_turn_alert_label(?array $meta, array $season): string {
  $users = array_values(array_filter(array_map('strval', $meta['users'] ?? $season['humanManagers'] ?? []), fn($name) => trim($name) !== ''));
  $mode = (string)($meta['modeLabel'] ?? strtoupper((string)($meta['mode'] ?? (count($users) > 1 ? 'ONLINE' : 'SOLO'))));
  $division = (string)($meta['divisionName'] ?? $season['divisionName'] ?? 'Manager');
  $year = (int)($meta['seasonYear'] ?? manager_season_year($season));
  $parts = array_filter([$mode, $users ? implode(' + ', $users) : 'Manager', $division, $year > 0 ? (string)$year : '']);
  return implode(' | ', $parts);
}

function manager_turn_alerts(array $state, string $user): array {
  $user = clean_name($user);
  if ($user === '') return [];
  $currentId = (string)(current_manager_season_id($state, $user) ?? '');
  $currentGameId = '';
  if ($currentId !== '' && is_array($state['managerSeasons'][$currentId] ?? null)) $currentGameId = manager_game_id($state['managerSeasons'][$currentId]);
  $latestByGame = [];
  foreach (($state['managerSeasons'] ?? []) as $seasonId => $season) {
    if (!is_array($season) || ($season['status'] ?? '') === 'aborted' || !manager_season_visible_to_user($season, $user)) continue;
    $gameId = manager_game_id($season);
    $key = $gameId !== '' ? $gameId : (string)$seasonId;
    $score = manager_current_season_score($state, $season);
    if (!isset($latestByGame[$key]) || $score > (int)($latestByGame[$key]['score'] ?? -1)) {
      $latestByGame[$key] = ['seasonId' => (string)$seasonId, 'score' => $score];
    }
  }
  $alerts = [];
  foreach (($state['managerSeasons'] ?? []) as $seasonId => $season) {
    if (!is_array($season)) continue;
    $seasonId = (string)$seasonId;
    if ($seasonId === $currentId) continue;
    $gameId = manager_game_id($season);
    $gameKey = $gameId !== '' ? $gameId : $seasonId;
    if (($latestByGame[$gameKey]['seasonId'] ?? $seasonId) !== $seasonId) continue;
    if ($gameId !== '' && $currentGameId !== '' && $gameId === $currentGameId) continue;
    $reason = manager_turn_alert_reason($state, $season, $user);
    if ($reason === null) continue;
    $key = $gameId !== '' ? $gameId : $seasonId;
    $meta = manager_turn_alert_save_meta($user, $gameId);
    $row = [
      'id' => $key,
      'managerGameId' => $gameId,
      'seasonId' => $seasonId,
      'saveId' => (string)($meta['file'] ?? ''),
      'label' => manager_turn_alert_label($meta, $season),
      'reason' => $reason,
      'updated' => max((int)($season['updated'] ?? 0), (int)($meta['savedAt'] ?? 0)),
    ];
    if (!isset($alerts[$key]) || (int)$row['updated'] > (int)($alerts[$key]['updated'] ?? 0)) $alerts[$key] = $row;
  }
  foreach (public_manager_saves($user) as $meta) {
    $turn = is_array($meta['turnAlerts'] ?? null) ? ($meta['turnAlerts'][$user] ?? null) : null;
    if (!is_array($turn)) continue;
    $gameId = (string)($turn['managerGameId'] ?? $meta['managerGameId'] ?? '');
    if ($gameId !== '' && $currentGameId !== '' && $gameId === $currentGameId) continue;
    $seasonId = (string)($turn['seasonId'] ?? $meta['seasonId'] ?? '');
    if ($seasonId !== '' && $seasonId === $currentId) continue;
    $key = $gameId !== '' ? $gameId : ((string)($meta['saveGroupKey'] ?? $meta['file'] ?? $seasonId));
    $row = [
      'id' => $key,
      'managerGameId' => $gameId,
      'seasonId' => $seasonId,
      'saveId' => (string)($meta['file'] ?? ''),
      'label' => manager_turn_alert_label($meta, ['humanManagers' => $meta['users'] ?? [], 'divisionName' => $meta['divisionName'] ?? '', 'seasonYear' => $meta['seasonYear'] ?? 0]),
      'reason' => (string)($turn['reason'] ?? 'save'),
      'updated' => (int)($meta['savedAt'] ?? 0),
    ];
    if (!isset($alerts[$key]) || (int)$row['updated'] > (int)($alerts[$key]['updated'] ?? 0)) $alerts[$key] = $row;
  }
  usort($alerts, fn($a, $b) => ((int)($b['updated'] ?? 0) <=> (int)($a['updated'] ?? 0)) ?: strcmp((string)($a['label'] ?? ''), (string)($b['label'] ?? '')));
  return array_slice(array_values($alerts), 0, 8);
}

function manager_status_hash(mixed $value): string {
  $json = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
  return sha1(is_string($json) ? $json : serialize($value));
}

function manager_user_revision_context(array $state, string $user): array {
  $account = is_array($state['users'][$user] ?? null) ? $state['users'][$user] : [];
  $squad = is_array($account['squad'] ?? null) ? $account['squad'] : [];
  $players = [];
  foreach (['starters', 'bench'] as $group) {
    foreach ((array)($squad[$group] ?? []) as $player) {
      if (!is_array($player)) continue;
      $players[] = [
        'g' => $group,
        'id' => (string)($player['id'] ?? ''),
        'name' => (string)($player['name'] ?? ''),
        'role' => (string)($player['role'] ?? $player['slotRole'] ?? ''),
        'rating' => (int)($player['rating'] ?? $player['effectiveRating'] ?? 0),
        'injury' => (int)($player['injuryUntilRound'] ?? $player['injuredUntilRound'] ?? 0),
        'suspended' => (int)($player['suspendedUntilRound'] ?? 0),
      ];
    }
  }
  return [
    'teamId' => (string)($account['teamId'] ?? ''),
    'teamName' => (string)($account['teamName'] ?? ''),
    'finance' => is_array($account['managerFinance'] ?? null) ? $account['managerFinance'] : [],
    'squad' => $players,
  ];
}

function manager_market_revision_context(array $state): array {
  $items = [];
  foreach (($state['managerMarket'] ?? []) as $player) {
    if (!is_array($player)) continue;
    $items[] = [
      'id' => (string)($player['id'] ?? ''),
      'name' => (string)($player['name'] ?? ''),
      'seller' => clean_name((string)($player['marketListedByUser'] ?? $player['seller'] ?? '')),
      'price' => (int)($player['marketSalePrice'] ?? $player['price'] ?? 0),
      'listedAt' => (int)($player['marketListedAt'] ?? 0),
      'soldAt' => (int)($player['soldAt'] ?? 0),
    ];
  }
  return $items;
}

function manager_transfer_revision_context(array $state, string $user): array {
  $offers = function_exists('public_manager_transfer_offers') ? public_manager_transfer_offers($state, $user) : ['incoming' => [], 'outgoing' => [], 'notices' => []];
  $slim = [];
  foreach (['incoming', 'outgoing', 'notices'] as $group) {
    $slim[$group] = [];
    foreach ((array)($offers[$group] ?? []) as $row) {
      if (!is_array($row)) continue;
      $player = is_array($row['player'] ?? null) ? $row['player'] : [];
      $slim[$group][] = [
        'id' => (string)($row['id'] ?? $row['offerId'] ?? ''),
        'status' => (string)($row['status'] ?? ''),
        'created' => (int)($row['created'] ?? 0),
        'updated' => (int)($row['updated'] ?? $row['created'] ?? 0),
        'playerId' => (string)($player['id'] ?? $row['playerId'] ?? ''),
        'playerName' => (string)($player['name'] ?? $row['playerName'] ?? ''),
      ];
    }
  }
  return $slim;
}

function manager_season_revision_context(?array $season): array {
  if (!$season) return [];
  $fixtures = [];
  $roundIndex = manager_pending_round_index($season);
  if ($roundIndex !== null) {
    foreach ((array)($season['rounds'][$roundIndex]['fixtures'] ?? []) as $index => $fixture) {
      if (!is_array($fixture)) continue;
      $fixtures[] = [
        'i' => (int)$index,
        'status' => (string)($fixture['status'] ?? ''),
        'score' => $fixture['score'] ?? null,
        'updated' => (int)($fixture['updated'] ?? 0),
      ];
    }
  }
  return [
    'id' => (string)($season['id'] ?? ''),
    'status' => (string)($season['status'] ?? ''),
    'updated' => (int)($season['updated'] ?? $season['created'] ?? 0),
    'created' => (int)($season['created'] ?? 0),
    'year' => (int)($season['seasonYear'] ?? 0),
    'divisionLevel' => (int)($season['divisionLevel'] ?? 0),
    'pendingRound' => $roundIndex,
    'humanManagers' => array_values(array_map('clean_name', array_map('strval', $season['humanManagers'] ?? []))),
    'roundContinues' => $season['roundContinues'] ?? [],
    'simVotes' => $season['simVotes'] ?? [],
    'watchVotes' => $season['watchVotes'] ?? [],
    'simSeasonVotes' => $season['simSeasonVotes'] ?? [],
    'fixtures' => $fixtures,
  ];
}

function public_manager_status(array $state, string $user): array {
  $user = clean_name($user);
  $seasonId = current_manager_season_id($state, $user);
  $season = $seasonId !== null && is_array($state['managerSeasons'][$seasonId] ?? null) ? $state['managerSeasons'][$seasonId] : null;
  $managerUsers = public_manager_users($state);
  $managerInvites = public_manager_invites($state, $user);
  $managerSaveUsers = manager_current_save_users($state, $user);
  $managerTurnAlerts = manager_turn_alerts($state, $user);
  $managerPrivateChatAlerts = public_manager_private_chat_alerts($state, $user);
  $revisionContext = [
    'seasonId' => (string)($seasonId ?? ''),
    'season' => manager_season_revision_context($season),
    'users' => array_map(fn($row) => [
      'name' => (string)($row['name'] ?? ''),
      'ready' => !empty($row['ready']),
      'team' => (string)($row['team']['name'] ?? ''),
    ], $managerUsers),
    'saveUsers' => $managerSaveUsers,
    'canContinue' => manager_user_can_continue($state, $user),
    'hasAnySave' => manager_user_has_any_save($user),
    'invites' => $managerInvites,
    'turnAlerts' => $managerTurnAlerts,
    'privateChatAlerts' => $managerPrivateChatAlerts,
    'user' => manager_user_revision_context($state, $user),
    'market' => manager_market_revision_context($state),
    'transfers' => manager_transfer_revision_context($state, $user),
  ];
  return [
    'managerRevision' => manager_status_hash($revisionContext),
    'managerUsers' => $managerUsers,
    'managerInvites' => $managerInvites,
    'managerHasSeason' => $season !== null,
    'managerSeasonStarted' => $season !== null ? manager_season_has_started($season) : false,
    'managerSaveUsers' => $managerSaveUsers,
    'managerCanContinue' => manager_user_can_continue($state, $user),
    'managerHasAnySave' => manager_user_has_any_save($user),
    'managerTurnAlerts' => $managerTurnAlerts,
    'managerPrivateChatAlerts' => $managerPrivateChatAlerts,
    'appVersion' => app_version(),
  ];
}

function manager_log(array &$state, string $event, array $data = []): void {
  $entry = ['at' => time(), 'event' => $event, 'data' => $data];
  if (!isset($state['managerLog']) || !is_array($state['managerLog'])) $state['managerLog'] = [];
  $state['managerLog'][] = $entry;
  if (count($state['managerLog']) > 400) $state['managerLog'] = array_slice($state['managerLog'], -400);
  @file_put_contents(YSP_CODE_ROOT . '/.manager.log', json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL, FILE_APPEND);
}

function client_debug_slim(mixed $value, int $depth = 0): mixed {
  if ($depth > 4) return '[depth]';
  if (is_string($value)) return mb_substr($value, 0, 500);
  if (is_int($value) || is_float($value) || is_bool($value) || $value === null) return $value;
  if (!is_array($value)) return '[value]';
  $out = [];
  $count = 0;
  foreach ($value as $key => $item) {
    if ($count++ >= 40) {
      $out['_truncated'] = true;
      break;
    }
    $out[is_int($key) ? $key : mb_substr((string)$key, 0, 80)] = client_debug_slim($item, $depth + 1);
  }
  return $out;
}

function client_debug_log(array $data): array {
  $events = $data['events'] ?? [];
  if (!is_array($events)) return ['count' => 0, 'bytes' => 0, 'error' => 'bad_events'];
  $sessionUser = $_SESSION['user'] ?? '';
  $base = [
    'serverAt' => time(),
    'sessionUser' => is_string($sessionUser) ? clean_name($sessionUser) : '',
    'ip' => mb_substr((string)($_SERVER['REMOTE_ADDR'] ?? ''), 0, 80),
    'ua' => mb_substr((string)($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 180),
    'appVersion' => app_version(),
  ];
  $lines = [];
  $entries = [];
  foreach (array_slice($events, -80) as $event) {
    if (!is_array($event)) continue;
    $entry = $base + [
      'clientAt' => mb_substr((string)($event['at'] ?? ''), 0, 80),
      'type' => mb_substr((string)($event['type'] ?? 'debug'), 0, 80),
      'scope' => mb_substr((string)($event['scope'] ?? ''), 0, 80),
      'message' => mb_substr((string)($event['message'] ?? ''), 0, 500),
      'data' => client_debug_slim($event['data'] ?? []),
    ];
    $entries[] = $entry;
    $lines[] = json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
  }
  $path = YSP_CODE_ROOT . '/.client-debug.log';
  $bytes = $lines ? @file_put_contents($path, implode(PHP_EOL, $lines) . PHP_EOL, FILE_APPEND | LOCK_EX) : 0;
  return ['count' => count($lines), 'bytes' => $bytes === false ? 0 : (int)$bytes, 'path' => $path, 'writable' => is_writable($path), 'entries' => $entries];
}

function manager_save_dir(): string {
  if (!is_dir(MANAGER_SAVE_DIR)) @mkdir(MANAGER_SAVE_DIR, 0775, true);
  if (is_dir(MANAGER_SAVE_DIR)) @chmod(MANAGER_SAVE_DIR, 0777);
  return MANAGER_SAVE_DIR;
}

function manager_save_meta_dir(): string {
  $dir = manager_save_dir() . '/.meta';
  if (!is_dir($dir)) @mkdir($dir, 0775, true);
  if (is_dir($dir)) @chmod($dir, 0777);
  return $dir;
}

function manager_save_backup_dir(): string {
  if (!is_dir(MANAGER_SAVE_BACKUP_DIR)) @mkdir(MANAGER_SAVE_BACKUP_DIR, 0775, true);
  if (is_dir(MANAGER_SAVE_BACKUP_DIR)) @chmod(MANAGER_SAVE_BACKUP_DIR, 0777);
  return MANAGER_SAVE_BACKUP_DIR;
}

function manager_save_backup_subdir(string $name): string {
  $name = preg_replace('/[^a-z0-9_-]+/i', '-', strtolower(trim($name))) ?: 'misc';
  $dir = manager_save_backup_dir() . '/' . $name;
  if (!is_dir($dir)) @mkdir($dir, 0775, true);
  if (is_dir($dir)) @chmod($dir, 0777);
  return $dir;
}

function manager_gzip_copy_file(string $source, string $target): bool {
  $data = @file_get_contents($source);
  if (!is_string($data)) return false;
  $encoded = function_exists('gzencode') ? @gzencode($data, MANAGER_SAVE_BACKUP_GZIP_LEVEL) : false;
  if (!is_string($encoded)) return false;
  if (@file_put_contents($target, $encoded, LOCK_EX) === false) return false;
  @chmod($target, 0666);
  return true;
}

function manager_backup_original_filename(string $file): string {
  $name = basename($file);
  if (str_ends_with($name, '.gz')) $name = substr($name, 0, -3);
  if (preg_match('/^\d{8}-\d{6}\.[a-z0-9_-]+\.([a-z0-9_.-]+\.json(?:\.\d+)?)$/i', $name, $m)) return $m[1];
  return $name;
}

function manager_backup_group_key(string $file): string {
  $name = manager_backup_original_filename($file);
  $name = preg_replace('/\.json$/i', '', $name);
  $key = preg_replace('/\.\d{8}-\d{6}(?:\.\d+)?$/', '', $name);
  return is_string($key) && $key !== '' ? $key : $name;
}

function manager_prune_backup_files(string $dir, int $limit = MANAGER_SAVE_BACKUP_KEEP_PER_GROUP): void {
  $limit = max(1, $limit);
  if (!is_dir($dir)) return;
  $groups = [];
  foreach ((glob($dir . '/*.json') ?: []) as $file) {
    if (is_file($file)) $groups[manager_backup_group_key($file)][] = $file;
  }
  foreach ((glob($dir . '/*.json.gz') ?: []) as $file) {
    if (is_file($file)) $groups[manager_backup_group_key($file)][] = $file;
  }
  foreach ($groups as $files) {
    usort($files, fn($a, $b) => ((int)@filemtime($b) <=> (int)@filemtime($a)) ?: strcmp(basename($b), basename($a)));
    foreach (array_slice($files, $limit) as $stale) @unlink($stale);
  }
}

function manager_backup_save_file(string $file, string $reason = 'backup'): bool {
  if (!is_file($file) || !str_ends_with(basename($file), '.json')) return false;
  $liveDir = manager_save_backup_subdir('live');
  $archiveDir = manager_save_backup_subdir('archive');
  if (!is_dir($liveDir) || !is_dir($archiveDir)) return false;
  $liveTarget = $liveDir . '/' . basename($file) . '.gz';
  $liveOk = manager_gzip_copy_file($file, $liveTarget);
  $reason = preg_replace('/[^a-z0-9_-]+/i', '-', strtolower(trim($reason))) ?: 'backup';
  $base = date('Ymd-His') . '.' . $reason . '.' . basename($file) . '.gz';
  $target = $archiveDir . '/' . $base;
  for ($i = 2; is_file($target); $i++) {
    $target = $archiveDir . '/' . preg_replace('/\.json\.gz$/', '.' . $i . '.json.gz', $base);
  }
  $archiveOk = manager_gzip_copy_file($file, $target);
  manager_prune_backup_files($liveDir);
  manager_prune_backup_files($archiveDir);
  return (bool)($liveOk && $archiveOk);
}

function manager_save_users(array $season): array {
  $users = [];
  foreach (($season['humanManagers'] ?? []) as $name) {
    $name = clean_name((string)$name);
    if ($name !== '' && !in_array($name, $users, true)) $users[] = $name;
  }
  return $users;
}

function manager_save_filename(array $users, int $savedAt, string $seasonId = ''): string {
  $baseUsers = implode('.', array_map(fn($name) => preg_replace('/[^a-z0-9_-]+/', '', clean_name((string)$name)) ?: 'user', $users));
  if ($baseUsers === '') $baseUsers = 'manager';
  $seasonPart = preg_replace('/[^a-z0-9_-]+/i', '', $seasonId);
  $base = $baseUsers . ($seasonPart !== '' ? '.' . $seasonPart : '') . '.' . date('Ymd-His', $savedAt);
  $dir = manager_save_dir();
  $file = $base . '.json';
  $i = 2;
  while (is_file($dir . '/' . $file)) $file = $base . '.' . $i++ . '.json';
  return $file;
}

function manager_save_user_snapshot(array $state, array $users, array $seasons = [], array $currentByUser = []): array {
  $out = [];
  foreach ($users as $name) {
    $name = clean_name((string)$name);
    if ($name === '') continue;
    $user = null;
    $candidateSeasons = [];
    $currentId = (string)($currentByUser[$name] ?? '');
    if ($currentId !== '' && is_array($seasons[$currentId] ?? null)) $candidateSeasons[$currentId] = $seasons[$currentId];
    foreach ($seasons as $seasonId => $season) {
      if (!is_array($season) || isset($candidateSeasons[(string)$seasonId])) continue;
      if (manager_participant($season, $name) !== null) $candidateSeasons[(string)$seasonId] = $season;
    }
    foreach ($candidateSeasons as $season) {
      if (is_array($state['users'][$name] ?? null) && manager_global_user_matches_season($state, $season, $name)) {
        $user = $state['users'][$name];
        break;
      }
    }
    if (!is_array($user)) {
      foreach ($candidateSeasons as $season) {
        $participant = manager_participant($season, $name);
        if (!$participant) continue;
        $snapshot = manager_participant_user_snapshot($participant, $name);
        if (is_array($snapshot)) {
          $user = $snapshot;
          break;
        }
      }
    }
    if (!is_array($user) && is_array($state['users'][$name] ?? null)) $user = $state['users'][$name];
    if (!is_array($user)) continue;
    unset($user['password'], $user['created'], $user['lastSeen']);
    $out[$name] = $user;
  }
  return $out;
}

function manager_save_user_list(array $save): array {
  $meta = is_array($save['meta'] ?? null) ? $save['meta'] : [];
  $users = [];
  foreach (($meta['users'] ?? []) as $name) {
    $name = clean_name((string)$name);
    if ($name !== '' && !in_array($name, $users, true)) $users[] = $name;
  }
  return $users;
}

function manager_save_owner(array $save): string {
  $meta = is_array($save['meta'] ?? null) ? $save['meta'] : [];
  $savedBy = clean_name((string)($meta['savedBy'] ?? ''));
  if ($savedBy !== '') return $savedBy;
  $users = manager_save_user_list($save);
  return clean_name((string)($users[0] ?? ''));
}

function manager_game_id(array $season): string {
  $gameId = (string)($season['managerGameId'] ?? '');
  if ($gameId === '') $gameId = (string)($season['saveGameId'] ?? '');
  if ($gameId === '') $gameId = (string)($season['id'] ?? '');
  return $gameId;
}

function manager_save_game_id(array $save): string {
  $meta = is_array($save['meta'] ?? null) ? $save['meta'] : [];
  $gameId = (string)($meta['managerGameId'] ?? '');
  if ($gameId !== '') return $gameId;
  $savedSeasons = is_array($save['state']['managerSeasons'] ?? null) ? $save['state']['managerSeasons'] : [];
  foreach ($savedSeasons as $season) {
    if (!is_array($season)) continue;
    $gameId = manager_game_id($season);
    if ($gameId !== '') return $gameId;
  }
  return (string)($meta['seasonId'] ?? '');
}

function manager_save_group_key(array $meta): string {
  $users = array_values(array_map('clean_name', array_map('strval', $meta['users'] ?? [])));
  sort($users, SORT_STRING);
  $mode = clean_name((string)($meta['mode'] ?? (count($users) > 1 ? 'online' : 'solo')));
  $slot = (string)($meta['slotKey'] ?? '');
  if ($slot === '') $slot = ($mode !== '' ? $mode : 'save') . ':' . implode('+', $users);
  $game = (string)($meta['managerGameId'] ?? '');
  if ($game === '') $game = (string)($meta['seasonId'] ?? '');
  if ($game === '') return $slot;
  return $slot . '|' . $game;
}

function manager_save_division_level(array $meta): int {
  if (isset($meta['divisionLevel']) && is_numeric($meta['divisionLevel'])) return clamp_int((int)$meta['divisionLevel'], 1, 5);
  $name = strtolower(trim((string)($meta['divisionName'] ?? '')));
  $map = [
    'premier league' => 1,
    'championship' => 2,
    'league one' => 3,
    'league two' => 4,
    'national league' => 5,
  ];
  return $map[$name] ?? 5;
}

function manager_save_prefer(array $candidate, ?array $current): bool {
  if (!is_array($current)) return true;
  $candidateYear = (int)($candidate['seasonYear'] ?? 0);
  $currentYear = (int)($current['seasonYear'] ?? 0);
  if ($candidateYear !== $currentYear) return $candidateYear > $currentYear;
  $candidateLevel = manager_save_division_level($candidate);
  $currentLevel = manager_save_division_level($current);
  if ($candidateLevel !== $currentLevel) return $candidateLevel < $currentLevel;
  $candidateSaved = (int)($candidate['savedAt'] ?? 0);
  $currentSaved = (int)($current['savedAt'] ?? 0);
  if ($candidateSaved !== $currentSaved) return $candidateSaved > $currentSaved;
  return strcmp((string)($candidate['file'] ?? ''), (string)($current['file'] ?? '')) > 0;
}

function manager_latest_game_save_metas_by_user(array $users, string $gameId): array {
  $gameId = (string)$gameId;
  if ($gameId === '') return [];
  $wanted = [];
  foreach ($users as $name) {
    $name = clean_name((string)$name);
    if ($name !== '') $wanted[$name] = true;
  }
  if (!$wanted) return [];
  $out = [];
  foreach ((glob(manager_save_dir() . '/*.json') ?: []) as $file) {
    $meta = manager_save_cached_metadata($file, '');
    if (!is_array($meta) || (string)($meta['managerGameId'] ?? '') !== $gameId) continue;
    $metaUsers = array_values(array_map('clean_name', array_map('strval', $meta['users'] ?? [])));
    foreach ($metaUsers as $name) {
      if ($name === '' || !isset($wanted[$name])) continue;
      if (manager_save_prefer($meta, $out[$name] ?? null)) $out[$name] = $meta;
    }
  }
  return $out;
}

function manager_enrich_online_save_meta(array $meta): array {
  $users = array_values(array_map('clean_name', array_map('strval', $meta['users'] ?? [])));
  $gameId = (string)($meta['managerGameId'] ?? '');
  if ($gameId === '' || count($users) < 2) return $meta;
  $branches = manager_latest_game_save_metas_by_user($users, $gameId);
  if (!$branches) return $meta;
  $years = [];
  $divisions = [];
  $branchRows = [];
  foreach ($users as $name) {
    if (empty($branches[$name]) || !is_array($branches[$name])) continue;
    $branch = $branches[$name];
    $years[] = (int)($branch['seasonYear'] ?? 0);
    $division = (string)($branch['divisionName'] ?? '');
    if ($division !== '' && !in_array($division, $divisions, true)) $divisions[] = $division;
    $branchRows[] = [
      'user' => $name,
      'file' => (string)($branch['file'] ?? ''),
      'seasonId' => (string)($branch['seasonId'] ?? ''),
      'seasonYear' => (int)($branch['seasonYear'] ?? 0),
      'divisionName' => $division,
      'seasonStatus' => (string)($branch['seasonStatus'] ?? ''),
    ];
  }
  if ($years) $meta['seasonYear'] = max($years);
  if ($divisions) $meta['divisionName'] = implode(' / ', $divisions);
  if ($branchRows) {
    $meta['branchSaves'] = $branchRows;
    $meta['seasonStatus'] = 'synced';
    $meta['match'] = 'Synced online save';
    $meta['score'] = '';
  }
  return $meta;
}

function manager_save_turn_alerts(array $save): array {
  $state = is_array($save['state'] ?? null) ? $save['state'] : [];
  if (!$state) return [];
  $users = manager_save_user_list($save);
  $out = [];
  foreach ($users as $user) {
    $user = clean_name((string)$user);
    if ($user === '') continue;
    $seasonId = current_manager_season_id($state, $user);
    if ($seasonId === null || !is_array($state['managerSeasons'][$seasonId] ?? null)) continue;
    $season = $state['managerSeasons'][$seasonId];
    $reason = manager_turn_alert_reason($state, $season, $user);
    if ($reason === null) continue;
    $out[$user] = [
      'seasonId' => (string)$seasonId,
      'managerGameId' => manager_game_id($season),
      'reason' => $reason,
    ];
  }
  return $out;
}

function manager_save_metadata(array $save, string $id = '', string $viewer = '', array $state = []): array {
  $meta = is_array($save['meta'] ?? null) ? $save['meta'] : [];
  $savedAt = (int)($meta['savedAt'] ?? 0);
  $savedBy = manager_save_owner($save);
  $users = manager_save_user_list($save);
  $mode = (string)($meta['mode'] ?? (count($users) > 1 ? 'online' : 'solo'));
  $managerGameId = manager_save_game_id($save);
  $slotKey = (string)($meta['slotKey'] ?? ($mode . ':' . implode('+', $users)));
  $groupMeta = $meta + ['mode' => $mode, 'users' => $users, 'slotKey' => $slotKey, 'managerGameId' => $managerGameId];
  return [
    'id' => $id,
    'file' => $id,
    'label' => (string)($meta['label'] ?? $id),
    'savedBy' => $savedBy,
    'users' => $users,
    'mode' => $mode,
    'modeLabel' => strtoupper($mode),
    'slotKey' => $slotKey,
    'managerGameId' => $managerGameId,
    'saveGroupKey' => manager_save_group_key($groupMeta),
    'savedAt' => $savedAt,
    'savedAtText' => $savedAt > 0 ? date('Y-m-d H:i:s', $savedAt) : '',
    'seasonId' => (string)($meta['seasonId'] ?? ''),
    'seasonYear' => (int)($meta['seasonYear'] ?? 0),
    'divisionName' => (string)($meta['divisionName'] ?? ''),
    'seasonStatus' => (string)($meta['seasonStatus'] ?? ''),
    'match' => (string)($meta['match'] ?? ''),
    'score' => (string)($meta['score'] ?? ''),
    'phase' => (string)($meta['phase'] ?? ''),
    'turnAlerts' => manager_save_turn_alerts($save),
    'canDelete' => $viewer !== '' && manager_save_can_delete($save, $viewer, $state),
  ];
}

function manager_write_save_file(string $filename, string $json, array $save): bool {
  $path = manager_save_dir() . '/' . $filename;
  if (@file_put_contents($path, $json, LOCK_EX) === false) return false;
  @chmod($path, 0666);
  manager_save_cached_metadata($path, '');
  manager_backup_save_file($path, 'write');
  manager_prune_saves('', MANAGER_SAVE_KEEP_PER_GROUP);
  manager_save_index_invalidate();
  return true;
}

function manager_save_cached_can_delete(array $meta, string $viewer, array $state = []): bool {
  $viewer = clean_name($viewer);
  if ($viewer === '') return false;
  return ($state && admin_user_can_manage($state, $viewer)) || $viewer === clean_name((string)($meta['savedBy'] ?? ''));
}

function manager_save_cached_allowed(array $meta, string $user): bool {
  $user = clean_name($user);
  if ($user === '') return false;
  foreach (($meta['users'] ?? []) as $name) {
    if (clean_name((string)$name) === $user) return true;
  }
  return false;
}

function manager_save_cached_metadata(string $file, string $viewer = ''): ?array {
  $id = basename($file);
  if (!str_ends_with($id, '.json')) return null;
  $sidecar = manager_save_meta_dir() . '/' . $id . '.meta.json';
  $size = (int)@filesize($file);
  $mtime = (int)@filemtime($file);
  $cached = is_file($sidecar) ? json_decode((string)@file_get_contents($sidecar), true) : null;
  if (
    is_array($cached)
    && (int)($cached['cacheVersion'] ?? 0) === MANAGER_SAVE_META_CACHE_VERSION
    && (int)($cached['sourceSize'] ?? -1) === $size
    && (int)($cached['sourceMtime'] ?? -1) === $mtime
    && is_array($cached['meta'] ?? null)
  ) {
    $meta = $cached['meta'];
  } else {
    $save = json_decode((string)@file_get_contents($file), true);
    if (!is_array($save)) return null;
    $meta = manager_save_metadata($save, $id, '');
    @file_put_contents($sidecar, json_encode(['cacheVersion' => MANAGER_SAVE_META_CACHE_VERSION, 'sourceSize' => $size, 'sourceMtime' => $mtime, 'meta' => $meta], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), LOCK_EX);
    @chmod($sidecar, 0666);
  }
  $meta['id'] = $id;
  $meta['file'] = $id;
  $meta['canDelete'] = $viewer !== '' && manager_save_cached_can_delete($meta, $viewer);
  if (function_exists('ysp_db_upsert_manager_save_meta')) ysp_db_upsert_manager_save_meta($id, $meta, $size, $mtime);
  return $meta;
}

function manager_save_index_path(): string {
  return manager_save_meta_dir() . '/index.json';
}

function manager_save_index_invalidate(): void {
  unset($GLOBALS['YSP_MANAGER_SAVE_INDEX_CACHE']);
  @unlink(manager_save_index_path());
}

function manager_save_index(): array {
  $cachedIndex = $GLOBALS['YSP_MANAGER_SAVE_INDEX_CACHE'] ?? null;
  if (is_array($cachedIndex)) return $cachedIndex;

  if (function_exists('ysp_db_manager_save_index')) {
    $dbIndex = ysp_db_manager_save_index();
    $hasJsonSaves = (bool)(glob(manager_save_dir() . '/*.json') ?: []);
    if (is_array($dbIndex) && (!empty($dbIndex['users']) || !$hasJsonSaves)) {
      $GLOBALS['YSP_MANAGER_SAVE_INDEX_CACHE'] = $dbIndex;
      return $dbIndex;
    }
  }

  $path = manager_save_index_path();
  $cached = is_file($path) ? json_decode((string)@file_get_contents($path), true) : null;
  if (
    is_array($cached)
    && (int)($cached['cacheVersion'] ?? 0) === MANAGER_SAVE_META_CACHE_VERSION
    && (time() - (int)($cached['builtAt'] ?? 0)) <= 30
    && is_array($cached['users'] ?? null)
    && is_array($cached['seasons'] ?? null)
    && is_array($cached['seasonAny'] ?? null)
  ) {
    $GLOBALS['YSP_MANAGER_SAVE_INDEX_CACHE'] = $cached;
    return $cached;
  }

  $users = [];
  $seasons = [];
  $seasonAny = [];
  foreach ((glob(manager_save_dir() . '/*.json') ?: []) as $file) {
    if (!is_file($file)) continue;
    $meta = manager_save_cached_metadata($file, '');
    if (!is_array($meta)) continue;
    $seasonId = (string)($meta['seasonId'] ?? '');
    if ($seasonId !== '') $seasonAny[$seasonId] = true;
    foreach (($meta['users'] ?? []) as $name) {
      $name = clean_name((string)$name);
      if ($name === '') continue;
      $users[$name] = true;
      if ($seasonId !== '') $seasons[$seasonId][$name] = true;
    }
  }

  $index = [
    'cacheVersion' => MANAGER_SAVE_META_CACHE_VERSION,
    'builtAt' => time(),
    'users' => $users,
    'seasons' => $seasons,
    'seasonAny' => $seasonAny,
  ];
  @file_put_contents($path, json_encode($index, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), LOCK_EX);
  @chmod($path, 0666);
  $GLOBALS['YSP_MANAGER_SAVE_INDEX_CACHE'] = $index;
  return $index;
}

function manager_save_allowed(array $save, string $user): bool {
  $user = clean_name($user);
  return $user !== '' && in_array($user, manager_save_user_list($save), true);
}

function manager_save_can_delete(array $save, string $user, array $state = []): bool {
  $user = clean_name($user);
  $savedBy = manager_save_owner($save);
  return ($state && admin_user_can_manage($state, $user)) || ($savedBy !== '' && $user === $savedBy);
}

function manager_prune_saves(string $owner = '', int $limit = 2): void {
  $limit = max(1, $limit);
  $owner = clean_name($owner);
  $groups = [];
  $changed = false;
  foreach ((glob(manager_save_dir() . '/*.json') ?: []) as $file) {
    if (!is_file($file)) continue;
    $meta = manager_save_cached_metadata($file, '');
    if (!is_array($meta)) continue;
    if ($owner !== '') {
      $mentions = clean_name((string)($meta['savedBy'] ?? '')) === $owner;
      foreach (($meta['users'] ?? []) as $name) {
        if (clean_name((string)$name) === $owner) {
          $mentions = true;
          break;
        }
      }
      if (!$mentions) continue;
    }
    $key = manager_save_group_key($meta);
    if ($key === '') $key = (string)($meta['file'] ?? basename($file));
    $groups[$key][] = ['file' => $file, 'id' => basename($file), 'meta' => $meta];
  }
  foreach ($groups as $items) {
    usort($items, function ($a, $b) {
      $am = is_array($a['meta'] ?? null) ? $a['meta'] : [];
      $bm = is_array($b['meta'] ?? null) ? $b['meta'] : [];
      if (manager_save_prefer($am, $bm)) return -1;
      if (manager_save_prefer($bm, $am)) return 1;
      return strcmp((string)($b['id'] ?? ''), (string)($a['id'] ?? ''));
    });
    foreach (array_slice($items, $limit) as $stale) {
      $file = (string)($stale['file'] ?? '');
      if ($file === '' || !is_file($file)) continue;
      manager_backup_save_file($file, 'prune');
      if (@unlink($file)) {
        @unlink(manager_save_sidecar_path((string)($stale['id'] ?? basename($file))));
        if (function_exists('ysp_db_delete_manager_save_meta')) ysp_db_delete_manager_save_meta((string)($stale['id'] ?? basename($file)));
        $changed = true;
      }
    }
  }
  if ($changed) manager_save_index_invalidate();
}

function manager_delete_all_saves(): int {
  $deleted = 0;
  foreach ((glob(manager_save_dir() . '/*.json') ?: []) as $file) {
    if (!is_file($file)) continue;
    $id = basename($file);
    manager_backup_save_file($file, 'delete-all');
    if (@unlink($file)) {
      @unlink(manager_save_sidecar_path($id));
      if (function_exists('ysp_db_delete_manager_save_meta')) ysp_db_delete_manager_save_meta($id);
      $deleted++;
    }
  }
  if ($deleted > 0) manager_save_index_invalidate();
  return $deleted;
}

function manager_delete_saves_for_user(string $user): int {
  $user = clean_name($user);
  if ($user === '') return 0;
  $deleted = 0;
  foreach ((glob(manager_save_dir() . '/*.json') ?: []) as $file) {
    if (!is_file($file)) continue;
    $meta = manager_save_cached_metadata($file, '');
    if (!is_array($meta)) continue;
    $mentions = clean_name((string)($meta['savedBy'] ?? '')) === $user;
    foreach (($meta['users'] ?? []) as $name) {
      if (clean_name((string)$name) === $user) {
        $mentions = true;
        break;
      }
    }
    if (!$mentions) continue;
    $id = basename($file);
    manager_backup_save_file($file, 'delete-user-' . $user);
    if (@unlink($file)) {
      @unlink(manager_save_sidecar_path($id));
      if (function_exists('ysp_db_delete_manager_save_meta')) ysp_db_delete_manager_save_meta($id);
      $deleted++;
    }
  }
  if ($deleted > 0) manager_save_index_invalidate();
  return $deleted;
}

function manager_any_save_for_season(string $seasonId): bool {
  if ($seasonId === '') return false;
  $index = manager_save_index();
  return !empty($index['seasonAny'][$seasonId]);
}

function manager_remove_active_season_if_unsaved(array &$state, string $seasonId): void {
  $seasonId = (string)$seasonId;
  if ($seasonId === '' || manager_any_save_for_season($seasonId)) return;
  foreach (($state['matches'] ?? []) as $matchId => $match) {
    if ((string)($match['managerFixture']['seasonId'] ?? '') === $seasonId) unset($state['matches'][$matchId]);
  }
  unset($state['managerSeasons'][$seasonId]);
  foreach (($state['managerCurrentByUser'] ?? []) as $name => $id) {
    if ((string)$id === $seasonId) unset($state['managerCurrentByUser'][$name]);
  }
  if ((string)($state['managerCurrentId'] ?? '') === $seasonId) $state['managerCurrentId'] = null;
}

function manager_autosave_context(array $state, array $season): array {
  $seasonId = (string)($season['id'] ?? '');
  $seasonIds = $seasonId !== '' ? [$seasonId] : [];
  foreach (manager_related_current_season_ids($state, $season) as $id) {
    $id = (string)$id;
    if ($id !== '' && !in_array($id, $seasonIds, true)) $seasonIds[] = $id;
  }
  $seasons = [];
  $users = [];
  foreach ($seasonIds as $id) {
    $candidate = is_array($state['managerSeasons'][$id] ?? null) ? $state['managerSeasons'][$id] : ($id === $seasonId ? $season : null);
    if (!is_array($candidate) || ($candidate['status'] ?? '') === 'aborted') continue;
    manager_refresh_human_participant_snapshots($state, $candidate);
    $seasons[$id] = $candidate;
    foreach (manager_save_users($candidate) as $name) {
      if ($name !== '' && !in_array($name, $users, true)) $users[] = $name;
    }
  }
  if (!$seasons && $seasonId !== '') {
    $candidate = $season;
    manager_refresh_human_participant_snapshots($state, $candidate);
    $seasons[$seasonId] = $candidate;
  }
  if (!$users) $users = manager_save_users($season);
  $currentByUser = [];
  foreach ($users as $name) {
    $currentId = (string)(current_manager_season_id($state, $name) ?? '');
    if ($currentId !== '' && isset($seasons[$currentId])) $currentByUser[$name] = $currentId;
    elseif ($seasonId !== '') $currentByUser[$name] = $seasonId;
  }
  return [
    'users' => $users,
    'seasons' => $seasons,
    'currentByUser' => $currentByUser,
  ];
}

function manager_autosave_after_fixture(array $state, array $season, array $fixture, array $homeData, array $awayData, string $savedBy = ''): void {
  if (empty($homeData['human']) && empty($awayData['human'])) return;
  $saveContext = manager_autosave_context($state, $season);
  $users = $saveContext['users'];
  if (!$users) return;
  $savedBy = clean_name($savedBy);
  if ($savedBy === '' || !in_array($savedBy, $users, true)) $savedBy = $users[0];
  $savedAt = time();
  $score = $fixture['score'] ?? [0, 0];
  $homeTeam = (string)($homeData['team']['name'] ?? $fixture['home'] ?? '');
  $awayTeam = (string)($awayData['team']['name'] ?? $fixture['away'] ?? '');
  $seasonId = (string)($season['id'] ?? '');
  $managerGameId = manager_game_id($season);
  $filename = manager_save_filename($users, $savedAt, $seasonId);
  $mode = count($users) > 1 ? 'online' : 'solo';
  $save = [
    'schemaVersion' => 1,
    'meta' => [
      'savedAt' => $savedAt,
      'label' => preg_replace('/\.json$/', '', $filename),
      'savedBy' => $savedBy,
      'users' => $users,
      'mode' => $mode,
      'slotKey' => $mode . ':' . implode('+', $users),
      'managerGameId' => $managerGameId,
      'seasonId' => $seasonId,
      'seasonYear' => manager_season_year($season, $state),
      'divisionName' => (string)($season['divisionName'] ?? ''),
      'seasonStatus' => (string)($season['status'] ?? ''),
      'phase' => (string)($fixture['phase'] ?? 'league'),
      'match' => trim($homeTeam . ' - ' . $awayTeam),
      'score' => (int)($score[0] ?? 0) . '-' . (int)($score[1] ?? 0),
    ],
    'state' => [
      'managerSeasons' => $saveContext['seasons'],
      'managerCurrentId' => (string)($season['id'] ?? ''),
      'managerCurrentByUser' => $saveContext['currentByUser'],
      'managerYear' => manager_season_year($season, $state),
      'teamTrophies' => $state['teamTrophies'] ?? [],
      'managerChats' => $state['managerChats'] ?? [],
      'managerMarket' => $state['managerMarket'] ?? [],
      'managerBrowseOffers' => $state['managerBrowseOffers'] ?? [],
      'managerTransferOffers' => $state['managerTransferOffers'] ?? [],
      'managerBrowseSold' => $state['managerBrowseSold'] ?? [],
      'users' => manager_save_user_snapshot($state, $users, $saveContext['seasons'], $saveContext['currentByUser']),
    ],
  ];
  $json = json_encode($save, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
  if (!is_string($json)) return;
  manager_write_save_file($filename, $json, $save);
}

function manager_latest_human_fixture(array $season): ?array {
  $humans = array_flip(manager_save_users($season));
  $latest = null;
  $latestAt = -1;
  foreach (($season['rounds'] ?? []) as $round) {
    foreach (($round['fixtures'] ?? []) as $fixture) {
      if (($fixture['status'] ?? '') !== 'played') continue;
      $home = (string)($fixture['home'] ?? '');
      $away = (string)($fixture['away'] ?? '');
      if (!isset($humans[$home]) && !isset($humans[$away])) continue;
      $playedAt = (int)($fixture['playedAt'] ?? 0);
      if ($playedAt >= $latestAt) {
        $latestAt = $playedAt;
        $latest = $fixture;
      }
    }
  }
  return $latest;
}

function manager_autosave_snapshot(array $state, array $season, string $savedBy = '', string $match = 'Season autosave', string $score = ''): void {
  $saveContext = manager_autosave_context($state, $season);
  $users = $saveContext['users'];
  if (!$users) return;
  $savedBy = clean_name($savedBy);
  if ($savedBy === '' || !in_array($savedBy, $users, true)) $savedBy = $users[0];
  $savedAt = time();
  $seasonId = (string)($season['id'] ?? '');
  $managerGameId = manager_game_id($season);
  $filename = manager_save_filename($users, $savedAt, $seasonId);
  $mode = count($users) > 1 ? 'online' : 'solo';
  $save = [
    'schemaVersion' => 1,
    'meta' => [
      'savedAt' => $savedAt,
      'label' => preg_replace('/\.json$/', '', $filename),
      'savedBy' => $savedBy,
      'users' => $users,
      'mode' => $mode,
      'slotKey' => $mode . ':' . implode('+', $users),
      'managerGameId' => $managerGameId,
      'seasonId' => $seasonId,
      'seasonYear' => manager_season_year($season, $state),
      'divisionName' => (string)($season['divisionName'] ?? ''),
      'seasonStatus' => (string)($season['status'] ?? ''),
      'phase' => 'season',
      'match' => $match,
      'score' => $score,
    ],
    'state' => [
      'managerSeasons' => $saveContext['seasons'],
      'managerCurrentId' => (string)($season['id'] ?? ''),
      'managerCurrentByUser' => $saveContext['currentByUser'],
      'managerYear' => manager_season_year($season, $state),
      'teamTrophies' => $state['teamTrophies'] ?? [],
      'managerChats' => $state['managerChats'] ?? [],
      'managerMarket' => $state['managerMarket'] ?? [],
      'managerBrowseOffers' => $state['managerBrowseOffers'] ?? [],
      'managerTransferOffers' => $state['managerTransferOffers'] ?? [],
      'managerBrowseSold' => $state['managerBrowseSold'] ?? [],
      'users' => manager_save_user_snapshot($state, $users, $saveContext['seasons'], $saveContext['currentByUser']),
    ],
  ];
  $json = json_encode($save, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
  if (!is_string($json)) return;
  manager_write_save_file($filename, $json, $save);
}

function manager_autosave_season(array &$state, array $season, ?array $fixture = null, string $savedBy = ''): void {
  $fixture = $fixture ?: manager_latest_human_fixture($season);
  if (!$fixture) {
    $label = ((string)($season['status'] ?? '') === 'finished') ? 'Season finished autosave' : 'Season autosave';
    manager_autosave_snapshot($state, $season, $savedBy, $label, '');
    return;
  }
  $home = (string)($fixture['home'] ?? '');
  $away = (string)($fixture['away'] ?? '');
  if ($home === '' || $away === '') {
    manager_autosave_snapshot($state, $season, $savedBy, 'Season autosave', '');
    return;
  }
  $homeData = manager_side_data($state, $season, $home);
  $awayData = manager_side_data($state, $season, $away);
  manager_autosave_after_fixture($state, $season, $fixture, $homeData, $awayData, $savedBy);
}

function manager_has_save_for_season(string $user, string $seasonId): bool {
  $user = clean_name($user);
  if ($user === '' || $seasonId === '') return false;
  $index = manager_save_index();
  return !empty($index['seasons'][$seasonId][$user]);
}

function manager_user_can_continue(array $state, string $user): bool {
  $user = clean_name($user);
  if ($user === '') return false;
  $seasonId = (string)(current_manager_season_id($state, $user) ?? '');
  return $seasonId !== '' && manager_has_save_for_season($user, $seasonId);
}

function ensure_current_manager_save(array &$state, string $user): void {
  $candidates = [];
  foreach (($state['managerSeasons'] ?? []) as $season) {
    if (!is_array($season)) continue;
    if (($season['status'] ?? '') === 'aborted') continue;
    if (!manager_season_visible_to_user($season, $user)) continue;
    $seasonId = (string)($season['id'] ?? '');
    if ($seasonId === '' || manager_has_save_for_season($user, $seasonId)) continue;
    $candidates[] = $season;
  }
  usort($candidates, function ($a, $b) {
    $rank = fn($s) => (($s['status'] ?? '') === 'finished' ? 3 : (in_array(($s['status'] ?? ''), ['active', 'playoff'], true) ? 2 : 1));
    return ($rank($b) <=> $rank($a)) ?: ((int)($b['updated'] ?? $b['created'] ?? 0) <=> (int)($a['updated'] ?? $a['created'] ?? 0));
  });
  foreach (array_slice($candidates, 0, 5) as $season) manager_autosave_season($state, $season, null, $user);
}

function manager_save_path(string $id): string {
  $id = basename($id);
  if (!preg_match('/^[a-z0-9_.-]+\.json$/i', $id)) reply(['ok' => false, 'error' => 'bad_save'], 400);
  return manager_save_dir() . '/' . $id;
}

function manager_save_sidecar_path(string $id): string {
  return manager_save_meta_dir() . '/' . basename($id) . '.meta.json';
}

function read_manager_save(string $id): array {
  $path = manager_save_path($id);
  if (!is_file($path)) reply(['ok' => false, 'error' => 'save_not_found'], 404);
  $save = json_decode((string)file_get_contents($path), true);
  if (!is_array($save) || !is_array($save['state'] ?? null)) reply(['ok' => false, 'error' => 'bad_save_file'], 500);
  return $save;
}

function public_manager_saves_from_metas(array $metas, string $user, array $state = []): array {
  $latestByLeague = [];
  foreach ($metas as $meta) {
    if (!is_array($meta) || !manager_save_cached_allowed($meta, $user)) continue;
    $meta['canDelete'] = manager_save_cached_can_delete($meta, $user, $state);
    $key = manager_save_group_key($meta);
    if ($key === '') $key = (string)($meta['file'] ?? '');
    $current = $latestByLeague[$key] ?? null;
    if (manager_save_prefer($meta, $current)) {
      $latestByLeague[$key] = $meta;
    }
  }
  $latestByDisplay = [];
  foreach (array_values($latestByLeague) as $meta) {
    $meta = manager_enrich_online_save_meta($meta);
    $users = array_values(array_map('clean_name', array_map('strval', $meta['users'] ?? [])));
    sort($users, SORT_STRING);
    $displayKey = implode('|', [
      clean_name((string)($meta['mode'] ?? '')),
      implode('+', $users),
      clean_name((string)($meta['divisionName'] ?? '')),
      (string)(int)($meta['seasonYear'] ?? 0),
    ]);
    if ($displayKey === '|||0') $displayKey = (string)($meta['saveGroupKey'] ?? $meta['file'] ?? '');
    $current = $latestByDisplay[$displayKey] ?? null;
    if (
      !is_array($current)
      || (int)($meta['savedAt'] ?? 0) > (int)($current['savedAt'] ?? 0)
      || ((int)($meta['savedAt'] ?? 0) === (int)($current['savedAt'] ?? 0) && strcmp((string)($meta['file'] ?? ''), (string)($current['file'] ?? '')) > 0)
    ) {
      $latestByDisplay[$displayKey] = $meta;
    }
  }
  $out = array_values($latestByDisplay);
  usort($out, fn($a, $b) => ((int)($b['savedAt'] ?? 0) <=> (int)($a['savedAt'] ?? 0)) ?: strcmp((string)($b['file'] ?? ''), (string)($a['file'] ?? '')));
  $out = array_slice($out, 0, 80);
  return $out;
}

function public_manager_saves(string $user, array $state = []): array {
  $user = clean_name($user);
  if ($user === '') return [];
  if (function_exists('ysp_db_manager_save_metas_for_user')) {
    $dbMetas = ysp_db_manager_save_metas_for_user($user);
    $hasJsonSaves = (bool)(glob(manager_save_dir() . '/*.json') ?: []);
    if (is_array($dbMetas) && ($dbMetas || !$hasJsonSaves)) return public_manager_saves_from_metas($dbMetas, $user, $state);
  }
  $metas = [];
  foreach ((glob(manager_save_dir() . '/*.json') ?: []) as $file) {
    $meta = manager_save_cached_metadata($file, $user);
    if (is_array($meta)) $metas[] = $meta;
  }
  return public_manager_saves_from_metas($metas, $user, $state);
}

function manager_user_has_any_save(string $user): bool {
  $user = clean_name($user);
  if ($user === '') return false;
  $index = manager_save_index();
  return !empty($index['users'][$user]);
}

function merge_saved_team_trophies(array &$state, array $savedTrophies): void {
  if (!isset($state['teamTrophies']) || !is_array($state['teamTrophies'])) $state['teamTrophies'] = [];
  foreach ($savedTrophies as $teamKey => $items) {
    if (!is_string($teamKey) || !is_array($items)) continue;
    if (!isset($state['teamTrophies'][$teamKey]) || !is_array($state['teamTrophies'][$teamKey])) $state['teamTrophies'][$teamKey] = [];
    $seen = [];
    foreach ($state['teamTrophies'][$teamKey] as $existing) {
      if (!is_array($existing)) continue;
      $seen[json_encode($existing, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: ''] = true;
    }
    foreach ($items as $item) {
      if (!is_array($item)) continue;
      $key = json_encode($item, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: '';
      if ($key === '' || isset($seen[$key])) continue;
      $state['teamTrophies'][$teamKey][] = $item;
      $seen[$key] = true;
    }
  }
}

function merge_saved_manager_chats(array &$state, array $savedChats): void {
  if (!isset($state['managerChats']) || !is_array($state['managerChats'])) $state['managerChats'] = [];
  foreach ($savedChats as $key => $thread) {
    if (!is_string($key) || !is_array($thread)) continue;
    $existing = is_array($state['managerChats'][$key] ?? null) ? $state['managerChats'][$key] : [];
    $messages = array_merge(
      is_array($existing['messages'] ?? null) ? $existing['messages'] : [],
      is_array($thread['messages'] ?? null) ? $thread['messages'] : []
    );
    $participants = array_values(array_unique(array_filter(array_map('strval', array_merge(
      is_array($existing['participants'] ?? null) ? $existing['participants'] : [],
      is_array($thread['participants'] ?? null) ? $thread['participants'] : []
    )), fn($name) => $name !== '')));
    $state['managerChats'][$key] = [
      'key' => $key,
      'participants' => $participants,
      'messages' => manager_chat_normalize_messages($messages, 220),
      'updated' => max((int)($existing['updated'] ?? 0), (int)($thread['updated'] ?? 0)),
    ];
  }
}

function manager_expand_loaded_save_branches(array $save): array {
  $users = manager_save_user_list($save);
  $gameId = manager_save_game_id($save);
  if ($gameId === '' || count($users) < 2) return $save;
  $branches = manager_latest_game_save_metas_by_user($users, $gameId);
  if (!$branches) return $save;

  $expanded = $save;
  if (!isset($expanded['state']) || !is_array($expanded['state'])) $expanded['state'] = [];
  foreach (['managerSeasons', 'managerCurrentByUser', 'users', 'teamTrophies', 'managerChats'] as $key) {
    if (!isset($expanded['state'][$key]) || !is_array($expanded['state'][$key])) $expanded['state'][$key] = [];
  }
  $wanted = array_fill_keys($users, true);
  $latestGlobal = -1;
  $seenFiles = [];
  foreach ($branches as $branchMeta) {
    if (!is_array($branchMeta)) continue;
    $file = (string)($branchMeta['file'] ?? '');
    if ($file === '' || isset($seenFiles[$file])) continue;
    $seenFiles[$file] = true;
    $branch = read_manager_save($file);
    $branchState = is_array($branch['state'] ?? null) ? $branch['state'] : [];
    foreach (($branchState['managerSeasons'] ?? []) as $seasonId => $season) {
      if (is_string($seasonId) && is_array($season)) $expanded['state']['managerSeasons'][$seasonId] = $season;
    }
    foreach (($branchState['users'] ?? []) as $name => $savedUser) {
      $name = clean_name((string)$name);
      if ($name !== '' && isset($wanted[$name]) && is_array($savedUser)) $expanded['state']['users'][$name] = $savedUser;
    }
    $branchCurrent = is_array($branchState['managerCurrentByUser'] ?? null) ? $branchState['managerCurrentByUser'] : [];
    foreach (array_intersect($users, array_map('clean_name', array_map('strval', $branchMeta['users'] ?? []))) as $name) {
      $seasonId = is_string($branchCurrent[$name] ?? null) ? (string)$branchCurrent[$name] : (string)($branchMeta['seasonId'] ?? '');
      if ($seasonId !== '' && isset($expanded['state']['managerSeasons'][$seasonId])) $expanded['state']['managerCurrentByUser'][$name] = $seasonId;
    }
    $expanded['state']['managerYear'] = max((int)($expanded['state']['managerYear'] ?? manager_base_season_year()), (int)($branchState['managerYear'] ?? 0), (int)($branchMeta['seasonYear'] ?? 0));
    if (is_array($branchState['teamTrophies'] ?? null)) merge_saved_team_trophies($expanded['state'], $branchState['teamTrophies']);
    if (is_array($branchState['managerChats'] ?? null)) merge_saved_manager_chats($expanded['state'], $branchState['managerChats']);
    $savedAt = (int)($branchMeta['savedAt'] ?? 0);
    if ($savedAt >= $latestGlobal) {
      $latestGlobal = $savedAt;
      foreach (['managerMarket', 'managerBrowseOffers', 'managerTransferOffers', 'managerBrowseSold'] as $key) {
        if (is_array($branchState[$key] ?? null)) $expanded['state'][$key] = $branchState[$key];
      }
    }
  }
  $expanded['meta']['users'] = $users;
  $expanded['meta']['mode'] = 'online';
  $expanded['meta']['slotKey'] = 'online:' . implode('+', $users);
  $expanded['meta']['managerGameId'] = $gameId;
  return $expanded;
}

function load_manager_save_into_state(array &$state, array $save, string $viewer = ''): void {
  $save = manager_expand_loaded_save_branches($save);
  $saved = $save['state'] ?? [];
  if (!is_array($saved)) reply(['ok' => false, 'error' => 'bad_save_file'], 500);
  $savedUsers = is_array($saved['users'] ?? null) ? $saved['users'] : [];
  $saveUsers = manager_save_user_list($save);
  if (!$saveUsers) $saveUsers = array_values(array_map('clean_name', array_keys($savedUsers)));
  $viewer = clean_name($viewer);
  $switchUsers = ($viewer !== '' && in_array($viewer, $saveUsers, true)) ? [$viewer] : $saveUsers;
  $savedGameId = manager_save_game_id($save);
  $missing = [];
  foreach (array_keys($savedUsers) as $name) {
    if (!isset($state['users'][$name])) $missing[] = $name;
  }
  if ($missing) reply(['ok' => false, 'error' => 'missing_users', 'missingUsers' => $missing], 409);

  foreach ($switchUsers as $name) {
    manager_store_current_user_snapshot($state, (string)$name);
    reset_user_manager_transfer_state($state, (string)$name);
  }
  foreach ($switchUsers as $name) {
    unset($state['managerCurrentByUser'][$name]);
  }

  $savedSeasons = is_array($saved['managerSeasons'] ?? null) ? $saved['managerSeasons'] : [];
  $savedSeasonIds = array_fill_keys(array_map('strval', array_keys($savedSeasons)), true);
  $removedSeasonIds = [];
  foreach (($state['managerSeasons'] ?? []) as $seasonId => $season) {
    if (!is_string($seasonId) || isset($savedSeasonIds[$seasonId]) || !is_array($season)) continue;
    $seasonGameId = manager_game_id($season);
    $sameGame = $savedGameId !== '' && $seasonGameId !== '' && $seasonGameId === $savedGameId;
    $seasonUsers = array_map(fn($name) => clean_name((string)$name), is_array($season['humanManagers'] ?? null) ? $season['humanManagers'] : []);
    $legacySameSave = $savedGameId === '' && array_intersect($saveUsers, $seasonUsers);
    if (!$sameGame && !$legacySameSave) continue;
    unset($state['managerSeasons'][$seasonId]);
    $removedSeasonIds[$seasonId] = true;
  }
  foreach (($state['managerCurrentByUser'] ?? []) as $name => $seasonId) {
    if (isset($removedSeasonIds[(string)$seasonId])) {
      $name = clean_name((string)$name);
      if ($name !== '' && in_array($name, $saveUsers, true) && !in_array($name, $switchUsers, true)) $switchUsers[] = $name;
      unset($state['managerCurrentByUser'][$name]);
    }
  }
  foreach (($state['matches'] ?? []) as $matchId => $match) {
    $fixtureSeasonId = (string)($match['managerFixture']['seasonId'] ?? '');
    $home = clean_name((string)($match['players']['home'] ?? ''));
    $away = clean_name((string)($match['players']['away'] ?? ''));
    if (($fixtureSeasonId !== '' && (isset($savedSeasonIds[$fixtureSeasonId]) || isset($removedSeasonIds[$fixtureSeasonId]))) || in_array($home, $switchUsers, true) || in_array($away, $switchUsers, true)) {
      unset($state['matches'][$matchId]);
    }
  }

  foreach ($savedSeasons as $seasonId => $season) {
    if (!is_string($seasonId) || !is_array($season)) continue;
    manager_refresh_human_participant_snapshots($state, $season, $savedUsers);
    $state['managerSeasons'][$seasonId] = $season;
  }

  $savedCurrentByUser = is_array($saved['managerCurrentByUser'] ?? null) ? $saved['managerCurrentByUser'] : [];
  $savedCurrentId = is_string($saved['managerCurrentId'] ?? null) ? $saved['managerCurrentId'] : null;
  $loadedCurrentId = '';
  foreach ($switchUsers as $name) {
    if ($name === '') continue;
    $seasonId = is_string($savedCurrentByUser[$name] ?? null) ? (string)$savedCurrentByUser[$name] : (string)($savedCurrentId ?? '');
    if ($seasonId !== '' && isset($state['managerSeasons'][$seasonId])) $state['managerCurrentByUser'][$name] = $seasonId;
    if ($seasonId !== '' && isset($state['managerSeasons'][$seasonId]) && $loadedCurrentId === '') $loadedCurrentId = $seasonId;
  }
  if ($loadedCurrentId !== '') $state['managerCurrentId'] = $loadedCurrentId;
  elseif ($viewer === '' && $savedCurrentId !== null && isset($state['managerSeasons'][$savedCurrentId])) $state['managerCurrentId'] = $savedCurrentId;
  $state['managerYear'] = max(manager_state_year($state), (int)($saved['managerYear'] ?? manager_base_season_year()));
  if (is_array($saved['teamTrophies'] ?? null)) merge_saved_team_trophies($state, $saved['teamTrophies']);
  if (is_array($saved['managerChats'] ?? null)) merge_saved_manager_chats($state, $saved['managerChats']);
  $state['managerBrowseOffers'] = is_array($saved['managerBrowseOffers'] ?? null) ? $saved['managerBrowseOffers'] : [];
  $state['managerTransferOffers'] = is_array($saved['managerTransferOffers'] ?? null) ? $saved['managerTransferOffers'] : [];
  $state['managerTransferNotices'] = [];
  $state['managerBrowseSold'] = is_array($saved['managerBrowseSold'] ?? null) ? $saved['managerBrowseSold'] : [];
  $state['managerMarket'] = is_array($saved['managerMarket'] ?? null) ? array_values($saved['managerMarket']) : [];
  unset($state['managerMarketVersion']);
  foreach (($state['managerInvites'] ?? []) as $inviteId => $invite) {
    $inviteUsers = array_map(fn($name) => clean_name((string)$name), is_array($invite['users'] ?? null) ? $invite['users'] : []);
    if (array_intersect($saveUsers, $inviteUsers)) unset($state['managerInvites'][$inviteId]);
  }
  $switchUserSet = array_fill_keys(array_map('strval', $switchUsers), true);
  foreach ($savedUsers as $name => $savedUser) {
    if (!isset($switchUserSet[(string)$name])) continue;
    if (!is_array($savedUser) || !isset($state['users'][$name])) continue;
    $runtimeFields = manager_runtime_account_fields($state['users'][$name], $savedUser);
    $state['users'][$name] = $savedUser;
    foreach ($runtimeFields as $key => $value) $state['users'][$name][$key] = $value;
    $seasonId = (string)($state['managerCurrentByUser'][$name] ?? '');
    if ($seasonId !== '' && is_array($state['managerSeasons'][$seasonId] ?? null)) {
      manager_mark_user_active_context($state['users'][$name], $state['managerSeasons'][$seasonId]);
    }
  }
  normalize_state_player_ids($state);
  normalize_state_cpu_formations($state);
  $state['managerPlayerIdVersion'] = SQUAD_ID_VERSION;
  $state['managerCpuFormationVersion'] = CPU_FORMATION_VERSION;
}

function manager_season_visible_to_user(array $season, string $user): bool {
  return $user !== '' && in_array($user, $season['humanManagers'] ?? [], true);
}

function manager_current_season_score(array $state, array $season): int {
  $status = (string)($season['status'] ?? '');
  $statusRank = in_array($status, ['active', 'playoff'], true) ? 3 : ($status === 'finished' ? 2 : 1);
  $year = manager_season_year($season, $state);
  $divisionLevel = clamp_int((int)($season['divisionLevel'] ?? 5), 1, 5);
  $divisionRank = 6 - $divisionLevel;
  return $year * 1000000000000 + $divisionRank * 100000000000 + $statusRank * 10000000000 + (int)($season['updated'] ?? $season['created'] ?? 0);
}

function current_manager_season_id(array $state, string $user = ''): ?string {
  if ($user !== '') {
    $valid = function ($id) use ($state, $user): ?string {
      if (!is_string($id) || $id === '' || !isset($state['managerSeasons'][$id]) || !is_array($state['managerSeasons'][$id])) return null;
      $season = $state['managerSeasons'][$id];
      if (($season['status'] ?? '') === 'aborted' || !manager_season_visible_to_user($season, $user)) return null;
      return (string)$id;
    };
    $explicit = $valid($state['managerCurrentByUser'][$user] ?? null);
    if ($explicit !== null) return $explicit;
    $bestId = null;
    $bestScore = -1;
    $consider = function ($id) use (&$bestId, &$bestScore, $state, $user): void {
      if (!is_string($id) || $id === '' || !isset($state['managerSeasons'][$id]) || !is_array($state['managerSeasons'][$id])) return;
      $season = $state['managerSeasons'][$id];
      if (($season['status'] ?? '') === 'aborted' || !manager_season_visible_to_user($season, $user)) return;
      $score = manager_current_season_score($state, $season);
      if ($score > $bestScore) {
        $bestScore = $score;
        $bestId = (string)$id;
      }
    };
    $consider($state['managerCurrentByUser'][$user] ?? null);
    $consider($state['managerCurrentId'] ?? null);
    foreach (($state['managerSeasons'] ?? []) as $id => $season) {
      if (!is_array($season)) continue;
      if (($season['status'] ?? '') === 'aborted') continue;
      if (!manager_season_visible_to_user($season, $user)) continue;
      $score = manager_current_season_score($state, $season);
      if ($score > $bestScore) {
        $bestScore = $score;
        $bestId = (string)$id;
      }
    }
    return $bestId;
  }

  $id = $state['managerCurrentId'] ?? null;
  if (is_string($id) && isset($state['managerSeasons'][$id]) && ($state['managerSeasons'][$id]['status'] ?? '') !== 'aborted') {
    return $id;
  }
  return null;
}

function current_manager_season(array $state, string $user = ''): ?array {
  $id = current_manager_season_id($state, $user);
  return $id !== null ? ($state['managerSeasons'][$id] ?? null) : null;
}

function manager_current_save_users(array $state, string $user = ''): array {
  $season = current_manager_season($state, $user);
  if (!$season) return [];
  $chainKey = manager_season_chain_key($state, $season);
  $users = [];
  $add = function ($name) use (&$users): void {
    $name = clean_name((string)$name);
    if ($name !== '' && !in_array($name, $users, true)) $users[] = $name;
  };
  foreach (($season['humanManagers'] ?? []) as $name) $add($name);
  foreach (($state['managerCurrentByUser'] ?? []) as $name => $seasonId) {
    $seasonId = (string)$seasonId;
    if ($seasonId === '' || empty($state['managerSeasons'][$seasonId]) || !is_array($state['managerSeasons'][$seasonId])) continue;
    $candidate = $state['managerSeasons'][$seasonId];
    if (($candidate['status'] ?? '') === 'aborted') continue;
    if (manager_season_chain_key($state, $candidate) !== $chainKey) continue;
    $add($name);
    foreach (($candidate['humanManagers'] ?? []) as $human) $add($human);
  }
  foreach (($state['managerSeasons'] ?? []) as $candidate) {
    if (!is_array($candidate) || ($candidate['status'] ?? '') === 'aborted') continue;
    if (manager_season_chain_key($state, $candidate) !== $chainKey) continue;
    foreach (($candidate['humanManagers'] ?? []) as $name) $add($name);
  }
  return $users;
}

function set_manager_current(array &$state, string $seasonId, array $users): void {
  if (!isset($state['managerCurrentByUser']) || !is_array($state['managerCurrentByUser'])) $state['managerCurrentByUser'] = [];
  foreach ($users as $user) {
    $user = clean_name((string)$user);
    if ($user === '') continue;
    $state['managerCurrentByUser'][$user] = $seasonId;
    if (is_array($state['users'][$user] ?? null) && is_array($state['managerSeasons'][$seasonId] ?? null)) {
      manager_mark_user_active_context($state['users'][$user], $state['managerSeasons'][$seasonId]);
    }
  }
  $state['managerCurrentId'] = $seasonId;
}

function manager_user_division_level(array $state, string $user): int {
  return clamp_int((int)($state['users'][$user]['managerDivisionLevel'] ?? 5), 1, 5);
}
