<?php
declare(strict_types=1);

ini_set('display_errors', '0');
ini_set('memory_limit', '256M');
session_name('YSPWEB');
session_set_cookie_params([
  'lifetime' => 0,
  'path' => '/',
  'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
  'httponly' => true,
  'samesite' => 'Strict',
]);
session_start();

header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: 0');

require_once __DIR__ . '/.lib/app_version.php';
require_once __DIR__ . '/.lib/http.php';

const STATE_FILE = __DIR__ . '/.state.json';
const STATE_BACKUP_FILE = __DIR__ . '/.state.json.bak';
const STATE_LOCK_FILE = __DIR__ . '/.state.lock';
const STATE_SCHEMA_VERSION = 2;
const MATCH_GAME_MINUTES = 90.0;
const MATCH_REAL_SECONDS = 180.0;
const BALL_GRAVITY = 0.65;
const BALL_BOUNCE = 0.22;
const BALL_AIR_DRAG = 0.28;
const BALL_ROLL_DECEL = 0.050;
const BALL_SHOT_ROLL_DECEL = 0.040;
const BALL_MAX_SPEED = 0.58;
const BALL_STOP_SPEED = 0.006;
const BALL_SHORT_LINE_OVER_MARGIN = 0.025;
const BALL_SHORT_LINE_VISIBLE_MARGIN = 0.090;
const KICKOFF_PAUSE_GAME_MINUTES = 1.75;
const PITCH_WIDTH_M = 68.0;
const GOAL_WIDTH_M = 7.32;
const SQUAD_SCALE_VERSION = 3;
const SQUAD_NAME_VERSION = 2;
const SQUAD_ID_VERSION = 1;
const CPU_FORMATION_VERSION = 1;
const MANAGER_MARKET_VERSION = 5;
const MANAGER_CUP_VERSION = 3;
const MANAGER_MAX_SKILL_POINTS = 29000;
const MANAGER_MAX_BENCH_PLAYERS = 9;
const MANAGER_MAX_MATCH_SUBS = 5;
const MANAGER_MARKET_TARGET_PLAYERS = 40;
const MANAGER_SIM_RESULT_REVEAL_SECONDS = 65;
const MANAGER_SAVE_DIR = __DIR__ . '/.manager-saves';
const LEGACY_PLAYER_NAMES_FILE = __DIR__ . '/.legacy-player-names.json';
const LEGACY_YSP_ENGLAND_DIR = '';

function app_version(): string {
  return ysp_app_version(__DIR__);
}

function clean_name(string $name): string {
  return strtolower(trim($name));
}

function legacy_ysp_team_dirs(): array {
  return array_values(array_filter([
    __DIR__ . '/../../backup/ysp/code/gamefiles/data/teams/2024-25/CLUB_TEAMS/EUROPE/ENGLAND',
    __DIR__ . '/../../backup/code/gamefiles/data/teams/2024-25/CLUB_TEAMS/EUROPE/ENGLAND',
    __DIR__ . '/../backup/ysp/code/gamefiles/data/teams/2024-25/CLUB_TEAMS/EUROPE/ENGLAND',
    __DIR__ . '/../backup/code/gamefiles/data/teams/2024-25/CLUB_TEAMS/EUROPE/ENGLAND',
    LEGACY_YSP_ENGLAND_DIR,
  ], fn($dir) => is_dir($dir)));
}

function source_team_id(string $teamId): string {
  $teamId = trim($teamId);
  if (str_contains($teamId, ':')) {
    $parts = explode(':', $teamId);
    $teamId = (string)end($parts);
  }
  return $teamId;
}

function legacy_team_aliases(): array {
  return [
    'accrington-stanley' => 'accrington',
    'afc-wimbledon' => 'wimbledon',
    'cambridge-united' => 'cambridge_utd',
    'charlton-athletic' => 'charlton_ath',
    'colchester-united' => 'colchester_utd',
    'huddersfield-town' => 'huddersfield',
    'manchester-united' => 'manchester_utd',
    'nottingham-forest' => 'nott_m_forest',
    'peterborough-united' => 'peterborough_utd',
    'preston-north-end' => 'preston_n_end',
    'sheffield-wednesday' => 'sheffield_wed',
    'tottenham-hotspur' => 'tottenham',
    'west-bromwich-albion' => 'west_bromwich',
    'wycombe-wanderers' => 'wycombe',
  ];
}

function legacy_team_slug_candidates(string $teamId): array {
  $sourceId = source_team_id($teamId);
  $aliases = legacy_team_aliases();
  $direct = str_replace('-', '_', $sourceId);
  $candidates = [];
  if (isset($aliases[$sourceId])) $candidates[] = $aliases[$sourceId];
  $candidates[] = $direct;
  $candidates[] = str_replace('_united', '_utd', $direct);
  $candidates[] = str_replace('_athletic', '_ath', $direct);
  $candidates[] = str_replace('_wednesday', '_wed', $direct);
  return array_values(array_unique(array_filter($candidates)));
}

function legacy_player_name_cache(): array {
  static $data = null;
  if ($data !== null) return $data;
  if (!is_file(LEGACY_PLAYER_NAMES_FILE)) return $data = [];
  $json = json_decode(file_get_contents(LEGACY_PLAYER_NAMES_FILE) ?: '', true);
  return $data = is_array($json) ? $json : [];
}

function legacy_team_players(string $teamId, string $teamName = ''): array {
  static $cache = [];
  $cacheKey = source_team_id($teamId);
  if (isset($cache[$cacheKey])) return $cache[$cacheKey];

  $nameCache = legacy_player_name_cache();
  foreach (legacy_team_slug_candidates($teamId) as $slug) {
    if (!empty($nameCache[$slug]) && is_array($nameCache[$slug])) return $cache[$cacheKey] = $nameCache[$slug];
  }

  foreach (legacy_ysp_team_dirs() as $dir) {
    foreach (legacy_team_slug_candidates($teamId) as $slug) {
      $file = $dir . '/team.' . $slug . '.json';
      if (!is_file($file)) continue;
      $json = json_decode(file_get_contents($file) ?: '', true);
      if (!is_array($json) || empty($json['players']) || !is_array($json['players'])) continue;
      $players = [];
      foreach ($json['players'] as $player) {
        $name = trim((string)($player['name'] ?? ''));
        if ($name === '') continue;
        $row = [
          'name' => $name,
          'shirtName' => trim((string)($player['shirtName'] ?? $name)),
          'legacyRole' => trim((string)($player['role'] ?? '')),
        ];
        if (isset($player['number'])) $row['legacyNumber'] = (int)$player['number'];
        $players[] = $row;
      }
      return $cache[$cacheKey] = $players;
    }
  }

  return $cache[$cacheKey] = [];
}

function team_source_key(array $team): string {
  return source_team_id((string)($team['sourceId'] ?? $team['id'] ?? ''));
}

function apply_legacy_names_to_squad(array &$squad, string $teamId, string $teamName = ''): bool {
  $legacy = legacy_team_players($teamId, $teamName);
  if (!$legacy) return false;

  $index = 0;
  foreach (['starters', 'bench'] as $group) {
    if (empty($squad[$group]) || !is_array($squad[$group])) continue;
    foreach ($squad[$group] as &$player) {
      if (!isset($legacy[$index])) {
        $index++;
        continue;
      }
      $player['name'] = $legacy[$index]['name'];
      $player['shirtName'] = $legacy[$index]['shirtName'];
      $player['legacyRole'] = $legacy[$index]['legacyRole'];
      if (isset($legacy[$index]['legacyNumber'])) $player['legacyNumber'] = $legacy[$index]['legacyNumber'];
      $index++;
    }
    unset($player);
  }

  return true;
}

function teams(): array {
  static $teams = null;
  if ($teams !== null) return $teams;
  $rows = [
    ['afc-fylde', 'AFC FYLDE', '#ffffff', '#003b79'],
    ['aldershot-town', 'ALDERSHOT TOWN', '#d71920', '#0057b8'],
    ['altrincham', 'ALTRINCHAM', '#d71920', '#ffffff'],
    ['barrow', 'BARROW', '#0057b8', '#ffffff'],
    ['boreham-wood', 'BOREHAM WOOD', '#ffffff', '#1e1e1e'],
    ['boston-united', 'BOSTON UNITED', '#f2a900', '#1e1e1e'],
    ['carlisle-united', 'CARLISLE UNITED', '#0057b8', '#ffffff'],
    ['eastleigh', 'EASTLEIGH', '#0057b8', '#ffffff'],
    ['fc-halifax-town', 'FC HALIFAX TOWN', '#0057b8', '#ffffff'],
    ['forest-green-rovers', 'FOREST GREEN ROVERS', '#78be20', '#1e1e1e'],
    ['gateshead', 'GATESHEAD', '#ffffff', '#1e1e1e'],
    ['harrogate-town', 'HARROGATE TOWN', '#f3c300', '#1e1e1e'],
    ['hartlepool-united', 'HARTLEPOOL UNITED', '#003b79', '#ffffff'],
    ['hornchurch', 'HORNCHURCH', '#d71920', '#ffffff'],
    ['kidderminster-harriers', 'KIDDERMINSTER HARRIERS', '#d71920', '#ffffff'],
    ['scunthorpe-united', 'SCUNTHORPE UNITED', '#7a263a', '#9ec8ff'],
    ['solihull-moors', 'SOLIHULL MOORS', '#f3c300', '#0057b8'],
    ['southend-united', 'SOUTHEND UNITED', '#003b79', '#f0c420'],
    ['sutton-united', 'SUTTON UNITED', '#f2c300', '#5b1a8e'],
    ['tamworth', 'TAMWORTH', '#d71920', '#1e1e1e'],
    ['wealdstone', 'WEALDSTONE', '#0057b8', '#ffffff'],
    ['woking', 'WOKING', '#d71920', '#ffffff'],
    ['worthing', 'WORTHING', '#d71920', '#ffffff'],
    ['yeovil-town', 'YEOVIL TOWN', '#15803d', '#ffffff'],
  ];
  $teams = array_map(fn($row) => ['id' => $row[0], 'name' => $row[1], 'shirt' => $row[2], 'trim' => $row[3]], $rows);
  return $teams;
}

function team_by_id(string $id): ?array {
  foreach (teams() as $team) {
    if ($team['id'] === $id) return $team;
  }
  return null;
}

function training_divisions(): array {
  $rows = [
    ['national-league', 'National League', 43, []],
    ['league-two', 'League Two', 51, [
      ['accrington-stanley', 'ACCRINGTON STANLEY', '#d71920', '#ffffff'],
      ['barnet', 'BARNET', '#f5732c', '#1e1e1e'],
      ['bristol-rovers', 'BRISTOL ROVERS', '#0057b8', '#ffffff'],
      ['cheltenham-town', 'CHELTENHAM TOWN', '#d71920', '#ffffff'],
      ['chesterfield', 'CHESTERFIELD', '#0057b8', '#ffffff'],
      ['colchester-united', 'COLCHESTER UNITED', '#0057b8', '#ffffff'],
      ['crawley-town', 'CRAWLEY TOWN', '#d71920', '#ffffff'],
      ['crewe-alexandra', 'CREWE ALEXANDRA', '#d71920', '#ffffff'],
      ['exeter-city', 'EXETER CITY', '#d71920', '#1e1e1e'],
      ['fleetwood-town', 'FLEETWOOD TOWN', '#d71920', '#ffffff'],
      ['gillingham', 'GILLINGHAM', '#0057b8', '#ffffff'],
      ['grimsby-town', 'GRIMSBY TOWN', '#1e1e1e', '#ffffff'],
      ['newport-county', 'NEWPORT COUNTY', '#f97316', '#1e1e1e'],
      ['northampton-town', 'NORTHAMPTON TOWN', '#7a263a', '#ffffff'],
      ['oldham-athletic', 'OLDHAM ATHLETIC', '#0b4ea2', '#ffffff'],
      ['port-vale', 'PORT VALE', '#ffffff', '#1e1e1e'],
      ['rochdale', 'ROCHDALE', '#0057b8', '#1e1e1e'],
      ['rotherham-united', 'ROTHERHAM UNITED', '#d71920', '#ffffff'],
      ['salford-city', 'SALFORD CITY', '#d71920', '#ffffff'],
      ['shrewsbury-town', 'SHREWSBURY TOWN', '#0057b8', '#f3c300'],
      ['swindon-town', 'SWINDON TOWN', '#d71920', '#ffffff'],
      ['tranmere-rovers', 'TRANMERE ROVERS', '#ffffff', '#0057b8'],
      ['walsall', 'WALSALL', '#d71920', '#ffffff'],
      ['york-city', 'YORK CITY', '#c8102e', '#ffffff'],
    ]],
    ['league-one', 'League One', 59, [
      ['afc-wimbledon', 'AFC WIMBLEDON', '#0050a4', '#f3c300'],
      ['barnsley', 'BARNSLEY', '#d71920', '#ffffff'],
      ['blackpool', 'BLACKPOOL', '#f5732c', '#ffffff'],
      ['bradford-city', 'BRADFORD CITY', '#7a263a', '#f3c300'],
      ['bromley', 'BROMLEY', '#ffffff', '#1e1e1e'],
      ['burton-albion', 'BURTON ALBION', '#f3c300', '#1e1e1e'],
      ['cambridge-united', 'CAMBRIDGE UNITED', '#f3c300', '#1e1e1e'],
      ['doncaster-rovers', 'DONCASTER ROVERS', '#d71920', '#ffffff'],
      ['huddersfield-town', 'HUDDERSFIELD TOWN', '#0057b8', '#ffffff'],
      ['leicester-city', 'LEICESTER CITY', '#0057b8', '#ffffff'],
      ['leyton-orient', 'LEYTON ORIENT', '#d71920', '#ffffff'],
      ['luton-town', 'LUTON TOWN', '#f5732c', '#003b79'],
      ['mansfield-town', 'MANSFIELD TOWN', '#f3c300', '#0057b8'],
      ['mk-dons', 'MK DONS', '#ffffff', '#1e1e1e'],
      ['notts-county', 'NOTTS COUNTY', '#1e1e1e', '#ffffff'],
      ['oxford-united', 'OXFORD UNITED', '#f3c300', '#003b79'],
      ['peterborough-united', 'PETERBOROUGH UNITED', '#0057b8', '#ffffff'],
      ['plymouth-argyle', 'PLYMOUTH ARGYLE', '#006747', '#ffffff'],
      ['reading', 'READING', '#0057b8', '#ffffff'],
      ['sheffield-wednesday', 'SHEFFIELD WEDNESDAY', '#0057b8', '#ffffff'],
      ['stevenage', 'STEVENAGE', '#d71920', '#ffffff'],
      ['stockport-county', 'STOCKPORT COUNTY', '#0057b8', '#ffffff'],
      ['wigan-athletic', 'WIGAN ATHLETIC', '#0057b8', '#ffffff'],
      ['wycombe-wanderers', 'WYCOMBE WANDERERS', '#003b79', '#78c7ef'],
    ]],
    ['championship', 'Championship', 68, [
      ['birmingham-city', 'BIRMINGHAM CITY', '#0057b8', '#ffffff'],
      ['blackburn-rovers', 'BLACKBURN ROVERS', '#0057b8', '#ffffff'],
      ['bolton-wanderers', 'BOLTON WANDERERS', '#ffffff', '#003b79'],
      ['bristol-city', 'BRISTOL CITY', '#d71920', '#ffffff'],
      ['burnley', 'BURNLEY', '#7a263a', '#95bfe5'],
      ['cardiff-city', 'CARDIFF CITY', '#0057b8', '#ffffff'],
      ['charlton-athletic', 'CHARLTON ATHLETIC', '#d71920', '#ffffff'],
      ['derby-county', 'DERBY COUNTY', '#ffffff', '#1e1e1e'],
      ['lincoln-city', 'LINCOLN CITY', '#d71920', '#ffffff'],
      ['middlesbrough', 'MIDDLESBROUGH', '#d71920', '#ffffff'],
      ['millwall', 'MILLWALL', '#003b79', '#ffffff'],
      ['norwich-city', 'NORWICH CITY', '#f3c300', '#15803d'],
      ['portsmouth', 'PORTSMOUTH', '#0057b8', '#ffffff'],
      ['preston-north-end', 'PRESTON NORTH END', '#ffffff', '#003b79'],
      ['qpr', 'QPR', '#0057b8', '#ffffff'],
      ['sheffield-united', 'SHEFFIELD UNITED', '#d71920', '#ffffff'],
      ['southampton', 'SOUTHAMPTON', '#d71920', '#ffffff'],
      ['stoke-city', 'STOKE CITY', '#d71920', '#ffffff'],
      ['swansea-city', 'SWANSEA CITY', '#ffffff', '#1e1e1e'],
      ['watford', 'WATFORD', '#f3c300', '#1e1e1e'],
      ['west-bromwich-albion', 'WEST BROMWICH ALBION', '#003b79', '#ffffff'],
      ['west-ham-united', 'WEST HAM UNITED', '#7a263a', '#95bfe5'],
      ['wolves', 'WOLVES', '#f2a900', '#1e1e1e'],
      ['wrexham', 'WREXHAM', '#d71920', '#ffffff'],
    ]],
    ['premier-league', 'Premier League', 80, [
      ['arsenal', 'ARSENAL', '#d71920', '#ffffff'],
      ['aston-villa', 'ASTON VILLA', '#95bfe5', '#7a263a'],
      ['bournemouth', 'BOURNEMOUTH', '#d71920', '#1e1e1e'],
      ['brentford', 'BRENTFORD', '#d71920', '#ffffff'],
      ['brighton', 'BRIGHTON & HOVE ALBION', '#0057b8', '#ffffff'],
      ['chelsea', 'CHELSEA', '#034694', '#ffffff'],
      ['coventry-city', 'COVENTRY CITY', '#78c7ef', '#ffffff'],
      ['crystal-palace', 'CRYSTAL PALACE', '#0057b8', '#d71920'],
      ['everton', 'EVERTON', '#0057b8', '#ffffff'],
      ['fulham', 'FULHAM', '#ffffff', '#1e1e1e'],
      ['hull-city', 'HULL CITY', '#f28c28', '#1e1e1e'],
      ['ipswich-town', 'IPSWICH TOWN', '#0057b8', '#ffffff'],
      ['leeds-united', 'LEEDS UNITED', '#ffffff', '#f3c300'],
      ['liverpool', 'LIVERPOOL', '#c8102e', '#ffffff'],
      ['manchester-city', 'MANCHESTER CITY', '#6cabdd', '#ffffff'],
      ['manchester-united', 'MANCHESTER UNITED', '#d71920', '#f3c300'],
      ['newcastle-united', 'NEWCASTLE UNITED', '#1e1e1e', '#ffffff'],
      ['nottingham-forest', 'NOTTINGHAM FOREST', '#d71920', '#ffffff'],
      ['sunderland', 'SUNDERLAND', '#d71920', '#ffffff'],
      ['tottenham-hotspur', 'TOTTENHAM HOTSPUR', '#ffffff', '#132257'],
    ]],
  ];
  $rows[0][3] = array_map(fn($team) => [$team['id'], $team['name'], $team['shirt'], $team['trim']], teams());
  $divisions = [];
  foreach ($rows as $row) {
    [$divisionId, $divisionName, $base, $teamRows] = $row;
    $teamList = [];
    foreach ($teamRows as $team) {
      [$id, $name, $shirt, $trim] = $team;
      $rating = clamp_int((int)$base + (int)(crc32($id) % 11) - 5, 30, 92);
      $teamList[] = [
        'id' => $divisionId . ':' . $id,
        'sourceId' => $id,
        'divisionId' => $divisionId,
        'divisionName' => $divisionName,
        'name' => $name,
        'shirt' => $shirt,
        'trim' => $trim,
        'baseRating' => $rating,
      ];
    }
    $divisions[] = [
      'id' => $divisionId,
      'name' => $divisionName,
      'teams' => $teamList,
    ];
  }
  return $divisions;
}

function default_state(): array {
  return [
    'schemaVersion' => STATE_SCHEMA_VERSION,
    'users' => [],
    'matches' => [],
    'managerSeasons' => [],
    'managerCurrentId' => null,
    'managerCurrentByUser' => [],
    'managerPlayerIdVersion' => 0,
    'managerCpuFormationVersion' => 0,
    'managerYear' => 2026,
    'teamTrophies' => [],
    'managerLog' => [],
    'managerInvites' => [],
    'managerMarket' => [],
    'managerBrowseOffers' => [],
    'managerTransferOffers' => [],
    'managerTransferNotices' => [],
    'managerBrowseSold' => [],
  ];
}

function migrate_state(array $state): array {
  $defaults = default_state();
  $state += $defaults;
  foreach (['users', 'matches', 'managerSeasons', 'managerCurrentByUser', 'teamTrophies', 'managerLog', 'managerInvites', 'managerMarket', 'managerBrowseOffers', 'managerTransferOffers', 'managerTransferNotices', 'managerBrowseSold'] as $key) {
    if (!isset($state[$key]) || !is_array($state[$key])) $state[$key] = $defaults[$key];
  }
  if (!is_string($state['managerCurrentId'] ?? null)) $state['managerCurrentId'] = null;
  if (empty($state['managerYear']) || !is_numeric($state['managerYear'])) $state['managerYear'] = 2026;
  if (empty($state['managerCurrentByUser']) && is_string($state['managerCurrentId']) && !empty($state['managerSeasons'][$state['managerCurrentId']]['humanManagers'])) {
    foreach ($state['managerSeasons'][$state['managerCurrentId']]['humanManagers'] as $name) {
      $name = clean_name((string)$name);
      if ($name !== '') $state['managerCurrentByUser'][$name] = $state['managerCurrentId'];
    }
  }
  if ((int)($state['managerPlayerIdVersion'] ?? 0) < SQUAD_ID_VERSION) {
    normalize_state_player_ids($state);
    $state['managerPlayerIdVersion'] = SQUAD_ID_VERSION;
  }
  if ((int)($state['managerCpuFormationVersion'] ?? 0) < CPU_FORMATION_VERSION) {
    normalize_state_cpu_formations($state);
    $state['managerCpuFormationVersion'] = CPU_FORMATION_VERSION;
  }
  $state['schemaVersion'] = STATE_SCHEMA_VERSION;
  return $state;
}

function decode_state_json(string $raw): ?array {
  if (trim($raw) === '') return default_state();
  $state = json_decode($raw, true);
  return is_array($state) ? $state : null;
}

function read_state_file_locked($fh, ?string &$rawOut = null): array {
  $raw = is_file(STATE_FILE) ? (string)@file_get_contents(STATE_FILE) : '';
  $rawOut = $raw;
  $state = decode_state_json($raw ?: '');
  if ($state !== null) return $state;

  $backupRaw = is_file(STATE_BACKUP_FILE) ? (string)@file_get_contents(STATE_BACKUP_FILE) : '';
  $backupState = decode_state_json($backupRaw);
  if ($backupState !== null) {
    error_log('YSP state decode failed, recovered from backup');
    return $backupState;
  }

  error_log('YSP state decode failed and backup was unavailable');
  flock($fh, LOCK_UN);
  fclose($fh);
  reply(['ok' => false, 'error' => 'state_decode_failed'], 500);
}

function write_state_file_atomic(string $encoded): bool {
  $dir = dirname(STATE_FILE);
  $tmp = $dir . '/.state.json.tmp.' . getmypid() . '.' . bin2hex(random_bytes(3));
  $current = is_file(STATE_FILE) ? (string)@file_get_contents(STATE_FILE) : '';
  if (decode_state_json($current) !== null && trim($current) !== '') {
    @file_put_contents(STATE_BACKUP_FILE, $current, LOCK_EX);
    @chmod(STATE_BACKUP_FILE, 0666);
  }
  if (@file_put_contents($tmp, $encoded, LOCK_EX) === false) {
    @unlink($tmp);
    return write_state_file_in_place($encoded);
  }
  @chmod($tmp, 0666);
  if (!@rename($tmp, STATE_FILE)) {
    @unlink($tmp);
    return write_state_file_in_place($encoded);
  }
  @chmod(STATE_FILE, 0666);
  return true;
}

function write_state_file_in_place(string $encoded): bool {
  $fh = @fopen(STATE_FILE, 'c+');
  if (!$fh) return false;
  rewind($fh);
  if (!ftruncate($fh, 0)) {
    fclose($fh);
    return false;
  }
  $written = fwrite($fh, $encoded);
  fflush($fh);
  fclose($fh);
  @chmod(STATE_FILE, 0666);
  return $written === strlen($encoded);
}

function with_state(callable $fn): mixed {
  $fh = @fopen(STATE_LOCK_FILE, 'c');
  if (!$fh) $fh = @fopen(STATE_FILE, 'c+');
  if (!$fh) reply(['ok' => false, 'error' => 'state_lock_failed'], 500);
  @chmod(STATE_LOCK_FILE, 0666);
  flock($fh, LOCK_EX);
  $rawState = '';
  $state = read_state_file_locked($fh, $rawState);
  $state = migrate_state($state);
  cleanup_state($state);
  $result = $fn($state);
  $encoded = json_encode($state, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
  if (!is_string($encoded)) {
    $error = json_last_error_msg();
    flock($fh, LOCK_UN);
    fclose($fh);
    reply(['ok' => false, 'error' => 'state_encode_failed', 'jsonError' => $error], 500);
  }
  if ($encoded !== trim($rawState) && !write_state_file_atomic($encoded)) {
    flock($fh, LOCK_UN);
    fclose($fh);
    reply(['ok' => false, 'error' => 'state_write_failed'], 500);
  }
  flock($fh, LOCK_UN);
  fclose($fh);
  return $result;
}

function cleanup_state(array &$state): void {
  $now = time();
  foreach ($state['matches'] as $id => $match) {
    $age = $now - (int)($match['updated'] ?? $match['created'] ?? $now);
    if (($match['status'] ?? '') === 'finished' && $age > 3600) unset($state['matches'][$id]);
    if (($match['status'] ?? '') === 'waiting' && $age > 900) unset($state['matches'][$id]);
  }
  foreach (($state['managerBrowseOffers'] ?? []) as $user => $offers) {
    if (!is_array($offers)) {
      unset($state['managerBrowseOffers'][$user]);
      continue;
    }
    foreach ($offers as $offerId => $offer) {
      if ((int)($offer['expires'] ?? 0) > 0 && (int)$offer['expires'] < $now) unset($state['managerBrowseOffers'][$user][$offerId]);
    }
    if (empty($state['managerBrowseOffers'][$user])) unset($state['managerBrowseOffers'][$user]);
  }
  foreach (($state['managerTransferOffers'] ?? []) as $offerId => $offer) {
    if (
      !is_array($offer)
      || ($offer['status'] ?? 'pending') !== 'pending'
      || ((int)($offer['expires'] ?? 0) > 0 && (int)$offer['expires'] < $now)
    ) {
      unset($state['managerTransferOffers'][$offerId]);
    }
  }
  foreach (($state['managerTransferNotices'] ?? []) as $noticeId => $notice) {
    if (!is_array($notice) || (int)($notice['created'] ?? 0) < $now - 259200) unset($state['managerTransferNotices'][$noticeId]);
  }
}

function public_teams(array $state, ?string $self = null): array {
  return array_map(function ($team) {
    $team['taken'] = false;
    $team['baseRating'] = (int)($team['baseRating'] ?? team_base_rating((string)($team['sourceId'] ?? $team['id'] ?? '')));
    return $team;
  }, teams());
}

function require_user(array $state): string {
  $user = $_SESSION['user'] ?? '';
  if (!is_string($user) || !isset($state['users'][$user])) reply(['ok' => false, 'error' => 'not_logged_in'], 401);
  return $user;
}

function has_team(array $user): bool {
  return !empty($user['teamId']) && !empty($user['teamName']);
}

function public_user(string $name, array $user): array {
  $level = clamp_int((int)($user['managerDivisionLevel'] ?? 5), 1, 5);
  return [
    'name' => $name,
    'admin' => $name === 'jdc',
    'team' => has_team($user) ? user_team($user) : null,
    'managerDivisionName' => manager_division_name($level),
  ];
}

function user_match_id(array $state, string $user): ?string {
  foreach ($state['matches'] as $id => $match) {
    if (($match['status'] ?? '') === 'finished') continue;
    $players = $match['players'] ?? [];
    if (($players['home'] ?? null) === $user || ($players['away'] ?? null) === $user) return $id;
  }
  return null;
}

function team_base_rating(string $teamId): int {
  foreach (training_divisions() as $division) {
    foreach (($division['teams'] ?? []) as $team) {
      if (($team['id'] ?? '') === $teamId || ($team['sourceId'] ?? '') === $teamId) {
        return (int)($team['baseRating'] ?? 43);
      }
    }
  }
  return clamp_int(43 + (int)(crc32($teamId) % 11) - 5, 30, 92);
}

function cpu_team(array $state, string $userTeamId): array {
  $taken = [];
  foreach ($state['users'] as $user) {
    if (!empty($user['teamId'])) $taken[$user['teamId']] = true;
  }
  foreach (teams() as $team) {
    if ($team['id'] !== $userTeamId && empty($taken[$team['id']])) return $team;
  }
  foreach (teams() as $team) {
    if ($team['id'] !== $userTeamId) return $team;
  }
  return teams()[0];
}

function new_match(string $home, array $homeUser): array {
  $now = microtime(true);
  ensure_user_squad($homeUser);
  $homeBase = team_base_rating((string)$homeUser['teamId']);
  return [
    'id' => 'm' . bin2hex(random_bytes(5)),
    'status' => 'waiting',
    'created' => time(),
    'updated' => time(),
    'lastTick' => $now,
    'elapsed' => 0,
    'players' => ['home' => $home, 'away' => null],
    'teams' => ['home' => user_team($homeUser), 'away' => null],
    'units' => ['home' => user_roster($homeUser, 'home'), 'away' => []],
    'bench' => ['home' => user_bench($homeUser, 'home'), 'away' => []],
    'score' => [0, 0],
    'ball' => stopped_kickoff_ball(),
    'tactics' => ['home' => 'balanced', 'away' => 'balanced'],
    'engine' => default_engine($homeBase, 0),
    'events' => [],
  ];
}

function new_cpu_match(string $home, array $homeUser, array $state): array {
  $now = microtime(true);
  ensure_user_squad($homeUser);
  $homeBase = team_base_rating((string)$homeUser['teamId']);
  $awayTeam = cpu_team($state, (string)$homeUser['teamId']);
  $awayBase = team_base_rating((string)$awayTeam['id']);
  $awaySquad = make_named_squad('away', $awayBase, team_source_key($awayTeam), (string)($awayTeam['name'] ?? ''));
  $match = [
    'id' => 'm' . bin2hex(random_bytes(5)),
    'status' => 'ready',
    'created' => time(),
    'updated' => time(),
    'lastTick' => $now,
    'elapsed' => 0,
    'players' => ['home' => $home, 'away' => '__cpu__'],
    'teams' => ['home' => user_team($homeUser), 'away' => $awayTeam],
    'units' => ['home' => user_roster($homeUser, 'home'), 'away' => $awaySquad['starters']],
    'bench' => ['home' => user_bench($homeUser, 'home'), 'away' => $awaySquad['bench']],
    'score' => [0, 0],
    'ball' => stopped_kickoff_ball(),
    'tactics' => ['home' => 'balanced', 'away' => 'balanced'],
    'engine' => default_engine($homeBase, $awayBase),
    'events' => [],
  ];
  reset_units($match);
  return $match;
}

function user_team(array $user): array {
  return [
    'id' => $user['teamId'],
    'name' => $user['teamName'],
    'shirt' => $user['shirt'] ?? '#f5732c',
    'trim' => $user['trim'] ?? '#1e1e1e',
  ];
}

function valid_tactic(string $mode): string {
  return in_array($mode, ['defend', 'balanced', 'attack'], true) ? $mode : 'balanced';
}

function user_tactic(array $user): string {
  return valid_tactic((string)($user['manager']['tactic'] ?? 'balanced'));
}

function normalize_squad_left_right(array &$squad): void {
  foreach (['starters', 'bench'] as $group) {
    if (empty($squad[$group]) || !is_array($squad[$group])) continue;
    foreach ($squad[$group] as &$player) {
      ensure_player_roles($player);
      $slot = player_slot_role($player);
      $x = (float)($player['homeX'] ?? $player['x'] ?? 0.5);
      $shouldFlip = (in_array($slot, ['LB', 'LM'], true) && $x > 0.5) || (in_array($slot, ['RB', 'RM'], true) && $x < 0.5);
      if (!$shouldFlip) continue;
      foreach (['homeX', 'x'] as $key) {
        if (isset($player[$key])) $player[$key] = round(1 - (float)$player[$key], 4);
      }
    }
    unset($player);
  }
}

function player_id_slug(string $value): string {
  $slug = strtolower((string)preg_replace('/[^a-z0-9]+/i', '-', trim($value)));
  return trim($slug, '-');
}

function player_team_id_key(string $teamId = '', string $teamName = '', string $side = 'home'): string {
  $key = source_team_id($teamId);
  if ($key === '') $key = $teamId;
  if ($key === '') $key = $teamName;
  if ($key === '') $key = $side;
  return player_id_slug($key) ?: player_id_slug($side) ?: 'team';
}

function local_player_id_number(string $id): int {
  return preg_match('/^[HA](\d+)$/', $id, $m) ? (int)$m[1] : 0;
}

function generated_squad_player_id(string $teamId, string $teamName, string $side, int $number): string {
  $number = max(1, $number);
  return 'p_' . player_team_id_key($teamId, $teamName, $side) . '_' . str_pad((string)$number, 2, '0', STR_PAD_LEFT);
}

function squad_has_player_id(array $squad, string $id): bool {
  if ($id === '') return false;
  foreach (['starters', 'bench'] as $group) {
    foreach (($squad[$group] ?? []) as $player) {
      if ((string)($player['id'] ?? '') === $id) return true;
    }
  }
  return false;
}

function normalize_squad_player_ids(array &$squad, string $teamId = '', string $teamName = '', string $side = 'home'): void {
  $used = [];
  foreach (['starters', 'bench'] as $group) {
    if (empty($squad[$group]) || !is_array($squad[$group])) continue;
    $offset = $group === 'bench' ? 12 : 1;
    foreach ($squad[$group] as $index => &$player) {
      $oldId = (string)($player['id'] ?? '');
      $fallbackNumber = $offset + (int)$index;
      $localNumber = local_player_id_number($oldId);
      $identityNumber = $localNumber > 0 ? $localNumber : $fallbackNumber;
      if (empty($player['shirtNumber'])) {
        $shirtNumber = (int)($player['legacyNumber'] ?? ($localNumber ?: $fallbackNumber));
        $player['shirtNumber'] = max(1, $shirtNumber);
      }
      if ($oldId === '' || $localNumber > 0) {
        if ($oldId !== '') $player['legacyLocalId'] = $oldId;
        $player['id'] = generated_squad_player_id($teamId, $teamName, $side, $identityNumber);
      }
      $id = (string)($player['id'] ?? '');
      if ($id === '' || isset($used[$id])) {
        $player['id'] = next_transfer_player_id(['starters' => array_map(fn($pid) => ['id' => $pid], array_keys($used)), 'bench' => []]);
        $id = (string)$player['id'];
      }
      $used[$id] = true;
    }
    unset($player);
  }
}

function ensure_player_id_unique_for_squad(array &$player, array $squad): void {
  $id = (string)($player['id'] ?? '');
  if ($id === '' || squad_has_player_id($squad, $id)) {
    $player['id'] = next_transfer_player_id($squad);
  }
}

function normalize_state_player_ids(array &$state): void {
  if (!empty($state['users']) && is_array($state['users'])) {
    foreach ($state['users'] as &$user) {
      if (is_array($user) && has_team($user)) ensure_user_squad($user);
    }
    unset($user);
  }
  if (!empty($state['managerSeasons']) && is_array($state['managerSeasons'])) {
    foreach ($state['managerSeasons'] as &$season) {
      if (!is_array($season)) continue;
      foreach (['participants', 'cupParticipants'] as $listKey) {
        if (empty($season[$listKey]) || !is_array($season[$listKey])) continue;
        foreach ($season[$listKey] as &$participant) {
          if (!is_array($participant) || empty($participant['squad']) || !is_array($participant['squad'])) continue;
          $team = is_array($participant['team'] ?? null) ? $participant['team'] : [];
          normalize_squad_player_ids($participant['squad'], team_source_key($team), (string)($team['name'] ?? ''), 'home');
          ensure_squad_stats($participant['squad']);
          $participant['idVersion'] = SQUAD_ID_VERSION;
          $participant['rating'] = squad_skill_rating($participant['squad']);
          $participant['strength'] = squad_skill_points($participant['squad']);
        }
        unset($participant);
      }
    }
    unset($season);
  }
}

function ensure_user_squad(array &$user): void {
  if (!has_team($user)) return;
  $teamId = (string)$user['teamId'];
  if (
    ($user['squadTeamId'] ?? '') === $teamId
    && (int)($user['squadScaleVersion'] ?? 0) >= SQUAD_SCALE_VERSION
    && !empty($user['squad']['starters'])
    && !empty($user['squad']['bench'])
  ) {
    normalize_squad_player_ids($user['squad'], $teamId, (string)($user['teamName'] ?? ''), 'home');
    ensure_squad_stats($user['squad']);
    normalize_squad_left_right($user['squad']);
    if ((int)($user['squadNameVersion'] ?? 0) < SQUAD_NAME_VERSION) {
      apply_legacy_names_to_squad($user['squad'], $teamId, (string)($user['teamName'] ?? ''));
      $user['squadNameVersion'] = SQUAD_NAME_VERSION;
    }
    $user['squadIdVersion'] = SQUAD_ID_VERSION;
    $user['manager']['formation'] = manager_user_formation($user);
    return;
  }
  $base = team_base_rating($teamId);
  $user['squadTeamId'] = $teamId;
  $user['squadScaleVersion'] = SQUAD_SCALE_VERSION;
  $user['squad'] = make_named_squad('home', $base, $teamId, (string)($user['teamName'] ?? ''));
  $user['squadNameVersion'] = SQUAD_NAME_VERSION;
  $user['squadIdVersion'] = SQUAD_ID_VERSION;
  ensure_squad_stats($user['squad']);
  normalize_squad_left_right($user['squad']);
  $user['manager']['formation'] = manager_user_formation($user);
  $user['manager']['tactic'] = user_tactic($user);
}

function reset_user_manager_squad(array &$user): void {
  if (!has_team($user)) return;
  unset(
    $user['squad'],
    $user['squadTeamId'],
    $user['squadScaleVersion'],
    $user['squadNameVersion'],
    $user['squadIdVersion'],
    $user['manager']
  );
  ensure_user_squad($user);
}

function reset_user_manager_finance(array &$user): void {
  $level = clamp_int((int)($user['managerDivisionLevel'] ?? 5), 1, 5);
  unset($user['managerFinance']);
  $user['managerDivisionLevel'] = $level;
  ensure_manager_finance($user);
}

function reset_user_manager_transfer_state(array &$state, string $user): void {
  $user = clean_name($user);
  if ($user === '') return;
  if (isset($state['managerMarket']) && is_array($state['managerMarket'])) {
    $state['managerMarket'] = array_values(array_filter($state['managerMarket'], fn($player) => !is_array($player) || clean_name((string)($player['marketListedByUser'] ?? '')) !== $user));
  }
  foreach (['managerTransferOffers', 'managerTransferNotices'] as $key) {
    if (empty($state[$key]) || !is_array($state[$key])) continue;
    foreach ($state[$key] as $id => $item) {
      if (!is_array($item)) continue;
      if (
        clean_name((string)($item['buyer'] ?? '')) === $user
        || clean_name((string)($item['seller'] ?? '')) === $user
        || clean_name((string)($item['user'] ?? '')) === $user
      ) {
        unset($state[$key][$id]);
      }
    }
  }
  if (isset($state['managerBrowseOffers'][$user])) unset($state['managerBrowseOffers'][$user]);
}

function reset_active_manager_game(array &$state): void {
  $state['matches'] = [];
  $state['managerSeasons'] = [];
  $state['managerCurrentId'] = null;
  $state['managerCurrentByUser'] = [];
  $state['managerYear'] = manager_base_season_year();
  $state['managerLog'] = [];
  $state['managerInvites'] = [];
  $state['managerMarket'] = [];
  unset($state['managerMarketVersion']);
  $state['managerBrowseOffers'] = [];
  $state['managerTransferOffers'] = [];
  $state['managerTransferNotices'] = [];
  $state['managerBrowseSold'] = [];
  $state['teamTrophies'] = [];
  $state['managerPlayerIdVersion'] = SQUAD_ID_VERSION;
  $state['managerCpuFormationVersion'] = CPU_FORMATION_VERSION;
  foreach (array_keys($state['users'] ?? []) as $name) {
    unset(
      $state['users'][$name]['squad'],
      $state['users'][$name]['squadTeamId'],
      $state['users'][$name]['squadScaleVersion'],
      $state['users'][$name]['squadNameVersion'],
      $state['users'][$name]['squadIdVersion'],
      $state['users'][$name]['manager'],
      $state['users'][$name]['managerFinance'],
      $state['users'][$name]['managerDivisionLevel']
    );
    if (has_team($state['users'][$name])) {
      $state['users'][$name]['managerDivisionLevel'] = 5;
      ensure_user_squad($state['users'][$name]);
      ensure_manager_finance($state['users'][$name]);
    }
  }
}

function side_squad(array $players, string $side, int $offset = 0): array {
  $out = [];
  foreach ($players as $index => $player) {
    ensure_player_stats($player);
    $number = $index + 1 + $offset;
    if (!empty($player['shirtNumber'])) $number = (int)$player['shirtNumber'];
    elseif (!empty($player['legacyNumber'])) $number = (int)$player['legacyNumber'];
    elseif (($localNumber = local_player_id_number((string)($player['id'] ?? ''))) > 0) $number = $localNumber;
    $homeY = (float)($player['homeY'] ?? $player['y'] ?? 0.5);
    $y = $side === 'away' ? 1.0 - $homeY : $homeY;
    $slotRole = player_slot_role($player);
    $baseRating = (int)($player['rating'] ?? 50);
    $player['shirtNumber'] = max(1, $number);
    $player['baseRating'] = $baseRating;
    $player['effectiveRating'] = player_effective_rating($player);
    $player['rating'] = $player['effectiveRating'];
    $player['displayRole'] = player_display_role($player);
    $player['naturalRole'] = player_natural_role($player);
    $player['slotRole'] = $slotRole;
    $player['role'] = $slotRole;
    $player['foot'] = player_preferred_foot($player);
    $player['footBonus'] = player_foot_slot_bonus($player, $slotRole);
    $player['positionPenalty'] = player_position_penalty_percent($player, $slotRole);
    $player['homeY'] = $y;
    $player['y'] = $y;
    $player['x'] = (float)($player['homeX'] ?? $player['x'] ?? 0.5);
    $out[] = $player;
  }
  return $out;
}

function user_roster(array $user, string $side): array {
  if (!empty($user['squad']['starters']) && is_array($user['squad']['starters'])) {
    return side_squad($user['squad']['starters'], $side, 0);
  }
  $squad = make_named_squad($side, team_base_rating((string)($user['teamId'] ?? '')), (string)($user['teamId'] ?? ''), (string)($user['teamName'] ?? ''));
  return $squad['starters'];
}

function user_bench(array $user, string $side): array {
  if (!empty($user['squad']['bench']) && is_array($user['squad']['bench'])) {
    return side_squad(array_slice($user['squad']['bench'], 0, MANAGER_MAX_BENCH_PLAYERS), $side, 11);
  }
  $squad = make_named_squad($side, team_base_rating((string)($user['teamId'] ?? '')), (string)($user['teamId'] ?? ''), (string)($user['teamName'] ?? ''));
  return side_squad(array_slice($squad['bench'], 0, MANAGER_MAX_BENCH_PLAYERS), $side, 11);
}

function public_manager(array $user): array {
  $starters = $user['squad']['starters'] ?? [];
  $bench = $user['squad']['bench'] ?? [];
  return [
    'team' => user_team($user),
    'tactic' => user_tactic($user),
    'rating' => average_rating($starters),
    'starters' => $starters,
    'bench' => $bench,
  ];
}

function public_manager_users(array $state): array {
  $users = [];
  foreach ($state['users'] as $name => $user) {
    $name = (string)$name;
    $users[] = [
      'name' => $name,
      'ready' => has_team($user),
      'team' => has_team($user) ? user_team($user) : null,
      'lastSeen' => (int)($user['lastSeen'] ?? 0),
    ];
  }
  usort($users, fn($a, $b) => strcmp((string)$a['name'], (string)$b['name']));
  return $users;
}

function public_manager_invites(array $state, string $user): array {
  $out = [];
  foreach (($state['managerInvites'] ?? []) as $id => $invite) {
    if (($invite['status'] ?? 'pending') !== 'pending') continue;
    $users = array_values(array_map('strval', $invite['users'] ?? []));
    if (!in_array($user, $users, true)) continue;
    $accepted = $invite['accepted'] ?? [];
    $from = (string)($invite['from'] ?? '');
    $ready = array_values(array_filter($users, fn($name) => !empty($accepted[$name])));
    $waiting = array_values(array_filter($users, fn($name) => empty($accepted[$name])));
    $out[] = [
      'id' => (string)$id,
      'from' => $from,
      'users' => $users,
      'accepted' => $accepted,
      'ready' => $ready,
      'waiting' => $waiting,
      'readyCount' => count($ready),
      'neededCount' => count($users),
      'needsMe' => empty($accepted[$user]),
      'divisionName' => manager_division_name((int)($invite['divisionLevel'] ?? 5)),
    ];
  }
  return $out;
}

function manager_invite_ready(array $invite): bool {
  $accepted = $invite['accepted'] ?? [];
  foreach (($invite['users'] ?? []) as $name) {
    if (empty($accepted[$name])) return false;
  }
  return true;
}

function manager_cpu_id(string $teamId): string {
  return 'cpu:' . $teamId;
}

function is_cpu_manager(string $id): bool {
  return str_starts_with($id, 'cpu:');
}

function manager_human_team_keys(array $state, ?array $season = null): array {
  $keys = [];
  $names = is_array($season) ? array_values($season['humanManagers'] ?? []) : array_keys($state['users'] ?? []);
  foreach ($names as $name) {
    $name = (string)$name;
    if (isset($state['users'][$name]) && has_team($state['users'][$name])) {
      $teamId = (string)($state['users'][$name]['teamId'] ?? '');
      if ($teamId !== '') {
        $keys[$teamId] = true;
        $keys[source_team_id($teamId)] = true;
      }
    }
    if (is_array($season)) {
      $participant = manager_participant($season, $name);
      $team = is_array($participant['team'] ?? null) ? $participant['team'] : [];
      $teamId = (string)($team['id'] ?? '');
      $sourceId = (string)($team['sourceId'] ?? $teamId);
      foreach ([$teamId, $sourceId, source_team_id($teamId), source_team_id($sourceId)] as $key) {
        if ($key !== '') $keys[$key] = true;
      }
    }
  }
  return $keys;
}

function manager_team_unique_key(array $team): string {
  foreach ([(string)($team['sourceId'] ?? ''), (string)($team['id'] ?? '')] as $id) {
    $key = source_team_id($id);
    if ($key !== '') return $key;
  }
  return player_id_slug((string)($team['name'] ?? ''));
}

function manager_duplicate_human_team(array $state, array $selected): ?array {
  $seen = [];
  foreach ($selected as $name) {
    $name = clean_name((string)$name);
    if ($name === '' || !isset($state['users'][$name]) || !has_team($state['users'][$name])) continue;
    $team = user_team($state['users'][$name]);
    $key = manager_team_unique_key($team);
    if ($key === '') continue;
    if (isset($seen[$key])) {
      return [
        'team' => $team,
        'users' => [$seen[$key], $name],
      ];
    }
    $seen[$key] = $name;
  }
  return null;
}

function manager_team_controlled_by_human(array $state, ?array $season, string $teamId): bool {
  $teamId = source_team_id($teamId);
  if ($teamId === '') return false;
  $keys = manager_human_team_keys($state, $season);
  return isset($keys[$teamId]);
}

function manager_division_id(int $level): string {
  return match (clamp_int($level, 1, 5)) {
    1 => 'premier-league',
    2 => 'championship',
    3 => 'league-one',
    4 => 'league-two',
    default => 'national-league',
  };
}

function manager_division_target_count(int $level): int {
  return clamp_int($level, 1, 5) === 1 ? 20 : 24;
}

function manager_promotion_rules(int $level): array {
  return match (clamp_int($level, 1, 5)) {
    1 => [
      'direct' => 0,
      'playoffStart' => 0,
      'playoffEnd' => 0,
      'relegation' => 0,
      'text' => 'Premier League: no promotion above this division.',
    ],
    2, 3 => [
      'direct' => 2,
      'playoffStart' => 3,
      'playoffEnd' => 6,
      'relegation' => 0,
      'text' => 'Top 2 promoted. 3rd-6th enter playoffs.',
    ],
    4 => [
      'direct' => 3,
      'playoffStart' => 4,
      'playoffEnd' => 7,
      'relegation' => 2,
      'text' => 'Top 3 promoted. 4th-7th enter playoffs. Bottom 2 relegated.',
    ],
    default => [
      'direct' => 1,
      'playoffStart' => 2,
      'playoffEnd' => 7,
      'relegation' => 0,
      'text' => 'Champion promoted. 2nd-7th enter playoffs.',
    ],
  };
}

function manager_relegation_count_from_above(int $level): int {
  return match (clamp_int($level, 1, 5)) {
    2, 3 => 3,
    4 => 4,
    5 => 2,
    default => 0,
  };
}

function manager_seeded_unit(string $seed): float {
  return (crc32($seed) % 10000) / 10000.0;
}

function manager_shadow_team_strength(array $team): int {
  $base = (int)($team['baseRating'] ?? team_base_rating((string)($team['id'] ?? '')));
  return $base * 360 + (int)(crc32((string)($team['id'] ?? '') . '|shadow') % 900);
}

function manager_shadow_score(string $seed, int $homeStrength, int $awayStrength): array {
  $odds = manager_match_probabilities($homeStrength, $awayStrength);
  $roll = 1 + (int)floor(manager_seeded_unit($seed . '|result') * 100);
  if ($roll <= $odds['home']) $outcome = 'home';
  elseif ($roll <= $odds['home'] + $odds['draw']) $outcome = 'draw';
  else $outcome = 'away';
  $baseGoals = (int)floor(manager_seeded_unit($seed . '|goals') * 3);
  if ($outcome === 'draw') return [$baseGoals, $baseGoals];
  $loserGoals = (int)floor(manager_seeded_unit($seed . '|loser') * 2);
  $margin = 1 + (manager_seeded_unit($seed . '|margin') > 0.78 ? 1 : 0);
  $winnerGoals = min(5, $loserGoals + $margin);
  return $outcome === 'home' ? [$winnerGoals, $loserGoals] : [$loserGoals, $winnerGoals];
}

function manager_shadow_relegation_report(int $level, string $seasonId): array {
  $level = clamp_int($level, 1, 5);
  $count = manager_relegation_count_from_above($level);
  if ($count <= 0 || $level <= 1 || $seasonId === '') return ['teams' => [], 'table' => []];
  $seed = $seasonId . '|relegated|' . $level;
  $upperTeams = manager_division_teams($level - 1);
  $rows = [];
  foreach ($upperTeams as $team) {
    $id = (string)($team['id'] ?? '');
    if ($id === '') continue;
    $rows[$id] = [
      'team' => $team,
      'played' => 0,
      'won' => 0,
      'drawn' => 0,
      'lost' => 0,
      'gf' => 0,
      'ga' => 0,
      'gd' => 0,
      'points' => 0,
      'strength' => manager_shadow_team_strength($team),
    ];
  }
  $ids = array_keys($rows);
  for ($i = 0; $i < count($ids); $i++) {
    for ($j = $i + 1; $j < count($ids); $j++) {
      $home = $ids[$i];
      $away = $ids[$j];
      [$hg, $ag] = manager_shadow_score($seed . '|' . $home . '|' . $away, (int)$rows[$home]['strength'], (int)$rows[$away]['strength']);
      foreach ([[$home, $hg, $ag], [$away, $ag, $hg]] as $result) {
        [$id, $gf, $ga] = $result;
        $rows[$id]['played']++;
        $rows[$id]['gf'] += $gf;
        $rows[$id]['ga'] += $ga;
        $rows[$id]['gd'] = $rows[$id]['gf'] - $rows[$id]['ga'];
        if ($gf > $ga) {
          $rows[$id]['won']++;
          $rows[$id]['points'] += 3;
        } elseif ($gf === $ga) {
          $rows[$id]['drawn']++;
          $rows[$id]['points'] += 1;
        } else {
          $rows[$id]['lost']++;
        }
      }
    }
  }
  $table = array_values($rows);
  usort($table, fn($a, $b) => [$b['points'], $b['gd'], $b['gf'], $a['team']['name'] ?? ''] <=> [$a['points'], $a['gd'], $a['gf'], $b['team']['name'] ?? '']);
  $relegatedRows = array_slice(array_reverse($table), 0, $count);
  return [
    'fromDivision' => manager_division_name($level - 1),
    'teams' => array_map(fn($row) => $row['team'], $relegatedRows),
    'table' => $table,
  ];
}

function manager_relegated_from_above_teams(int $level, string $seasonId, ?array $previousSeason = null): array {
  if (is_array($previousSeason)) {
    $stored = $previousSeason['shadowRelegationFromAbove']['teams'] ?? $previousSeason['relegatedFromAbove'] ?? null;
    if (is_array($stored)) return array_values($stored);
  }
  return manager_shadow_relegation_report($level, $seasonId)['teams'];
}

function manager_division_teams(int $level): array {
  $divisionId = manager_division_id($level);
  foreach (training_divisions() as $division) {
    if (($division['id'] ?? '') !== $divisionId) continue;
    return array_map(function ($team) use ($divisionId) {
      return [
        'id' => $divisionId === 'national-league' ? (string)($team['sourceId'] ?? $team['id']) : (string)$team['id'],
        'sourceId' => (string)($team['sourceId'] ?? $team['id']),
        'divisionId' => $divisionId,
        'divisionName' => (string)($team['divisionName'] ?? manager_division_name(5)),
        'name' => (string)$team['name'],
        'shirt' => (string)($team['shirt'] ?? '#ffffff'),
        'trim' => (string)($team['trim'] ?? '#111111'),
        'baseRating' => (int)($team['baseRating'] ?? team_base_rating((string)($team['id'] ?? ''))),
      ];
    }, $division['teams'] ?? []);
  }
  return teams();
}

function manager_all_division_teams(): array {
  $teams = [];
  for ($level = 1; $level <= 5; $level++) {
    foreach (manager_division_teams($level) as $team) {
      $team['managerDivisionLevel'] = $level;
      $team['managerDivisionName'] = manager_division_name($level);
      $teams[] = $team;
    }
  }
  return $teams;
}

function cup_cpu_manager_participant(array $team, int $divisionLevel): array {
  $participant = cpu_manager_participant($team, $divisionLevel);
  unset($participant['squad']);
  return $participant;
}

function manager_human_cup_participant(array &$state, string $name): ?array {
  if (!isset($state['users'][$name]) || !has_team($state['users'][$name])) return null;
  ensure_user_squad($state['users'][$name]);
  $team = user_team($state['users'][$name]);
  $squad = $state['users'][$name]['squad'] ?? ['starters' => [], 'bench' => []];
  $level = clamp_int((int)($state['users'][$name]['managerDivisionLevel'] ?? 5), 1, 5);
  return [
    'user' => $name,
    'managerType' => 'human',
    'team' => $team,
    'rating' => squad_skill_rating($squad),
    'strength' => squad_skill_points($squad),
    'divisionLevel' => $level,
    'divisionName' => manager_division_name($level),
  ];
}

function manager_cup_participants_for_levels(array &$state, array $selected, array $levels, int $maxTeams = 0): array {
  $levels = array_values(array_unique(array_map(fn($level) => clamp_int((int)$level, 1, 5), $levels)));
  $participants = [];
  $managedTeamIds = [];
  foreach ($selected as $name) {
    $participant = manager_human_cup_participant($state, (string)$name);
    if ($participant === null || !in_array((int)$participant['divisionLevel'], $levels, true)) continue;
    $team = $participant['team'];
    $managedTeamIds[(string)$team['id']] = true;
    $managedTeamIds[(string)($team['sourceId'] ?? $team['id'])] = true;
    $participants[] = $participant;
  }
  $cpu = [];
  foreach ($levels as $level) {
    foreach (manager_division_teams($level) as $team) {
      $teamId = (string)($team['id'] ?? '');
      $sourceId = (string)($team['sourceId'] ?? $teamId);
      if ($teamId === '' || isset($managedTeamIds[$teamId]) || ($sourceId !== '' && isset($managedTeamIds[$sourceId]))) continue;
      $team['managerDivisionLevel'] = $level;
      $team['managerDivisionName'] = manager_division_name($level);
      $cpu[] = cup_cpu_manager_participant($team, $level);
    }
  }
  usort($cpu, fn($a, $b) => ((int)($b['rating'] ?? 0)) <=> ((int)($a['rating'] ?? 0)));
  foreach ($cpu as $participant) {
    if ($maxTeams > 0 && count($participants) >= $maxTeams) break;
    $participants[] = $participant;
  }
  return $participants;
}

function manager_cup_participants(array &$state, array $selected): array {
  return manager_cup_participants_for_levels($state, $selected, [1, 2, 3, 4, 5]);
}

function manager_generated_team(string $id, string $name, int $baseRating, string $shirt, string $trim, string $divisionName): array {
  return [
    'id' => $id,
    'sourceId' => $id,
    'divisionId' => strtolower(preg_replace('/[^a-z0-9]+/', '-', $divisionName)),
    'divisionName' => $divisionName,
    'name' => $name,
    'shirt' => $shirt,
    'trim' => $trim,
    'baseRating' => $baseRating,
  ];
}

function manager_academy_cup_participants(): array {
  $clubs = [
    ['arsenal-u21', 'ARSENAL U21', 76, '#d71920', '#ffffff'],
    ['aston-villa-u21', 'ASTON VILLA U21', 69, '#7a003c', '#95c5e8'],
    ['chelsea-u21', 'CHELSEA U21', 77, '#034694', '#ffffff'],
    ['everton-u21', 'EVERTON U21', 68, '#003399', '#ffffff'],
    ['leeds-united-u21', 'LEEDS UNITED U21', 67, '#ffffff', '#1d428a'],
    ['leicester-city-u21', 'LEICESTER CITY U21', 70, '#003090', '#fdbE11'],
    ['liverpool-u21', 'LIVERPOOL U21', 78, '#c8102e', '#f6eb61'],
    ['manchester-city-u21', 'MANCHESTER CITY U21', 80, '#6cabdd', '#1c2c5b'],
    ['manchester-united-u21', 'MANCHESTER UNITED U21', 78, '#da291c', '#fbe122'],
    ['newcastle-united-u21', 'NEWCASTLE UNITED U21', 70, '#111111', '#ffffff'],
    ['nottingham-forest-u21', 'NOTTINGHAM FOREST U21', 66, '#dd0000', '#ffffff'],
    ['southampton-u21', 'SOUTHAMPTON U21', 65, '#d71920', '#ffffff'],
    ['tottenham-hotspur-u21', 'TOTTENHAM HOTSPUR U21', 76, '#ffffff', '#132257'],
    ['west-ham-united-u21', 'WEST HAM UNITED U21', 68, '#7a263a', '#1bb1e7'],
    ['wolves-u21', 'WOLVES U21', 66, '#fdb913', '#231f20'],
    ['brighton-u21', 'BRIGHTON U21', 68, '#0057b8', '#ffffff'],
  ];
  return array_map(function ($club) {
    [$id, $name, $base, $shirt, $trim] = $club;
    return cup_cpu_manager_participant(manager_generated_team('academy:' . $id, $name, $base, $shirt, $trim, 'Premier League 2'), 1);
  }, $clubs);
}

function manager_lower_nonleague_cup_participants(): array {
  $clubs = [
    ['maidstone-united', 'MAIDSTONE UNITED', 48, '#ffcc00', '#111111'],
    ['torquay-united', 'TORQUAY UNITED', 47, '#f7d117', '#2257a7'],
    ['truro-city', 'TRURO CITY', 45, '#ffffff', '#111111'],
    ['worthing-step2', 'WORTHING', 46, '#e71939', '#ffffff'],
    ['scarborough-athletic', 'SCARBOROUGH ATHLETIC', 45, '#d71920', '#ffffff'],
    ['chester', 'CHESTER', 46, '#0057b8', '#ffffff'],
    ['brackley-town', 'BRACKLEY TOWN', 47, '#ff0000', '#ffffff'],
    ['kidderminster-step2', 'KIDDERMINSTER HARRIERS', 48, '#e60012', '#ffffff'],
    ['weymouth', 'WEYMOUTH', 43, '#7a003c', '#95c5e8'],
    ['billericay-town', 'BILLERICAY TOWN', 42, '#0033a0', '#ffffff'],
    ['haringey-borough', 'HARINGEY BOROUGH', 40, '#ffd100', '#00843d'],
    ['dulwich-hamlet', 'DULWICH HAMLET', 44, '#ec008c', '#00205b'],
  ];
  return array_map(function ($club) {
    [$id, $name, $base, $shirt, $trim] = $club;
    return cup_cpu_manager_participant(manager_generated_team('nonleague:' . $id, $name, $base, $shirt, $trim, 'National League System Step 2-4'), 5);
  }, $clubs);
}

function manager_euro_cup_participants(int $target, int $baseRating = 82): array {
  $clubs = [
    ['real-madrid', 'REAL MADRID', 94, '#ffffff', '#c8a64b'],
    ['barcelona', 'BARCELONA', 91, '#004d98', '#a50044'],
    ['bayern-munich', 'BAYERN MUNICH', 92, '#dc052d', '#ffffff'],
    ['paris-saint-germain', 'PARIS SAINT-GERMAIN', 90, '#004170', '#da291c'],
    ['inter', 'INTER', 89, '#0057b8', '#111111'],
    ['ac-milan', 'AC MILAN', 87, '#fb090b', '#111111'],
    ['juventus', 'JUVENTUS', 86, '#ffffff', '#111111'],
    ['atletico-madrid', 'ATLETICO MADRID', 86, '#cb3524', '#ffffff'],
    ['borussia-dortmund', 'BORUSSIA DORTMUND', 86, '#fde100', '#111111'],
    ['bayer-leverkusen', 'BAYER LEVERKUSEN', 85, '#e32221', '#111111'],
    ['benfica', 'BENFICA', 84, '#e83030', '#ffffff'],
    ['porto', 'PORTO', 83, '#0057b8', '#ffffff'],
    ['ajax', 'AJAX', 82, '#ffffff', '#d71920'],
    ['psv', 'PSV', 82, '#d71920', '#ffffff'],
    ['feyenoord', 'FEYENOORD', 81, '#d71920', '#111111'],
    ['sporting-cp', 'SPORTING CP', 82, '#00843d', '#ffffff'],
    ['napoli', 'NAPOLI', 85, '#12a0d7', '#ffffff'],
    ['roma', 'ROMA', 82, '#8e1f2f', '#f0bc42'],
    ['marseille', 'MARSEILLE', 81, '#ffffff', '#2faee0'],
    ['lyon', 'LYON', 79, '#ffffff', '#0055a4'],
    ['monaco', 'MONACO', 80, '#ffffff', '#d71920'],
    ['celtic', 'CELTIC', 78, '#00843d', '#ffffff'],
    ['rangers', 'RANGERS', 77, '#0033a0', '#ffffff'],
    ['galatasaray', 'GALATASARAY', 80, '#a90432', '#fdb913'],
    ['fenerbahce', 'FENERBAHCE', 79, '#002f6c', '#ffed00'],
    ['shakhtar-donetsk', 'SHAKHTAR DONETSK', 78, '#f58220', '#111111'],
    ['dinamo-zagreb', 'DINAMO ZAGREB', 76, '#005bac', '#ffffff'],
    ['anderlecht', 'ANDERLECHT', 75, '#5f259f', '#ffffff'],
    ['club-brugge', 'CLUB BRUGGE', 78, '#0057b8', '#111111'],
    ['salzburg', 'SALZBURG', 78, '#d71920', '#ffffff'],
    ['sparta-prague', 'SPARTA PRAGUE', 75, '#7a1f2b', '#f4d03f'],
    ['young-boys', 'YOUNG BOYS', 74, '#f7d117', '#111111'],
    ['olympiacos', 'OLYMPIACOS', 77, '#d71920', '#ffffff'],
    ['real-sociedad', 'REAL SOCIEDAD', 81, '#0057b8', '#ffffff'],
    ['villarreal', 'VILLARREAL', 80, '#fff200', '#0057b8'],
    ['sevilla', 'SEVILLA', 79, '#ffffff', '#d71920'],
    ['lazio', 'LAZIO', 80, '#87ceeb', '#ffffff'],
    ['atalanta', 'ATALANTA', 84, '#0057b8', '#111111'],
  ];
  $clubs = array_slice($clubs, 0, $target);
  return array_map(function ($club) use ($baseRating) {
    [$id, $name, $base, $shirt, $trim] = $club;
    return cup_cpu_manager_participant(manager_generated_team('euro:' . $id, $name, max($baseRating, (int)$base), $shirt, $trim, 'Europe'), 1);
  }, $clubs);
}

function manager_seeded_ids(array $ids, string $seed): array {
  $ids = array_values(array_unique(array_map('strval', $ids)));
  usort($ids, fn($a, $b) => crc32($seed . '|' . $a) <=> crc32($seed . '|' . $b));
  return $ids;
}

function manager_cup_round_label(string $cupName, int $roundNo, int $teamCount, string $stage = 'knockout', string $label = ''): string {
  if ($label !== '') return $cupName . ' ' . $label;
  if ($stage === 'group') return $cupName . ' GROUP ' . $roundNo;
  if ($stage === 'league') return $cupName . ' LEAGUE ' . $roundNo;
  if ($teamCount <= 2) return $cupName . ' FINAL';
  if ($teamCount <= 4) return $cupName . ' SF';
  if ($teamCount <= 8) return $cupName . ' QF';
  if ($teamCount <= 16) return $cupName . ' R16';
  if ($teamCount <= 32) return $cupName . ' R32';
  return $cupName . ' R' . $roundNo;
}

function manager_cup_round(array $ids, int $roundNo, string $seed, array $cup, string $label = '', array $forcedByes = []): array {
  $cupKey = (string)($cup['key'] ?? 'cup');
  $cupName = (string)($cup['name'] ?? 'Cup');
  $ids = manager_seeded_ids($ids, $seed . '|' . $cupKey . '|cup|' . $roundNo);
  $forcedByes = array_values(array_unique(array_map('strval', $forcedByes)));
  $count = count($ids);
  $target = 1;
  while ($target * 2 < ($count + count($forcedByes))) $target *= 2;
  $fixturesNeeded = max(1, $count - $target + count($forcedByes));
  $byeCount = max(0, $target - $fixturesNeeded - count($forcedByes));
  if ($count + count($forcedByes) <= 2) {
    $fixturesNeeded = 1;
    $byeCount = 0;
  }
  $byes = array_values(array_unique(array_merge($forcedByes, array_slice($ids, 0, $byeCount))));
  $play = array_slice($ids, $byeCount);
  $fixtures = [];
  for ($i = 0; $i + 1 < count($play) && count($fixtures) < $fixturesNeeded; $i += 2) {
    $fixtures[] = [
      'home' => $play[$i],
      'away' => $play[$i + 1],
      'status' => 'pending',
      'score' => null,
      'phase' => 'cup',
      'cupKey' => $cupKey,
      'cupName' => $cupName,
      'cupStage' => 'knockout',
      'cupRoundNo' => $roundNo,
    ];
  }
  return [
    'round' => manager_cup_round_label($cupName, $roundNo, $count + count($forcedByes), 'knockout', $label),
    'phase' => 'cup',
    'cupKey' => $cupKey,
    'cupName' => $cupName,
    'cupStage' => 'knockout',
    'cupRoundNo' => $roundNo,
    'cupByes' => $byes,
    'fixtures' => $fixtures,
  ];
}

function manager_insert_round_after(array &$rounds, array $round, int $afterIndex): void {
  $afterIndex = clamp_int($afterIndex, -1, max(-1, count($rounds) - 1));
  array_splice($rounds, $afterIndex + 1, 0, [$round]);
}

function manager_cup_table(array $ids): array {
  $table = [];
  foreach ($ids as $id) {
    $id = (string)$id;
    $table[$id] = ['user' => $id, 'played' => 0, 'won' => 0, 'drawn' => 0, 'lost' => 0, 'gf' => 0, 'ga' => 0, 'gd' => 0, 'points' => 0];
  }
  return $table;
}

function manager_apply_cup_table_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])) continue;
    $table[$user]['played']++;
    $table[$user]['gf'] += $gf;
    $table[$user]['ga'] += $ga;
    $table[$user]['gd'] = $table[$user]['gf'] - $table[$user]['ga'];
    if ($gf > $ga) {
      $table[$user]['won']++;
      $table[$user]['points'] += 3;
    } elseif ($gf === $ga) {
      $table[$user]['drawn']++;
      $table[$user]['points'] += 1;
    } else {
      $table[$user]['lost']++;
    }
  }
}

function manager_rank_cup_table(array $table): array {
  $rows = array_values($table);
  usort($rows, fn($a, $b) => [$b['points'], $b['gd'], $b['gf'], $a['user']] <=> [$a['points'], $a['gd'], $a['gf'], $b['user']]);
  return $rows;
}

function manager_group_pair_rounds(array $ids): array {
  $ids = array_values(array_map('strval', $ids));
  if (count($ids) % 2 === 1) $ids[] = '__bye__';
  $n = count($ids);
  $rounds = [];
  $list = $ids;
  for ($r = 0; $r < $n - 1; $r++) {
    $fixtures = [];
    for ($i = 0; $i < $n / 2; $i++) {
      $home = $list[$i];
      $away = $list[$n - 1 - $i];
      if ($home !== '__bye__' && $away !== '__bye__') {
        if ($r % 2 === 1) [$home, $away] = [$away, $home];
        $fixtures[] = [$home, $away];
      }
    }
    $rounds[] = $fixtures;
    $fixed = $list[0];
    $rest = array_slice($list, 1);
    array_unshift($rest, array_pop($rest));
    $list = array_merge([$fixed], $rest);
  }
  return $rounds;
}

function manager_cup_group_stage_rounds(array $groups, array $cup): array {
  $cupKey = (string)$cup['key'];
  $cupName = (string)$cup['name'];
  $byRound = [];
  foreach ($groups as $groupId => $ids) {
    $pairRounds = (($cup['groupMode'] ?? '') === 'cross-academy') ? manager_cross_academy_group_rounds($ids, (int)($cup['groupRounds'] ?? 4)) : manager_group_pair_rounds($ids);
    foreach ($pairRounds as $roundNo => $pairs) {
      foreach ($pairs as $pair) {
        [$home, $away] = $pair;
        $byRound[$roundNo][] = [
          'home' => $home,
          'away' => $away,
          'status' => 'pending',
          'score' => null,
          'phase' => 'cup',
          'cupKey' => $cupKey,
          'cupName' => $cupName,
          'cupStage' => 'group',
          'cupGroup' => (string)$groupId,
          'cupRoundNo' => $roundNo + 1,
        ];
      }
    }
  }
  $rounds = [];
  foreach ($byRound as $i => $fixtures) {
    $roundNo = (int)$i + 1;
    $rounds[] = [
      'round' => manager_cup_round_label($cupName, $roundNo, 0, 'group'),
      'phase' => 'cup',
      'cupKey' => $cupKey,
      'cupName' => $cupName,
      'cupStage' => 'group',
      'cupRoundNo' => $roundNo,
      'fixtures' => $fixtures,
    ];
  }
  return $rounds;
}

function manager_cross_academy_group_rounds(array $ids, int $roundLimit): array {
  $academy = array_values(array_filter(array_map('strval', $ids), fn($id) => str_contains($id, 'academy:')));
  $senior = array_values(array_filter(array_map('strval', $ids), fn($id) => !str_contains($id, 'academy:')));
  if (!$academy || !$senior) return manager_group_pair_rounds($ids);
  $rounds = [];
  $roundLimit = max(1, min($roundLimit, count($academy)));
  for ($r = 0; $r < $roundLimit; $r++) {
    $fixtures = [];
    foreach ($senior as $i => $home) {
      $away = $academy[($i + $r) % count($academy)];
      $fixtures[] = [$home, $away];
    }
    $rounds[] = $fixtures;
  }
  return $rounds;
}

function manager_cup_league_phase_rounds(array $ids, array $cup, int $roundCount): array {
  $cupKey = (string)$cup['key'];
  $cupName = (string)$cup['name'];
  $ids = array_values(array_unique(array_map('strval', $ids)));
  $rounds = [];
  for ($roundNo = 1; $roundNo <= $roundCount; $roundNo++) {
    $list = manager_seeded_ids($ids, (string)($cup['seed'] ?? $cupKey) . '|league|' . $roundNo);
    if (count($list) % 2 === 1) $list[] = '__bye__';
    $fixtures = [];
    for ($i = 0; $i < count($list); $i += 2) {
      if (($list[$i] ?? '__bye__') === '__bye__' || ($list[$i + 1] ?? '__bye__') === '__bye__') continue;
      $fixtures[] = [
        'home' => $list[$i],
        'away' => $list[$i + 1],
        'status' => 'pending',
        'score' => null,
        'phase' => 'cup',
        'cupKey' => $cupKey,
        'cupName' => $cupName,
        'cupStage' => 'league',
        'cupGroup' => 'LEAGUE',
        'cupRoundNo' => $roundNo,
      ];
    }
    $rounds[] = [
      'round' => manager_cup_round_label($cupName, $roundNo, count($ids), 'league'),
      'phase' => 'cup',
      'cupKey' => $cupKey,
      'cupName' => $cupName,
      'cupStage' => 'league',
      'cupRoundNo' => $roundNo,
      'fixtures' => $fixtures,
    ];
  }
  return $rounds;
}

function manager_cup_definitions(): array {
  return [
    ['key' => 'community-shield', 'name' => 'Community Shield', 'format' => 'final', 'slot' => -1, 'rules' => 'Premier League champion vs FA Cup winner; if same club, Premier League runner-up is used.'],
    ['key' => 'fa-cup', 'name' => 'FA Cup', 'format' => 'knockout', 'levels' => [1, 2, 3, 4, 5], 'slot' => 5, 'rules' => 'Senior knockout cup. Winners stay in; losers are out. Lower qualifying rounds are abstracted.'],
    ['key' => 'efl-cup', 'name' => 'EFL Cup', 'format' => 'knockout', 'levels' => [1, 2, 3, 4], 'slot' => 2, 'rules' => 'Premier League and EFL clubs. Winners stay in; losers are out. European entry exemptions are abstracted into seeded byes.'],
    ['key' => 'efl-trophy', 'name' => 'EFL Trophy', 'format' => 'groups', 'levels' => [3, 4], 'slot' => 3, 'groupSize' => 4, 'advancePerGroup' => 2, 'extra' => 'academy', 'groupPenaltyBonus' => true, 'rules' => 'League One, League Two and invited academy teams. Top two per group advance to knockout rounds; drawn group games have a penalty bonus point.'],
    ['key' => 'fa-trophy', 'name' => 'FA Trophy', 'format' => 'knockout', 'levels' => [5], 'slot' => 8, 'extra' => 'nonleague', 'rules' => 'National League System Step 1-4 cup. Winners stay in; losers are out.'],
    ['key' => 'national-league-cup', 'name' => 'National League Cup', 'format' => 'groups', 'levels' => [5], 'slot' => 4, 'maxTeams' => 16, 'groupSize' => 8, 'groupRounds' => 4, 'groupMode' => 'cross-academy', 'advancePerGroup' => 2, 'extra' => 'academy', 'groupPenaltyBonus' => true, 'rules' => 'National League clubs plus PL2 academy teams. Groups contain senior and academy clubs; top two per group advance; drawn group games have a penalty bonus point.'],
    ['key' => 'uefa-champions-league', 'name' => 'UEFA Champions League', 'format' => 'league-phase', 'slot' => 6, 'targetTeams' => 36, 'leagueRounds' => 8, 'europe' => 'ucl', 'rules' => 'Premier League top Champions League qualifiers enter a 36-team league phase; top 24 stay in Europe, top 8 skip the knockout playoff.'],
    ['key' => 'uefa-europa-league', 'name' => 'UEFA Europa League', 'format' => 'league-phase', 'slot' => 7, 'targetTeams' => 36, 'leagueRounds' => 8, 'europe' => 'uel', 'rules' => 'Premier League and domestic cup Europa qualifiers enter a 36-team league phase; top 24 stay in Europe, top 8 skip the knockout playoff.'],
    ['key' => 'uefa-conference-league', 'name' => 'UEFA Conference League', 'format' => 'league-phase', 'slot' => 8, 'targetTeams' => 36, 'leagueRounds' => 6, 'europe' => 'uecl', 'rules' => 'League Cup or league Conference qualifiers enter the league phase; top 24 stay in Europe, top 8 skip the knockout playoff.'],
    ['key' => 'uefa-super-cup', 'name' => 'UEFA Super Cup', 'format' => 'final', 'slot' => 1, 'rules' => 'Champions League winner vs Europa League winner from the previous season.'],
  ];
}

function manager_participant_ids(array $participants): array {
  return array_values(array_map(fn($p) => (string)($p['user'] ?? ''), $participants));
}

function manager_cup_has_human(array $participants, array $selected): bool {
  $ids = array_flip(manager_participant_ids($participants));
  foreach ($selected as $name) {
    if (isset($ids[(string)$name])) return true;
  }
  return false;
}

function manager_participant_from_previous_or_current(array &$state, array $selected, ?array $previousSeason, string $id, int $fallbackLevel = 1): ?array {
  foreach ($selected as $name) {
    if ((string)$name !== $id) continue;
    return manager_human_cup_participant($state, (string)$name);
  }
  if ($previousSeason !== null) {
    $participant = manager_participant($previousSeason, $id);
    if ($participant !== null) return $participant;
  }
  for ($level = 1; $level <= 5; $level++) {
    foreach (manager_division_teams($level) as $team) {
      if (manager_cpu_id((string)$team['id']) === $id || (string)$team['id'] === $id) return cup_cpu_manager_participant($team, $level);
    }
  }
  return null;
}

function manager_static_premier_participants(): array {
  $teams = manager_division_teams(1);
  $participants = array_map(fn($team) => cup_cpu_manager_participant($team, 1), $teams);
  usort($participants, fn($a, $b) => ((int)($b['rating'] ?? 0)) <=> ((int)($a['rating'] ?? 0)));
  return $participants;
}

function manager_previous_premier_rows(?array $previousSeason): array {
  if ($previousSeason === null || (int)($previousSeason['divisionLevel'] ?? 0) !== 1) return [];
  return manager_ranked_rows($previousSeason);
}

function manager_community_shield_participants(array &$state, array $selected, ?array $previousSeason): array {
  $premier = manager_static_premier_participants();
  $champion = $premier[0] ?? null;
  $runnerUp = $premier[1] ?? null;
  $rows = manager_previous_premier_rows($previousSeason);
  if (!empty($rows[0])) $champion = manager_participant_from_previous_or_current($state, $selected, $previousSeason, (string)$rows[0]['user'], 1) ?? $champion;
  if (!empty($rows[1])) $runnerUp = manager_participant_from_previous_or_current($state, $selected, $previousSeason, (string)$rows[1]['user'], 1) ?? $runnerUp;
  $faWinnerId = (string)($previousSeason['cups']['fa-cup']['winner'] ?? $previousSeason['cup']['winner'] ?? '');
  $faWinner = $faWinnerId !== '' ? manager_participant_from_previous_or_current($state, $selected, $previousSeason, $faWinnerId, 1) : null;
  $second = $faWinner;
  if ($champion !== null && $second !== null && (string)$champion['user'] === (string)$second['user']) $second = $runnerUp;
  $participants = array_values(array_filter([$champion, $second]));
  return manager_cup_has_human($participants, $selected) ? $participants : [];
}

function manager_uefa_super_cup_participants(array &$state, array $selected, ?array $previousSeason): array {
  if ($previousSeason === null) return [];
  $ucl = (string)($previousSeason['cups']['uefa-champions-league']['winner'] ?? '');
  $uel = (string)($previousSeason['cups']['uefa-europa-league']['winner'] ?? '');
  if ($ucl === '' || $uel === '' || $ucl === $uel) return [];
  $participants = array_values(array_filter([
    manager_participant_from_previous_or_current($state, $selected, $previousSeason, $ucl, 1),
    manager_participant_from_previous_or_current($state, $selected, $previousSeason, $uel, 1),
  ]));
  return manager_cup_has_human($participants, $selected) ? $participants : [];
}

function manager_european_english_participants(array &$state, array $selected, ?array $previousSeason, string $type): array {
  $rows = manager_previous_premier_rows($previousSeason);
  $premier = manager_static_premier_participants();
  $ids = [];
  if ($rows) {
    $rankIds = array_map(fn($row) => (string)($row['user'] ?? ''), $rows);
    if ($type === 'ucl') $ids = array_slice($rankIds, 0, 4);
    elseif ($type === 'uel') $ids = array_values(array_filter([($rankIds[4] ?? ''), (string)($previousSeason['cups']['fa-cup']['winner'] ?? '')]));
    else $ids = array_values(array_filter([(string)($previousSeason['cups']['efl-cup']['winner'] ?? ''), ($rankIds[5] ?? '')]));
  } else {
    $staticIds = manager_participant_ids($premier);
    if ($type === 'ucl') $ids = array_slice($staticIds, 0, 4);
    elseif ($type === 'uel') $ids = array_slice($staticIds, 4, 2);
    else $ids = array_slice($staticIds, 6, 1);
  }
  $participants = [];
  foreach (array_values(array_unique(array_filter($ids))) as $id) {
    $participant = manager_participant_from_previous_or_current($state, $selected, $previousSeason, $id, 1);
    if ($participant !== null) $participants[] = $participant;
  }
  return manager_cup_has_human($participants, $selected) ? $participants : [];
}

function manager_cup_participants_for_definition(array &$state, array $selected, array $cup, ?array $previousSeason): array {
  $format = (string)($cup['format'] ?? 'knockout');
  if ($format === 'final' && ($cup['key'] ?? '') === 'community-shield') return manager_community_shield_participants($state, $selected, $previousSeason);
  if ($format === 'final' && ($cup['key'] ?? '') === 'uefa-super-cup') return manager_uefa_super_cup_participants($state, $selected, $previousSeason);
  if (!empty($cup['europe'])) {
    $english = manager_european_english_participants($state, $selected, $previousSeason, (string)$cup['europe']);
    if (!$english) return [];
    $target = (int)($cup['targetTeams'] ?? 36);
    return array_values(array_merge($english, manager_euro_cup_participants(max(0, $target - count($english)), (string)$cup['europe'] === 'ucl' ? 82 : 74)));
  }
  $participants = manager_cup_participants_for_levels($state, $selected, $cup['levels'] ?? [], (int)($cup['maxTeams'] ?? 0));
  if (!manager_cup_has_human($participants, $selected)) return [];
  if (($cup['extra'] ?? '') === 'academy') $participants = array_values(array_merge($participants, manager_academy_cup_participants()));
  if (($cup['extra'] ?? '') === 'nonleague') $participants = array_values(array_merge($participants, manager_lower_nonleague_cup_participants()));
  return $participants;
}

function manager_sync_primary_cup(array &$season): void {
  $cups = $season['cups'] ?? [];
  $season['cup'] = is_array($cups) && $cups ? array_values($cups)[0] : null;
}

function manager_add_cup_participants(array &$season, array $participants): void {
  $map = [];
  foreach (($season['cupParticipants'] ?? []) as $participant) {
    $map[(string)($participant['user'] ?? '')] = $participant;
  }
  foreach ($participants as $participant) {
    $id = (string)($participant['user'] ?? '');
    if ($id !== '') $map[$id] = $participant;
  }
  $season['cupParticipants'] = array_values($map);
}

function manager_start_cup_competition(array &$season, array $cup, array $participants): void {
  $ids = manager_participant_ids($participants);
  if (count($ids) < 2) return;
  $key = (string)$cup['key'];
  $name = (string)$cup['name'];
  if (!isset($season['cups']) || !is_array($season['cups'])) $season['cups'] = [];
  $season['cups'][$key] = [
    'key' => $key,
    'name' => $name,
    'format' => (string)($cup['format'] ?? 'knockout'),
    'version' => MANAGER_CUP_VERSION,
    'status' => 'active',
    'roundNo' => 1,
    'participantCount' => count($ids),
    'winner' => null,
    'runnerUp' => null,
    'rules' => (string)($cup['rules'] ?? ''),
  ];
  if (!empty($cup['groupPenaltyBonus'])) $season['cups'][$key]['groupPenaltyBonus'] = true;
  $seed = (string)($season['id'] ?? 'cup') . '|' . $key;
  $slot = (int)($cup['slot'] ?? 5);
  $format = (string)($cup['format'] ?? 'knockout');
  manager_add_cup_participants($season, $participants);
  if ($format === 'final') {
    $round = manager_cup_round($ids, 1, $seed, $cup, 'FINAL');
    manager_insert_round_after($season['rounds'], $round, $slot);
  } elseif ($format === 'groups') {
    $groupSize = max(2, (int)($cup['groupSize'] ?? 4));
    $ids = manager_seeded_ids($ids, $seed . '|groups');
    $groups = [];
    $letter = ord('A');
    if (($cup['groupMode'] ?? '') === 'cross-academy') {
      $academy = manager_seeded_ids(array_values(array_filter($ids, fn($id) => str_contains((string)$id, 'academy:'))), $seed . '|academy');
      $senior = manager_seeded_ids(array_values(array_filter($ids, fn($id) => !str_contains((string)$id, 'academy:'))), $seed . '|senior');
      $half = max(1, (int)floor($groupSize / 2));
      $chunks = [];
      $groupCount = max(1, (int)ceil(max(count($senior), count($academy)) / $half));
      for ($i = 0; $i < $groupCount; $i++) {
        $chunks[] = array_values(array_merge(array_slice($senior, $i * $half, $half), array_slice($academy, $i * $half, $half)));
      }
    } else {
      $chunks = array_chunk($ids, $groupSize);
    }
    foreach ($chunks as $chunk) {
      if (count($chunk) < 2) continue;
      $groupId = chr($letter++);
      $groups[$groupId] = $chunk;
      $season['cups'][$key]['groups'][$groupId] = ['teams' => $chunk, 'table' => manager_cup_table($chunk)];
    }
    $season['cups'][$key]['advancePerGroup'] = max(1, (int)($cup['advancePerGroup'] ?? 2));
    foreach (manager_cup_group_stage_rounds($groups, $cup) as $i => $round) {
      manager_insert_round_after($season['rounds'], $round, $slot + ($i * 5));
    }
  } elseif ($format === 'league-phase') {
    $season['cups'][$key]['leagueTable'] = manager_cup_table($ids);
    $season['cups'][$key]['leagueRounds'] = (int)($cup['leagueRounds'] ?? 8);
    foreach (manager_cup_league_phase_rounds($ids, $cup + ['seed' => $seed], (int)$season['cups'][$key]['leagueRounds']) as $i => $round) {
      manager_insert_round_after($season['rounds'], $round, $slot + ($i * 4));
    }
  } else {
    $round = manager_cup_round($ids, 1, $seed, $cup);
    manager_insert_round_after($season['rounds'], $round, $slot);
  }
  manager_sync_primary_cup($season);
}

function manager_start_all_cups(array &$state, array &$season, array $selected, ?array $previousSeason = null): void {
  $season['cups'] = [];
  $season['cupParticipants'] = [];
  foreach (manager_cup_definitions() as $cup) {
    $participants = manager_cup_participants_for_definition($state, $selected, $cup, $previousSeason);
    if (count($participants) < 2) continue;
    manager_start_cup_competition($season, $cup, $participants);
  }
  manager_sync_primary_cup($season);
}

function manager_start_english_cup(array &$season, array $cupParticipants): void {
  manager_start_cup_competition($season, ['key' => 'fa-cup', 'name' => 'FA Cup', 'format' => 'knockout', 'slot' => 5, 'rules' => 'Senior knockout cup. Winners stay in; losers are out.'], $cupParticipants);
}

function manager_cup_round_finished(array $round): bool {
  foreach (($round['fixtures'] ?? []) as $fixture) {
    if (($fixture['status'] ?? '') !== 'played') return false;
  }
  return true;
}

function manager_cup_has_unplayed_stage(array $season, string $cupKey, string $stage): bool {
  foreach (($season['rounds'] ?? []) as $round) {
    if (($round['phase'] ?? '') !== 'cup' || (string)($round['cupKey'] ?? '') !== $cupKey || (string)($round['cupStage'] ?? '') !== $stage) continue;
    foreach (($round['fixtures'] ?? []) as $fixture) {
      if (in_array(($fixture['status'] ?? ''), ['pending', 'watching'], true)) return true;
    }
  }
  return false;
}

function manager_cup_apply_result(array &$season, string $cupKey, string $stage, string $group, string $home, string $away, int $hg, int $ag, string $bonusWinner = ''): void {
  if (empty($season['cups'][$cupKey])) return;
  if ($stage === 'group') {
    if (!isset($season['cups'][$cupKey]['groups'][$group]['table'])) return;
    manager_apply_cup_table_result($season['cups'][$cupKey]['groups'][$group]['table'], $home, $away, $hg, $ag);
    if ($hg === $ag && $bonusWinner !== '' && !empty($season['cups'][$cupKey]['groupPenaltyBonus']) && isset($season['cups'][$cupKey]['groups'][$group]['table'][$bonusWinner])) {
      $season['cups'][$cupKey]['groups'][$group]['table'][$bonusWinner]['points']++;
    }
  } elseif ($stage === 'league') {
    if (!isset($season['cups'][$cupKey]['leagueTable'])) return;
    manager_apply_cup_table_result($season['cups'][$cupKey]['leagueTable'], $home, $away, $hg, $ag);
  }
}

function manager_cup_add_next_knockout(array &$season, string $cupKey, array $ids, int $nextNo, string $label = '', array $byes = []): void {
  if (empty($season['cups'][$cupKey])) return;
  $cup = $season['cups'][$cupKey];
  $round = manager_cup_round($ids, $nextNo, (string)($season['id'] ?? 'cup') . '|' . $cupKey, $cup, $label, $byes);
  $season['cups'][$cupKey]['roundNo'] = $nextNo;
  $season['cups'][$cupKey]['status'] = 'knockout';
  manager_insert_round_after($season['rounds'], $round, min(count($season['rounds']) - 1, manager_round_seq($season)));
  manager_sync_primary_cup($season);
}

function manager_cup_advance_from_groups(array &$season, string $cupKey): void {
  $cup =& $season['cups'][$cupKey];
  $advance = max(1, (int)($cup['advancePerGroup'] ?? 2));
  $qualified = [];
  foreach (($cup['groups'] ?? []) as $group) {
    foreach (array_slice(manager_rank_cup_table($group['table'] ?? []), 0, $advance) as $row) {
      $qualified[] = (string)$row['user'];
    }
  }
  unset($cup);
  manager_cup_add_next_knockout($season, $cupKey, $qualified, 1, '');
}

function manager_cup_advance_from_league(array &$season, string $cupKey): void {
  $rows = manager_rank_cup_table($season['cups'][$cupKey]['leagueTable'] ?? []);
  $top8 = array_map(fn($row) => (string)$row['user'], array_slice($rows, 0, 8));
  $playoff = array_map(fn($row) => (string)$row['user'], array_slice($rows, 8, 16));
  manager_cup_add_next_knockout($season, $cupKey, $playoff, 1, 'KO PLAYOFF', $top8);
}

function advance_manager_cup(array &$season, int $roundIndex): void {
  if (empty($season['rounds'][$roundIndex]) || (($season['rounds'][$roundIndex]['phase'] ?? '') !== 'cup')) return;
  $round = $season['rounds'][$roundIndex];
  if (!manager_cup_round_finished($round)) return;
  $cupKey = (string)($round['cupKey'] ?? 'fa-cup');
  $stage = (string)($round['cupStage'] ?? 'knockout');
  if (empty($season['cups'][$cupKey])) return;
  if ($stage === 'group') {
    if (!manager_cup_has_unplayed_stage($season, $cupKey, 'group')) manager_cup_advance_from_groups($season, $cupKey);
    manager_sync_primary_cup($season);
    return;
  }
  if ($stage === 'league') {
    if (!manager_cup_has_unplayed_stage($season, $cupKey, 'league')) manager_cup_advance_from_league($season, $cupKey);
    manager_sync_primary_cup($season);
    return;
  }
  $alive = array_values(array_map('strval', $round['cupByes'] ?? []));
  foreach (($round['fixtures'] ?? []) as $fixture) {
    if (!empty($fixture['winner'])) $alive[] = (string)$fixture['winner'];
  }
  $alive = array_values(array_unique(array_filter($alive)));
  if (count($alive) <= 1) {
    $winner = $alive[0] ?? '';
    $runnerUp = '';
    if (count($round['fixtures'] ?? []) === 1) {
      $fixture = $round['fixtures'][0];
      $runnerUp = (string)(($fixture['winner'] ?? '') === ($fixture['home'] ?? '') ? ($fixture['away'] ?? '') : ($fixture['home'] ?? ''));
    }
    $season['cups'][$cupKey]['status'] = 'finished';
    $season['cups'][$cupKey]['winner'] = $winner;
    $season['cups'][$cupKey]['runnerUp'] = $runnerUp;
    manager_sync_primary_cup($season);
    return;
  }
  $nextNo = (int)($round['cupRoundNo'] ?? 1) + 1;
  $season['cups'][$cupKey]['roundNo'] = $nextNo;
  $next = manager_cup_round($alive, $nextNo, (string)($season['id'] ?? 'cup') . '|' . $cupKey, $season['cups'][$cupKey]);
  manager_insert_round_after($season['rounds'], $next, min(count($season['rounds']) - 1, $roundIndex + 5));
  manager_sync_primary_cup($season);
}

function team_trophy_key(array $team): string {
  return strtolower((string)($team['sourceId'] ?? $team['id'] ?? $team['name'] ?? 'team'));
}

function team_trophy_visual(array $trophy): string {
  $kind = strtolower((string)($trophy['kind'] ?? ''));
  $visual = strtolower((string)($trophy['visual'] ?? ''));
  $source = trim($visual . ' ' . $kind);
  if (str_contains($source, 'runner') || str_contains($source, 'silver')) return 'medal_silver';
  if (str_contains($source, 'top_scorer') || str_contains($source, 'golden')) return 'medal';
  if (str_contains($source, 'promotion') || str_contains($source, 'medal')) return 'medal';
  return 'cup';
}

function add_team_trophy(array &$state, array $team, array $trophy): void {
  $key = team_trophy_key($team);
  if ($key === '') return;
  if (!isset($state['teamTrophies']) || !is_array($state['teamTrophies'])) $state['teamTrophies'] = [];
  if (!isset($state['teamTrophies'][$key]) || !is_array($state['teamTrophies'][$key])) $state['teamTrophies'][$key] = [];
  $trophy['visual'] = team_trophy_visual($trophy);
  $trophy['team'] = ['id' => $team['id'] ?? '', 'sourceId' => $team['sourceId'] ?? ($team['id'] ?? ''), 'name' => $team['name'] ?? ''];
  $fingerprint = implode('|', [(string)($trophy['seasonId'] ?? ''), (string)($trophy['kind'] ?? ''), (string)($trophy['title'] ?? ''), (string)($trophy['seasonYear'] ?? '')]);
  foreach ($state['teamTrophies'][$key] as $existing) {
    $existingFingerprint = implode('|', [(string)($existing['seasonId'] ?? ''), (string)($existing['kind'] ?? ''), (string)($existing['title'] ?? ''), (string)($existing['seasonYear'] ?? '')]);
    if ($existingFingerprint === $fingerprint) return;
  }
  $state['teamTrophies'][$key][] = $trophy;
}

function manager_trophy_season_ids(array $state, array $season): array {
  $ids = [];
  $current = $season;
  for ($guard = 0; $guard < 20 && is_array($current); $guard++) {
    $id = (string)($current['id'] ?? '');
    if ($id !== '') $ids[$id] = true;
    $previousId = (string)($current['continuedFrom'] ?? '');
    if ($previousId === '' || isset($ids[$previousId])) break;
    if (!isset($state['managerSeasons'][$previousId]) || !is_array($state['managerSeasons'][$previousId])) {
      $ids[$previousId] = true;
      break;
    }
    $current = $state['managerSeasons'][$previousId];
  }
  return $ids;
}

function public_team_trophies(array $state, array $team, ?array $seasonIds = null): array {
  $items = array_values($state['teamTrophies'][team_trophy_key($team)] ?? []);
  $items = array_values(array_filter($items, 'is_array'));
  if ($seasonIds !== null) {
    $items = array_values(array_filter($items, function ($item) use ($seasonIds) {
      $seasonId = (string)($item['seasonId'] ?? '');
      return $seasonId !== '' && isset($seasonIds[$seasonId]);
    }));
  }
  $items = array_map(function ($item) {
    $item['visual'] = team_trophy_visual($item);
    return $item;
  }, $items);
  usort($items, fn($a, $b) => [(int)($b['seasonYear'] ?? 0), (string)($b['title'] ?? '')] <=> [(int)($a['seasonYear'] ?? 0), (string)($a['title'] ?? '')]);
  return $items;
}

function manager_human_player_report(array $season): array {
  $humans = array_flip(array_map('strval', $season['humanManagers'] ?? []));
  if (!$humans) return ['workhorses' => [], 'best' => []];
  $players = [];
  foreach (($season['matchReports'] ?? []) as $report) {
    if (!is_array($report)) continue;
    $sideUsers = ['home' => (string)($report['home'] ?? ''), 'away' => (string)($report['away'] ?? '')];
    foreach (($report['players'] ?? []) as $player) {
      if (!is_array($player)) continue;
      $side = (string)($player['side'] ?? '');
      $user = $sideUsers[$side] ?? '';
      if ($user === '' || !isset($humans[$user])) continue;
      $name = (string)($player['name'] ?? 'Player');
      $key = $user . '|' . strtolower($name);
      if (!isset($players[$key])) {
        $players[$key] = [
          'user' => $user,
          'name' => $name,
          'teamName' => (string)($player['teamName'] ?? $user),
          'role' => (string)($player['role'] ?? ''),
          'reports' => 0,
          'distanceMeters' => 0,
          'impactTotal' => 0.0,
          'touches' => 0,
          'shots' => 0,
          'tackles' => 0,
          'complaints' => 0,
        ];
      }
      $players[$key]['reports']++;
      $players[$key]['distanceMeters'] += (int)round((float)($player['distanceMeters'] ?? 0));
      $players[$key]['impactTotal'] += (float)($player['impact'] ?? 0);
      $players[$key]['touches'] += (int)($player['touches'] ?? 0);
      $players[$key]['shots'] += (int)($player['shots'] ?? 0);
      $players[$key]['tackles'] += (int)($player['tackles'] ?? 0);
      $players[$key]['complaints'] += (int)($player['complaints'] ?? 0);
    }
  }
  $rows = array_values(array_map(function ($row) {
    $reports = max(1, (int)($row['reports'] ?? 0));
    $row['avgImpact'] = round(((float)($row['impactTotal'] ?? 0)) / $reports, 1);
    $row['impactTotal'] = round((float)($row['impactTotal'] ?? 0), 1);
    return $row;
  }, $players));
  $workhorses = $rows;
  usort($workhorses, fn($a, $b) => [(int)($b['distanceMeters'] ?? 0), (float)($b['impactTotal'] ?? 0)] <=> [(int)($a['distanceMeters'] ?? 0), (float)($a['impactTotal'] ?? 0)]);
  $best = $rows;
  usort($best, fn($a, $b) => [(float)($b['avgImpact'] ?? 0), (float)($b['impactTotal'] ?? 0), (int)($b['reports'] ?? 0)] <=> [(float)($a['avgImpact'] ?? 0), (float)($a['impactTotal'] ?? 0), (int)($a['reports'] ?? 0)]);
  return [
    'workhorses' => array_slice($workhorses, 0, 5),
    'best' => array_slice($best, 0, 5),
  ];
}

function manager_season_report(array $season, array $names): array {
  $level = clamp_int((int)($season['divisionLevel'] ?? 5), 1, 5);
  $toDivision = $level > 1 ? manager_division_name($level - 1) : '';
  $promoted = [];
  $seen = [];
  $directRaw = $season['promotion']['direct'] ?? [];
  $direct = is_array($directRaw) ? array_map('strval', $directRaw) : [(string)$directRaw];
  foreach ($direct as $id) {
    if ($id === '' || isset($seen[$id])) continue;
    $seen[$id] = true;
    $team = $names[$id]['team'] ?? manager_participant($season, $id)['team'] ?? ['name' => strtoupper($id), 'shirt' => '#fff', 'trim' => '#111'];
    $promoted[] = ['user' => $id, 'team' => $team, 'method' => 'direct'];
  }
  $playoff = (string)($season['promotion']['playoff'] ?? '');
  if ($playoff !== '' && !isset($seen[$playoff])) {
    $team = $names[$playoff]['team'] ?? manager_participant($season, $playoff)['team'] ?? ['name' => strtoupper($playoff), 'shirt' => '#fff', 'trim' => '#111'];
    $promoted[] = ['user' => $playoff, 'team' => $team, 'method' => 'playoff'];
  }

  $relegated = manager_relegated_from_above_teams($level, (string)($season['id'] ?? ''), $season);

  return [
    'fromDivision' => manager_division_name($level),
    'promotedToDivision' => $toDivision,
    'relegatedFromDivision' => $level > 1 ? manager_division_name($level - 1) : '',
    'promoted' => $promoted,
    'relegatedFromAbove' => $relegated,
    'cup' => $season['cup'] ?? null,
    'cups' => array_values($season['cups'] ?? []),
    'playerReport' => manager_human_player_report($season),
  ];
}

function manager_participant(array $season, string $id): ?array {
  foreach (($season['participants'] ?? []) as $participant) {
    if (($participant['user'] ?? '') === $id) return $participant;
  }
  foreach (($season['cupParticipants'] ?? []) as $participant) {
    if (($participant['user'] ?? '') === $id) return $participant;
  }
  return null;
}

function manager_cpu_formation_for_team(array $team, int $divisionLevel = 5): string {
  $teamKey = team_source_key($team);
  if ($teamKey === '') $teamKey = (string)($team['id'] ?? $team['name'] ?? 'cpu');
  $formations = ['4-4-2', '4-3-3', '4-2-3-1', '4-5-1', '3-5-2', '5-3-2'];
  $seed = abs((int)crc32($teamKey . '|division:' . clamp_int($divisionLevel, 1, 5) . '|cpu-formation'));
  return $formations[$seed % count($formations)];
}

function normalize_cpu_participant_formation(array &$participant): void {
  if (($participant['managerType'] ?? 'cpu') !== 'cpu') return;
  $team = is_array($participant['team'] ?? null) ? $participant['team'] : [];
  if (!$team || empty($participant['squad']) || !is_array($participant['squad'])) return;
  $level = clamp_int((int)($participant['divisionLevel'] ?? 5), 1, 5);
  $formation = manager_cpu_formation_for_team($team, $level);
  $currentFormation = valid_manager_formation((string)($participant['formation'] ?? ''));
  $needsFormation = (int)($participant['formationVersion'] ?? 0) < CPU_FORMATION_VERSION || $currentFormation !== $formation;
  if ($needsFormation) {
    apply_formation_to_squad($participant['squad'], $formation, [], true);
    $participant['formation'] = $formation;
    $participant['formationVersion'] = CPU_FORMATION_VERSION;
  }
  ensure_squad_stats($participant['squad']);
  $participant['rating'] = squad_skill_rating($participant['squad']);
  $participant['strength'] = squad_skill_points($participant['squad']);
}

function normalize_state_cpu_formations(array &$state): void {
  if (empty($state['managerSeasons']) || !is_array($state['managerSeasons'])) return;
  foreach ($state['managerSeasons'] as &$season) {
    if (!is_array($season)) continue;
    foreach (['participants', 'cupParticipants'] as $listKey) {
      if (empty($season[$listKey]) || !is_array($season[$listKey])) continue;
      foreach ($season[$listKey] as &$participant) {
        if (!is_array($participant) || !is_cpu_manager((string)($participant['user'] ?? ''))) continue;
        normalize_cpu_participant_formation($participant);
      }
      unset($participant);
    }
  }
  unset($season);
}

function cpu_manager_participant(array $team, int $divisionLevel = 5): array {
  $base = (int)($team['baseRating'] ?? team_base_rating((string)$team['id']));
  $squad = make_named_squad('home', $base, team_source_key($team), (string)($team['name'] ?? ''));
  $formation = manager_cpu_formation_for_team($team, $divisionLevel);
  apply_formation_to_squad($squad, $formation, [], true);
  return [
    'user' => manager_cpu_id((string)$team['id']),
    'managerType' => 'cpu',
    'team' => $team,
    'rating' => squad_skill_rating($squad),
    'strength' => squad_skill_points($squad),
    'divisionLevel' => clamp_int($divisionLevel, 1, 5),
    'divisionName' => manager_division_name($divisionLevel),
    'ratingBase' => $base,
    'scaleVersion' => SQUAD_SCALE_VERSION,
    'nameVersion' => SQUAD_NAME_VERSION,
    'idVersion' => SQUAD_ID_VERSION,
    'formation' => $formation,
    'formationVersion' => CPU_FORMATION_VERSION,
    'squad' => $squad,
  ];
}

function scaled_cpu_manager_participant(array $participant): array {
  if (($participant['managerType'] ?? 'cpu') !== 'cpu') return $participant;
  $team = $participant['team'] ?? null;
  if (!is_array($team)) return $participant;
  $level = clamp_int((int)($participant['divisionLevel'] ?? 5), 1, 5);
  $expectedBase = (int)($team['baseRating'] ?? team_base_rating((string)($team['id'] ?? '')));
  $hasSquad = !empty($participant['squad']['starters']) && !empty($participant['squad']['bench']);
  if (
    $hasSquad
    && (int)($participant['scaleVersion'] ?? 0) >= SQUAD_SCALE_VERSION
    && (int)($participant['ratingBase'] ?? 0) === $expectedBase
  ) {
    normalize_squad_player_ids($participant['squad'], team_source_key($team), (string)($team['name'] ?? ''), 'home');
    ensure_squad_stats($participant['squad']);
    if ((int)($participant['nameVersion'] ?? 0) < SQUAD_NAME_VERSION) {
      apply_legacy_names_to_squad($participant['squad'], team_source_key($team), (string)($team['name'] ?? ''));
      $participant['nameVersion'] = SQUAD_NAME_VERSION;
      normalize_squad_player_ids($participant['squad'], team_source_key($team), (string)($team['name'] ?? ''), 'home');
    }
    normalize_cpu_participant_formation($participant);
    $participant['idVersion'] = SQUAD_ID_VERSION;
    $participant['rating'] = squad_skill_rating($participant['squad']);
    $participant['strength'] = squad_skill_points($participant['squad']);
    return $participant;
  }
  $scaled = cpu_manager_participant($team, $level);
  $scaled['user'] = (string)($participant['user'] ?? $scaled['user']);
  return $scaled;
}

function manager_side_preview(?array $state, array $season, string $id): array {
  $participant = manager_participant($season, $id);
  if ($participant !== null && is_cpu_manager($id)) $participant = scaled_cpu_manager_participant($participant);
  if ($state !== null && !is_cpu_manager($id) && isset($state['users'][$id])) {
    $user = $state['users'][$id];
    ensure_user_squad($user);
    $team = has_team($user) ? user_team($user) : ($participant['team'] ?? ['name' => strtoupper($id), 'shirt' => '#fff', 'trim' => '#111']);
    $squad = $user['squad'] ?? ($participant['squad'] ?? ['starters' => [], 'bench' => []]);
    $formation = manager_user_formation($user);
  } else {
    $team = $participant['team'] ?? ['name' => strtoupper($id), 'shirt' => '#fff', 'trim' => '#111'];
    $squad = $participant['squad'] ?? ['starters' => [], 'bench' => []];
    $formation = valid_manager_formation((string)($participant['formation'] ?? '4-4-2'));
  }
  ensure_squad_stats($squad);
  return [
    'team' => $team,
    'squad' => $squad,
    'formation' => $formation,
    'rating' => squad_skill_rating($squad),
    'strength' => squad_skill_points($squad),
    'matchStrength' => squad_match_skill_points($squad),
  ];
}

function manager_side_data(array &$state, array $season, string $id): array {
  $participant = manager_participant($season, $id);
  if ($participant !== null && is_cpu_manager($id)) $participant = scaled_cpu_manager_participant($participant);
  if (!is_cpu_manager($id) && isset($state['users'][$id])) {
    ensure_user_squad($state['users'][$id]);
    ensure_manager_finance($state['users'][$id]);
    $squad = $state['users'][$id]['squad'] ?? ['starters' => [], 'bench' => []];
    return [
      'human' => true,
      'team' => user_team($state['users'][$id]),
      'squad' => $squad,
      'formation' => manager_user_formation($state['users'][$id]),
      'rating' => squad_skill_rating($squad),
      'strength' => squad_skill_points($squad),
      'matchStrength' => squad_match_skill_points($squad),
    ];
  }
  $squad = $participant['squad'] ?? ['starters' => [], 'bench' => []];
  ensure_squad_stats($squad);
  return [
    'human' => false,
    'team' => $participant['team'] ?? ['name' => strtoupper($id), 'shirt' => '#fff', 'trim' => '#111'],
    'squad' => $squad,
    'formation' => valid_manager_formation((string)($participant['formation'] ?? '4-4-2')),
    'rating' => squad_skill_rating($squad),
    'strength' => squad_skill_points($squad),
    'matchStrength' => squad_match_skill_points($squad),
  ];
}

function manager_table(array $participants): array {
  $table = [];
  foreach ($participants as $participant) {
    $name = (string)$participant['user'];
    $table[$name] = [
      'user' => $name,
      'team' => $participant['team'],
      'managerType' => $participant['managerType'] ?? 'human',
      'played' => 0,
      'won' => 0,
      'drawn' => 0,
      'lost' => 0,
      'gf' => 0,
      'ga' => 0,
      'gd' => 0,
      'points' => 0,
    ];
  }
  return $table;
}

function manager_fixtures(array $participants): array {
  $players = array_map(fn($p) => (string)$p['user'], $participants);
  if (count($players) % 2 === 1) $players[] = '__bye__';
  $n = count($players);
  $rounds = [];
  $roundNo = 1;
  $list = $players;
  for ($r = 0; $r < $n - 1; $r++) {
    $fixtures = [];
    for ($i = 0; $i < $n / 2; $i++) {
      $home = $list[$i];
      $away = $list[$n - 1 - $i];
      if ($home === '__bye__' || $away === '__bye__') continue;
      if ($r % 2 === 1) [$home, $away] = [$away, $home];
      $fixtures[] = ['home' => $home, 'away' => $away, 'status' => 'pending', 'score' => null, 'phase' => 'league'];
    }
    $rounds[] = ['round' => $roundNo++, 'fixtures' => $fixtures];
    $fixed = $list[0];
    $rest = array_slice($list, 1);
    array_unshift($rest, array_pop($rest));
    $list = array_merge([$fixed], $rest);
  }
  $firstLeg = $rounds;
  foreach ($firstLeg as $round) {
    $fixtures = [];
    foreach ($round['fixtures'] as $fixture) {
      $fixtures[] = ['home' => $fixture['away'], 'away' => $fixture['home'], 'status' => 'pending', 'score' => null, 'phase' => 'league'];
    }
    $rounds[] = ['round' => $roundNo++, 'fixtures' => $fixtures];
  }
  return $rounds;
}

function manager_pending_round_index(array $season): ?int {
  foreach (($season['rounds'] ?? []) as $i => $round) {
    foreach (($round['fixtures'] ?? []) as $fixture) {
      if (in_array(($fixture['status'] ?? ''), ['pending', 'watching'], true)) return (int)$i;
    }
    if (manager_round_continue_required($season, (int)$i)) {
      $split = manager_round_ready_waiting($season, (int)$i);
      if (!empty($split['waiting'])) return (int)$i;
    }
  }
  return null;
}

function manager_round_phase(array $round): string {
  $phase = trim((string)($round['phase'] ?? ''));
  if ($phase !== '') return strtolower($phase);
  foreach (($round['fixtures'] ?? []) as $fixture) {
    $fixturePhase = trim((string)($fixture['phase'] ?? ''));
    if ($fixturePhase !== '') return strtolower($fixturePhase);
  }
  return 'league';
}

function manager_next_round_stale_response(array &$state, array $season, string $user): array {
  return [
    'managerSeason' => public_manager_season($season, $state, $user),
    'managerFinance' => public_finance($state['users'][$user]),
    'managerSquad' => public_manager_context_squad($state, $user, $season),
    'managerMarket' => public_manager_market($state, $season),
    'managerTransferOffers' => public_manager_transfer_offers($state, $user),
    'staleRound' => true,
  ];
}

function manager_next_round_request_is_stale(array $season, ?int $roundIndex, array $data): bool {
  $hasRoundIndex = array_key_exists('roundIndex', $data) && is_numeric($data['roundIndex']);
  if (!$hasRoundIndex) return false;
  $requestedRoundIndex = (int)$data['roundIndex'];
  if ($roundIndex === null || $requestedRoundIndex !== $roundIndex) return true;

  $round = $season['rounds'][$roundIndex] ?? null;
  if (!is_array($round)) return true;

  $requestedKey = trim((string)($data['simKey'] ?? ''));
  if ($requestedKey !== '' && $requestedKey !== manager_sim_round_key($season, $roundIndex)) return true;

  $requestedPhase = strtolower(trim((string)($data['roundPhase'] ?? '')));
  if ($requestedPhase !== '' && $requestedPhase !== manager_round_phase($round)) return true;

  if (array_key_exists('fixtureIndex', $data) && is_numeric($data['fixtureIndex'])) {
    $fixtureIndex = (int)$data['fixtureIndex'];
    $fixture = $round['fixtures'][$fixtureIndex] ?? null;
    if (!is_array($fixture) || !in_array(($fixture['status'] ?? ''), ['pending', 'watching'], true)) return true;
  }
  return false;
}

function manager_round_continue_request_is_stale(array $season, ?int $roundIndex, array $data): bool {
  $hasRoundIndex = array_key_exists('roundIndex', $data) && is_numeric($data['roundIndex']);
  if (!$hasRoundIndex) return count(array_values($season['humanManagers'] ?? [])) > 1;
  $requestedRoundIndex = (int)$data['roundIndex'];
  if ($roundIndex === null || $requestedRoundIndex !== $roundIndex) return true;

  $round = $season['rounds'][$roundIndex] ?? null;
  if (!is_array($round)) return true;

  $requestedKey = trim((string)($data['simKey'] ?? ''));
  if ($requestedKey !== '' && $requestedKey !== manager_sim_round_key($season, $roundIndex)) return true;

  $requestedPhase = strtolower(trim((string)($data['roundPhase'] ?? '')));
  if ($requestedPhase !== '' && $requestedPhase !== manager_round_phase($round)) return true;

  return false;
}

function manager_round_log_context(array $season, int $roundIndex, ?int $fixtureIndex = null, array $fixtureOverride = []): array {
  $round = $season['rounds'][$roundIndex] ?? [];
  $fixture = $fixtureOverride;
  if (!$fixture && $fixtureIndex !== null) $fixture = $round['fixtures'][$fixtureIndex] ?? [];
  $phase = manager_round_phase(is_array($round) ? $round : []);
  if (!empty($fixture['phase'])) $phase = strtolower((string)$fixture['phase']);
  $out = [
    'seasonId' => (string)($season['id'] ?? ''),
    'roundIndex' => $roundIndex,
    'round' => $round['round'] ?? '',
    'phase' => $phase,
  ];
  if ($fixtureIndex !== null) {
    $out += [
      'fixtureIndex' => $fixtureIndex,
      'home' => (string)($fixture['home'] ?? ''),
      'away' => (string)($fixture['away'] ?? ''),
      'fixtureStatus' => (string)($fixture['status'] ?? ''),
    ];
  }
  if ($phase === 'cup') {
    $out += [
      'cupKey' => (string)($fixture['cupKey'] ?? $round['cupKey'] ?? ''),
      'cupName' => (string)($fixture['cupName'] ?? $round['cupName'] ?? 'Cup'),
      'cupStage' => (string)($fixture['cupStage'] ?? $round['cupStage'] ?? ''),
      'cupRoundNo' => (int)($fixture['cupRoundNo'] ?? $round['cupRoundNo'] ?? 0),
      'cupGroup' => (string)($fixture['cupGroup'] ?? $round['cupGroup'] ?? ''),
    ];
  }
  return $out;
}

function manager_sim_round_key(array $season, int $roundIndex): string {
  $round = $season['rounds'][$roundIndex]['round'] ?? $roundIndex;
  return (string)($season['status'] ?? 'active') . ':' . $roundIndex . ':' . (string)$round;
}

function manager_round_continue_key(array $season, int $roundIndex): string {
  $round = $season['rounds'][$roundIndex]['round'] ?? $roundIndex;
  return 'continue:' . (string)($season['id'] ?? '') . ':' . $roundIndex . ':' . (string)$round;
}

function manager_round_continue_required(array $season, int $roundIndex): bool {
  $key = manager_round_continue_key($season, $roundIndex);
  return !empty($season['roundContinueRequired'][$key]);
}

function manager_require_round_continue(array &$season, int $roundIndex): void {
  if (count(array_values($season['humanManagers'] ?? [])) <= 1) return;
  $key = manager_round_continue_key($season, $roundIndex);
  if (!isset($season['roundContinueRequired']) || !is_array($season['roundContinueRequired'])) $season['roundContinueRequired'] = [];
  $season['roundContinueRequired'][$key] = true;
}

function manager_has_round_continue(array $season, string $user, int $roundIndex): bool {
  $key = manager_round_continue_key($season, $roundIndex);
  return !empty($season['roundContinues'][$key][$user]);
}

function manager_mark_round_continue(array &$season, string $user, int $roundIndex, int $now): void {
  manager_require_round_continue($season, $roundIndex);
  $key = manager_round_continue_key($season, $roundIndex);
  if (!isset($season['roundContinues']) || !is_array($season['roundContinues'])) $season['roundContinues'] = [];
  if (!isset($season['roundContinues'][$key]) || !is_array($season['roundContinues'][$key])) $season['roundContinues'][$key] = [];
  $season['roundContinues'][$key][$user] = $now;
}

function manager_sim_fixture_key(array $season, int $roundIndex, int $fixtureIndex): string {
  $round = $season['rounds'][$roundIndex]['round'] ?? $roundIndex;
  return 'fixture:' . (string)($season['status'] ?? 'active') . ':' . $roundIndex . ':' . $fixtureIndex . ':' . (string)$round;
}

function manager_watch_fixture_key(array $season, int $roundIndex, int $fixtureIndex): string {
  return 'watch:' . manager_sim_fixture_key($season, $roundIndex, $fixtureIndex);
}

function manager_fixture_human_users(array $season, array $fixture): array {
  $sides = [(string)($fixture['home'] ?? ''), (string)($fixture['away'] ?? '')];
  $out = [];
  foreach (array_values($season['humanManagers'] ?? []) as $name) {
    $name = (string)$name;
    if ($name !== '' && in_array($name, $sides, true)) $out[] = $name;
  }
  return $out;
}

function manager_round_human_users(array $season, int $roundIndex): array {
  $found = [];
  foreach (($season['rounds'][$roundIndex]['fixtures'] ?? []) as $fixture) {
    foreach (manager_fixture_human_users($season, $fixture) as $name) $found[$name] = true;
  }
  $out = [];
  foreach (array_values($season['humanManagers'] ?? []) as $name) {
    $name = (string)$name;
    if ($name !== '' && isset($found[$name])) $out[] = $name;
  }
  return $out;
}

function manager_user_round_fixture(array $season, string $user, int $roundIndex): ?array {
  foreach (($season['rounds'][$roundIndex]['fixtures'] ?? []) as $fixtureIndex => $fixture) {
    if (($fixture['home'] ?? '') === $user || ($fixture['away'] ?? '') === $user) {
      return ['roundIndex' => $roundIndex, 'fixtureIndex' => (int)$fixtureIndex, 'fixture' => $fixture];
    }
  }
  return null;
}

function manager_requested_user_fixture(array $season, string $user, array $data): ?array {
  if (!isset($data['roundIndex']) || !is_numeric($data['roundIndex'])) return null;
  $roundIndex = (int)$data['roundIndex'];
  if (!isset($data['fixtureIndex']) || !is_numeric($data['fixtureIndex'])) {
    $entry = manager_user_round_fixture($season, $user, $roundIndex);
    if (!$entry || !in_array(($entry['fixture']['status'] ?? ''), ['pending', 'watching'], true)) return null;
    return $entry;
  }
  $fixtureIndex = (int)$data['fixtureIndex'];
  $fixture = $season['rounds'][$roundIndex]['fixtures'][$fixtureIndex] ?? null;
  if (!is_array($fixture)) return null;
  if (($fixture['home'] ?? '') !== $user && ($fixture['away'] ?? '') !== $user) return null;
  if (!in_array(($fixture['status'] ?? ''), ['pending', 'watching'], true)) return null;
  return ['roundIndex' => $roundIndex, 'fixtureIndex' => $fixtureIndex, 'fixture' => $fixture];
}

function manager_prepare_requested_user_fixture(array &$state, array &$season, string $user, array $data, int $now): ?int {
  $target = manager_requested_user_fixture($season, $user, $data);
  $roundIndex = manager_pending_round_index($season);
  if (!$target || $roundIndex === null || $roundIndex >= (int)$target['roundIndex']) return $roundIndex;
  $targetRound = (int)$target['roundIndex'];
  $guard = 0;
  while ($roundIndex !== null && $roundIndex < $targetRound && $guard++ < 40) {
    if (manager_has_watching($season)) return $roundIndex;
    if (manager_round_continue_required($season, $roundIndex)) {
      $own = manager_user_round_fixture($season, $user, $roundIndex);
      if ($own && ($own['fixture']['status'] ?? '') === 'played' && !manager_has_round_continue($season, $user, $roundIndex)) {
        if (manager_fixture_result_hidden_for_viewer($own['fixture'], $user, $now)) return $roundIndex;
        manager_mark_round_continue($season, $user, $roundIndex, $now);
      }
      $split = manager_round_ready_waiting($season, $roundIndex);
      if (!empty($split['waiting'])) return $roundIndex;
      $roundIndex = manager_pending_round_index($season);
      continue;
    }
    if (manager_round_human_users($season, $roundIndex)) return $roundIndex;
    simulate_manager_round_fixtures($state, $season, $roundIndex, false);
    $roundIndex = manager_pending_round_index($season);
    if ($roundIndex === null) {
      if (($season['status'] ?? '') === 'active' && empty($season['leagueResolved'])) start_manager_playoffs($season);
      elseif (($season['status'] ?? '') === 'playoff') advance_manager_playoffs($season);
      $roundIndex = manager_pending_round_index($season);
    }
  }
  return $roundIndex;
}

function manager_user_round_continue_pending(array $season, string $user, int $roundIndex): bool {
  if ($user === '' || !manager_round_continue_required($season, $roundIndex)) return false;
  $entry = manager_user_round_fixture($season, $user, $roundIndex);
  if (!$entry) return false;
  if (manager_fixture_result_hidden_for_viewer($entry['fixture'], $user)) return false;
  return ($entry['fixture']['status'] ?? '') === 'played' && !manager_has_round_continue($season, $user, $roundIndex);
}

function manager_round_ready_waiting(array $season, int $roundIndex): array {
  $ready = [];
  $waiting = [];
  $requiresContinue = manager_round_continue_required($season, $roundIndex);
  foreach (manager_round_human_users($season, $roundIndex) as $name) {
    $entry = manager_user_round_fixture($season, $name, $roundIndex);
    if (!$entry) {
      $ready[] = $name;
      continue;
    }
    $fixture = $entry['fixture'];
    $status = (string)($fixture['status'] ?? 'pending');
    if ($status === 'played') {
      if ($requiresContinue && !manager_has_round_continue($season, $name, $roundIndex)) $waiting[] = $name;
      else $ready[] = $name;
      continue;
    }
    $humans = manager_fixture_human_users($season, $fixture);
    $key = manager_sim_fixture_key($season, (int)$entry['roundIndex'], (int)$entry['fixtureIndex']);
    if ($status === 'pending' && count($humans) > 1 && !empty($season['simVotes'][$key][$name])) {
      $ready[] = $name;
      continue;
    }
    $waiting[] = $name;
  }
  return ['ready' => $ready, 'waiting' => $waiting];
}

function public_manager_sim_vote(array $season, string $viewer = ''): ?array {
  $roundIndex = manager_pending_round_index($season);
  if ($roundIndex === null) return null;
  if (count(array_values($season['humanManagers'] ?? [])) <= 1) return null;
  $humans = manager_round_human_users($season, $roundIndex);
  if (!$humans) return null;
  $key = manager_sim_round_key($season, $roundIndex);
  $split = manager_round_ready_waiting($season, $roundIndex);
  $ready = $split['ready'];
  $waiting = $split['waiting'];
  if (!$waiting) return null;
  return [
    'key' => $key,
    'roundIndex' => $roundIndex,
    'round' => $season['rounds'][$roundIndex]['round'] ?? '',
    'ready' => $ready,
    'waiting' => $waiting,
    'readyCount' => count($ready),
    'neededCount' => count($humans),
    'mineReady' => $viewer !== '' && in_array($viewer, $ready, true),
    'continueRequired' => manager_round_continue_required($season, $roundIndex),
    'mineContinuePending' => manager_user_round_continue_pending($season, $viewer, $roundIndex),
    'allReady' => false,
  ];
}

function public_manager_watch_vote(array $season, string $viewer = ''): ?array {
  $roundIndex = manager_pending_round_index($season);
  if ($roundIndex === null) return null;
  foreach (($season['rounds'][$roundIndex]['fixtures'] ?? []) as $fixtureIndex => $fixture) {
    if (($fixture['status'] ?? '') !== 'pending') continue;
    $humans = manager_fixture_human_users($season, $fixture);
    if (count($humans) <= 1) continue;
    $key = manager_watch_fixture_key($season, $roundIndex, (int)$fixtureIndex);
    $allVotes = isset($season['watchVotes']) && is_array($season['watchVotes']) ? $season['watchVotes'] : [];
    $votes = $allVotes[$key] ?? [];
    if (!is_array($votes)) $votes = [];
    $ready = array_values(array_filter($humans, fn($human) => !empty($votes[$human])));
    if (!$ready) continue;
    $waiting = array_values(array_filter($humans, fn($human) => empty($votes[$human])));
    if (!$waiting) return null;
    return [
      'key' => $key,
      'mode' => 'watch',
      'roundIndex' => $roundIndex,
      'fixtureIndex' => (int)$fixtureIndex,
      'round' => $season['rounds'][$roundIndex]['round'] ?? '',
      'ready' => $ready,
      'waiting' => $waiting,
      'readyCount' => count($ready),
      'neededCount' => count($humans),
      'mineReady' => $viewer !== '' && in_array($viewer, $ready, true),
      'continueRequired' => false,
      'mineContinuePending' => false,
      'allReady' => false,
    ];
  }
  return null;
}

function manager_sim_season_key(array $season): string {
  $roundIndex = manager_pending_round_index($season);
  $played = 0;
  foreach (($season['rounds'] ?? []) as $round) {
    foreach (($round['fixtures'] ?? []) as $fixture) {
      if (($fixture['status'] ?? '') === 'played') $played++;
    }
  }
  return 'season:' . (string)($season['id'] ?? '') . ':' . (string)($season['status'] ?? 'active') . ':' . (string)($roundIndex ?? 'done') . ':' . $played;
}

function public_manager_sim_season_vote(array $season, string $viewer = ''): ?array {
  if (($season['status'] ?? '') === 'finished') return null;
  $humans = array_values($season['humanManagers'] ?? []);
  if (count($humans) <= 1) return null;
  $key = manager_sim_season_key($season);
  $votes = $season['simSeasonVotes'][$key] ?? [];
  $ready = array_values(array_filter($humans, fn($name) => !empty($votes[$name])));
  $waiting = array_values(array_filter($humans, fn($name) => empty($votes[$name])));
  return [
    'key' => $key,
    'ready' => $ready,
    'waiting' => $waiting,
    'readyCount' => count($ready),
    'neededCount' => count($humans),
    'mineReady' => $viewer !== '' && !empty($votes[$viewer]),
    'allReady' => count($ready) >= count($humans),
  ];
}

function manager_base_season_year(): int {
  return 2026;
}

function manager_state_year(array $state): int {
  return max(manager_base_season_year(), (int)($state['managerYear'] ?? manager_base_season_year()));
}

function manager_season_year(array $season, ?array $state = null): int {
  $year = (int)($season['seasonYear'] ?? 0);
  if ($year <= 0 && $state !== null) $year = manager_state_year($state);
  return max(manager_base_season_year(), $year > 0 ? $year : manager_base_season_year());
}

function manager_estimated_start_balance(array $user): int {
  $finance = $user['managerFinance'] ?? [];
  return (int)($finance['balance'] ?? 0)
    - (int)($finance['income'] ?? 0)
    - (int)($finance['seasonAwards'] ?? 0)
    - (int)($finance['transferIncome'] ?? 0)
    + (int)($finance['wages'] ?? 0)
    + (int)($finance['transferSpent'] ?? 0)
    + (int)($finance['arenaSpent'] ?? 0);
}

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 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;
  $divisionLevel = 5;
  if (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 = [];
  $startingBalances = [];
  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));
    $team = user_team($state['users'][$name]);
    $managedTeamIds[$team['id']] = true;
    $squad = $state['users'][$name]['squad'] ?? ['starters' => [], 'bench' => []];
    $participants[] = [
      'user' => $name,
      'managerType' => 'human',
      'team' => $team,
      'rating' => squad_skill_rating($squad),
      'strength' => squad_skill_points($squad),
      'divisionLevel' => (int)($state['users'][$name]['managerDivisionLevel'] ?? $divisionLevel),
      'divisionName' => manager_division_name((int)($state['users'][$name]['managerDivisionLevel'] ?? $divisionLevel)),
    ];
  }
  $addCpuTeam = function (array $team) use (&$participants, &$managedTeamIds, $divisionLevel): bool {
    if (count($participants) >= manager_division_target_count($divisionLevel)) return false;
    $teamId = (string)($team['id'] ?? '');
    $sourceId = (string)($team['sourceId'] ?? $teamId);
    if ($teamId === '' || isset($managedTeamIds[$teamId]) || ($sourceId !== '' && isset($managedTeamIds[$sourceId]))) return false;
    $team['divisionId'] = manager_division_id($divisionLevel);
    $team['divisionName'] = manager_division_name($divisionLevel);
    $participants[] = cpu_manager_participant($team, $divisionLevel);
    $managedTeamIds[$teamId] = true;
    if ($sourceId !== '') $managedTeamIds[$sourceId] = true;
    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,
    'status' => 'active',
    'created' => $now,
    'updated' => $now,
    'seasonYear' => $seasonYear,
    'startingBalances' => $startingBalances,
    'createdBy' => $createdBy,
    'divisionLevel' => $divisionLevel,
    'divisionName' => manager_division_name($divisionLevel),
    'humanManagers' => $selected,
    'participants' => $participants,
    'table' => manager_table($participants),
    'rounds' => $rounds,
  ];
  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;
  return $viewer === '' || $viewer !== $visibleTo;
}

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

function manager_masked_live_score(array $fixture): ?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];
  $hiddenUntil = (int)($fixture['resultHiddenUntil'] ?? 0);
  $remaining = max(0, $hiddenUntil - time());
  $duration = max(1, MANAGER_SIM_RESULT_REVEAL_SECONDS);
  $progress = max(0.0, min(1.0, ($duration - $remaining) / $duration));
  $visibleGoals = (int)floor($progress * ($totalGoals + 1));
  if ($visibleGoals >= $totalGoals && $progress < 0.70) $visibleGoals = max(0, $totalGoals - 1);
  $visibleGoals = max(0, min($totalGoals, $visibleGoals));
  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 {
  if (manager_result_hidden_for_viewer((int)($report['resultHiddenUntil'] ?? 0), (string)($report['resultVisibleTo'] ?? ''), $viewer, $now)) return true;
  $roundIndex = (int)($report['roundIndex'] ?? -1);
  $fixtureIndex = (int)($report['fixtureIndex'] ?? -1);
  $fixture = $season['rounds'][$roundIndex]['fixtures'][$fixtureIndex] ?? null;
  return is_array($fixture) && manager_fixture_result_hidden_for_viewer($fixture, $viewer, $now);
}

function manager_mask_fixture_result(array $fixture): array {
  $fixture['status'] = 'simulating';
  $fixture['score'] = manager_masked_live_score($fixture);
  $fixture['resultMasked'] = true;
  $fixture['resultRevealIn'] = max(0, (int)($fixture['resultHiddenUntil'] ?? 0) - time());
  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;
  $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 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['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)) $fixture = manager_mask_fixture_result($fixture);
  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 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_group_stat_rows(array $rows, string $valueKey): array {
  $grouped = [];
  foreach ($rows as $row) {
    if (!is_array($row)) continue;
    $key = (string)($row['user'] ?? '') . ':' . (string)($row['player'] ?? 'Player');
    if (!isset($grouped[$key])) {
      $grouped[$key] = $row;
      $grouped[$key][$valueKey] = 0;
      if ($valueKey === 'goals') $grouped[$key]['penalties'] = 0;
    }
    $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_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);
  $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 = [];
  $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'])) {
        $liveSimulations[] = ['round' => $round['round'] ?? '', 'fixture' => $fixtureOut, 'revealIn' => (int)($fixtureOut['resultRevealIn'] ?? 0)];
      }
      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;
  }
  $rules = manager_promotion_rules((int)($season['divisionLevel'] ?? 5));
  $table = array_values($season['table'] ?? []);
  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);
  return [
    'id' => $season['id'] ?? '',
    'status' => $season['status'] ?? 'active',
    'seasonYear' => $seasonYear,
    'startBalance' => manager_viewer_start_balance($season, $state, $viewer),
    'createdBy' => $season['createdBy'] ?? '',
    'divisionLevel' => $season['divisionLevel'] ?? 5,
    'divisionName' => $season['divisionName'] ?? 'National League',
    'participantCount' => count($participants),
    'humanManagers' => array_values($season['humanManagers'] ?? []),
    'participants' => $publicParticipants,
    'table' => $table,
    'rounds' => $publicRounds,
    'cup' => $season['cup'] ?? null,
    'cups' => array_values($season['cups'] ?? []),
    'lastMatch' => $lastMatch,
    'humanLastMatches' => $humanLastMatches,
    'liveSimulations' => $liveSimulations,
    'promotionRules' => $rules,
    'playoffActive' => ($season['status'] ?? '') === 'playoff',
    'statContext' => $statContext,
    'topScorers' => array_slice($scorers, 0, 10),
    'topCupScorers' => array_slice($cupScorers, 0, 10),
    'topAssists' => array_slice($assists, 0, 10),
    'matchReports' => public_manager_match_reports($season, $viewer),
    'nextRound' => $nextRound,
    'outcome' => $season['outcome'] ?? null,
    'seasonReport' => ($season['status'] ?? '') === 'finished' ? manager_season_report($season, $names) : null,
    '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_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(__DIR__ . '/.manager.log', json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL, FILE_APPEND);
}

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_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 {
  $out = [];
  foreach ($users as $name) {
    if (empty($state['users'][$name]) || !is_array($state['users'][$name])) continue;
    $user = $state['users'][$name];
    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_save_metadata(array $save, string $id = '', string $viewer = ''): 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'));
  return [
    'id' => $id,
    'file' => $id,
    'label' => (string)($meta['label'] ?? $id),
    'savedBy' => $savedBy,
    'users' => $users,
    'mode' => $mode,
    'modeLabel' => strtoupper($mode),
    'slotKey' => (string)($meta['slotKey'] ?? implode('+', $users)),
    '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'] ?? ''),
    'canDelete' => $viewer !== '' && manager_save_can_delete($save, $viewer),
  ];
}

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, '');
  return true;
}

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

function manager_save_cached_allowed(array $meta, string $user): bool {
  $user = clean_name($user);
  if ($user === '') return false;
  if ($user === 'jdc') return true;
  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['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(['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);
  return $meta;
}

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

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

function manager_prune_saves(string $owner = '', int $limit = 2): void {
  // Saves are user-owned snapshots. Never auto-delete them; only explicit DELETE may remove a file.
}

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

function manager_any_save_for_season(string $seasonId): bool {
  if ($seasonId === '') return false;
  foreach ((glob(manager_save_dir() . '/*.json') ?: []) as $file) {
    $meta = manager_save_cached_metadata($file, '');
    if (is_array($meta) && (string)($meta['seasonId'] ?? '') === $seasonId) return true;
  }
  return false;
}

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_after_fixture(array $state, array $season, array $fixture, array $homeData, array $awayData, string $savedBy = ''): void {
  if (empty($homeData['human']) && empty($awayData['human'])) return;
  $users = manager_save_users($season);
  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'] ?? '');
  $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),
      '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' => [(string)($season['id'] ?? '') => $season],
      'managerCurrentId' => (string)($season['id'] ?? ''),
      'managerCurrentByUser' => array_fill_keys($users, (string)($season['id'] ?? '')),
      'managerYear' => manager_season_year($season, $state),
      'teamTrophies' => $state['teamTrophies'] ?? [],
      'managerMarket' => $state['managerMarket'] ?? [],
      'managerBrowseOffers' => $state['managerBrowseOffers'] ?? [],
      'managerTransferOffers' => $state['managerTransferOffers'] ?? [],
      'managerBrowseSold' => $state['managerBrowseSold'] ?? [],
      'users' => manager_save_user_snapshot($state, $users),
    ],
  ];
  $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 {
  $users = manager_save_users($season);
  if (!$users) return;
  $savedBy = clean_name($savedBy);
  if ($savedBy === '' || !in_array($savedBy, $users, true)) $savedBy = $users[0];
  $savedAt = time();
  $seasonId = (string)($season['id'] ?? '');
  $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),
      'seasonId' => $seasonId,
      'seasonYear' => manager_season_year($season, $state),
      'divisionName' => (string)($season['divisionName'] ?? ''),
      'seasonStatus' => (string)($season['status'] ?? ''),
      'phase' => 'season',
      'match' => $match,
      'score' => $score,
    ],
    'state' => [
      'managerSeasons' => [(string)($season['id'] ?? '') => $season],
      'managerCurrentId' => (string)($season['id'] ?? ''),
      'managerCurrentByUser' => array_fill_keys($users, (string)($season['id'] ?? '')),
      'managerYear' => manager_season_year($season, $state),
      'teamTrophies' => $state['teamTrophies'] ?? [],
      'managerMarket' => $state['managerMarket'] ?? [],
      'managerBrowseOffers' => $state['managerBrowseOffers'] ?? [],
      'managerTransferOffers' => $state['managerTransferOffers'] ?? [],
      'managerBrowseSold' => $state['managerBrowseSold'] ?? [],
      'users' => manager_save_user_snapshot($state, $users),
    ],
  ];
  $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 {
  if ($seasonId === '') return false;
  foreach ((glob(manager_save_dir() . '/*.json') ?: []) as $file) {
    $meta = manager_save_cached_metadata($file, '');
    if (!is_array($meta) || !manager_save_cached_allowed($meta, $user)) continue;
    if ((string)($meta['seasonId'] ?? '') === $seasonId) return true;
  }
  return false;
}

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(string $user): array {
  $files = glob(manager_save_dir() . '/*.json') ?: [];
  usort($files, fn($a, $b) => filemtime($b) <=> filemtime($a));
  $out = [];
  foreach ($files as $file) {
    $meta = manager_save_cached_metadata($file, $user);
    if (!is_array($meta) || !manager_save_cached_allowed($meta, $user)) continue;
    $out[] = $meta;
    if (count($out) >= 80) break;
  }
  return $out;
}

function manager_user_has_any_save(string $user): bool {
  $user = clean_name($user);
  if ($user === '') return false;
  foreach ((glob(manager_save_dir() . '/*.json') ?: []) as $file) {
    $meta = manager_save_cached_metadata($file, '');
    if (is_array($meta) && manager_save_cached_allowed($meta, $user)) return true;
  }
  return false;
}

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 load_manager_save_into_state(array &$state, array $save): void {
  $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)));
  $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);

  $savedSeasons = is_array($saved['managerSeasons'] ?? null) ? $saved['managerSeasons'] : [];
  $savedSeasonIds = array_fill_keys(array_map('strval', array_keys($savedSeasons)), true);
  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])) || in_array($home, $saveUsers, true) || in_array($away, $saveUsers, true)) {
      unset($state['matches'][$matchId]);
    }
  }

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

  $savedCurrentByUser = is_array($saved['managerCurrentByUser'] ?? null) ? $saved['managerCurrentByUser'] : [];
  $savedCurrentId = is_string($saved['managerCurrentId'] ?? null) ? $saved['managerCurrentId'] : null;
  foreach ($saveUsers 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 ($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 (!isset($state['managerBrowseOffers']) || !is_array($state['managerBrowseOffers'])) $state['managerBrowseOffers'] = [];
  foreach ($saveUsers as $name) {
    if (isset($saved['managerBrowseOffers'][$name]) && is_array($saved['managerBrowseOffers'][$name])) $state['managerBrowseOffers'][$name] = $saved['managerBrowseOffers'][$name];
  }
  if (is_array($saved['managerTransferOffers'] ?? null)) {
    if (!isset($state['managerTransferOffers']) || !is_array($state['managerTransferOffers'])) $state['managerTransferOffers'] = [];
    foreach ($saved['managerTransferOffers'] as $offerId => $offer) {
      if (!is_array($offer)) continue;
      $buyer = clean_name((string)($offer['buyer'] ?? ''));
      $seller = clean_name((string)($offer['seller'] ?? ''));
      if (in_array($buyer, $saveUsers, true) || in_array($seller, $saveUsers, true)) $state['managerTransferOffers'][$offerId] = $offer;
    }
  }
  if (is_array($saved['managerBrowseSold'] ?? null)) $state['managerBrowseSold'] = ($state['managerBrowseSold'] ?? []) + $saved['managerBrowseSold'];
  if (empty($state['managerMarket']) && is_array($saved['managerMarket'] ?? null)) $state['managerMarket'] = $saved['managerMarket'];
  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]);
  }
  foreach ($savedUsers as $name => $savedUser) {
    if (!is_array($savedUser) || !isset($state['users'][$name])) continue;
    $password = $state['users'][$name]['password'] ?? '';
    $created = (int)($state['users'][$name]['created'] ?? time());
    $lastSeen = (int)($state['users'][$name]['lastSeen'] ?? time());
    $state['users'][$name] = $savedUser;
    $state['users'][$name]['password'] = $password;
    $state['users'][$name]['created'] = $created;
    $state['users'][$name]['lastSeen'] = $lastSeen;
  }
  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 current_manager_season_id(array $state, string $user = ''): ?string {
  if ($user !== '') {
    $byUser = $state['managerCurrentByUser'][$user] ?? null;
    if (
      is_string($byUser)
      && isset($state['managerSeasons'][$byUser])
      && ($state['managerSeasons'][$byUser]['status'] ?? '') !== 'aborted'
      && manager_season_visible_to_user($state['managerSeasons'][$byUser], $user)
    ) {
      return $byUser;
    }

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

    $bestId = null;
    $bestScore = -1;
    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;
      $status = (string)($season['status'] ?? '');
      $statusRank = in_array($status, ['active', 'playoff'], true) ? 3 : ($status === 'finished' ? 2 : 1);
      $score = $statusRank * 10000000000 + (int)($season['updated'] ?? $season['created'] ?? 0);
      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 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 !== '') $state['managerCurrentByUser'][$user] = $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);
}

require_once __DIR__ . '/.lib/manager_progression.php';

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

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

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

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

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

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

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

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

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

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

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

function canonical_player_role(string $role, string $fallback = 'CM'): string {
  $role = strtoupper(trim($role));
  $map = [
    'GOALKEEPER' => 'GK', 'KEEPER' => 'GK',
    'LEFT BACK' => 'LB', 'RIGHT BACK' => 'RB', 'CENTRE BACK' => 'CB', 'CENTER BACK' => 'CB',
    'LEFT MIDFIELD' => 'LM', 'RIGHT MIDFIELD' => 'RM', 'MIDFIELD' => 'CM', 'MIDFIELDER' => 'CM',
    'FORWARD' => 'ST', 'ATTACKER' => 'ST', 'STRIKER' => 'ST', 'CF' => 'ST', 'FW' => 'ST',
  ];
  if (isset($map[$role])) return $map[$role];
  if (in_array($role, ['GK', 'LB', 'RB', 'CB', 'LM', 'RM', 'CM', 'ST'], true)) return $role;
  return $fallback;
}

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

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

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

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

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

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

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

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

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

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

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

function player_position_penalty_percent(array $player, ?string $slotRole = null): int {
  $natural = player_natural_role($player);
  $slot = canonical_player_role((string)($slotRole ?? $player['slotRole'] ?? $player['role'] ?? ''), $natural);
  if ($natural === $slot) return 0;
  if (player_side_pair($natural) === player_side_pair($slot)) return 0;
  return -10;
}

function player_effective_rating(array $player): int {
  $rating = (int)($player['rating'] ?? 50) + player_foot_slot_bonus($player);
  $positionPenalty = player_position_penalty_percent($player);
  if ($positionPenalty < 0) $rating = (int)round($rating * (100 + $positionPenalty) / 100);
  $formMultiplier = (float)($player['dailyFormMultiplier'] ?? 1.0);
  if ($formMultiplier > 0 && abs($formMultiplier - 1.0) > 0.0001) $rating = (int)round($rating * $formMultiplier);
  return clamp_int($rating, 0, 100);
}

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

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

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

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

  $layers = [[0 => 0]];
  $parents = [];
  for ($slotIndex = 0; $slotIndex < $slotCount; $slotIndex++) {
    $next = [];
    $parents[$slotIndex + 1] = [];
    foreach ($layers[$slotIndex] as $mask => $score) {
      for ($playerIndex = 0; $playerIndex < $playerCount; $playerIndex++) {
        $bit = 1 << $playerIndex;
        if (((int)$mask & $bit) !== 0) continue;
        $nextMask = (int)$mask | $bit;
        $nextScore = (int)$score + (int)$scores[$slotIndex][$playerIndex];
        if (!isset($next[$nextMask]) || $nextScore > $next[$nextMask]) {
          $next[$nextMask] = $nextScore;
          $parents[$slotIndex + 1][$nextMask] = [(int)$mask, $playerIndex];
        }
      }
    }
    if (!$next) return null;
    $layers[$slotIndex + 1] = $next;
  }

  $bestMask = null;
  $bestScore = PHP_INT_MIN;
  foreach ($layers[$slotCount] as $mask => $score) {
    if ((int)$score > $bestScore) {
      $bestScore = (int)$score;
      $bestMask = (int)$mask;
    }
  }
  if ($bestMask === null) return null;

  $assignment = array_fill(0, $slotCount, -1);
  $mask = $bestMask;
  for ($slotIndex = $slotCount; $slotIndex >= 1; $slotIndex--) {
    if (!isset($parents[$slotIndex][$mask])) return null;
    [$prevMask, $playerIndex] = $parents[$slotIndex][$mask];
    $assignment[$slotIndex - 1] = (int)$playerIndex;
    $mask = (int)$prevMask;
  }
  return $assignment;
}

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

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

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

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

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

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

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

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

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

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

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

function make_bench(string $side, int $baseRating, string $teamId = '', string $teamName = ''): array {
  $players = [];
  $roles = [
    ['GK', 0.50, 0.94],
    ['CB', 0.50, 0.80],
    ['LB', 0.18, 0.78],
    ['RB', 0.82, 0.78],
    ['CM', 0.50, 0.61],
    ['LM', 0.16, 0.58],
    ['ST', 0.50, 0.36],
  ];
  foreach ($roles as $index => $row) {
    [$role, $x, $homeY] = $row;
    $y = $side === 'away' ? 1 - $homeY : $homeY;
    $stats = [];
    foreach (player_stat_keys($role === 'GK') as $stat) {
      $stats[$stat] = stat_value($baseRating - 2, $role, $stat);
    }
	    $players[] = [
	      'id' => generated_squad_player_id($teamId, $teamName, $side, $index + 12),
	      'shirtNumber' => $index + 12,
	      'name' => player_name($side, $index + 11, $role),
	      'role' => $role,
	      'naturalRole' => $role,
	      'slotRole' => $role,
	      'rating' => average_stats($stats),
      'stats' => $stats,
      'weightClass' => random_weight_class(),
      'homeX' => $x,
      'homeY' => $y,
      'x' => $x,
      'y' => $y,
    ];
  }
  return $players;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

function manager_prepare_injuries_for_current_round(array &$state, array &$season, int $now): void {
  if (!in_array(($season['status'] ?? ''), ['active', 'playoff'], true)) return;
  $roundIndex = manager_pending_round_index($season);
  if ($roundIndex === null) return;
  manager_trigger_due_injuries($state, $season, $roundIndex, $now);
}

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

function manager_injured_starters(array $state, array $season, string $user): array {
  if (empty($state['users'][$user]) || !is_array($state['users'][$user])) return [];
  $active = manager_active_injuries($season, $user);
  if (!$active) return [];
  $out = [];
  foreach (($state['users'][$user]['squad']['starters'] ?? []) as $player) {
    $id = (string)($player['id'] ?? '');
    if ($id === '' || empty($active[$id])) continue;
    $injury = $active[$id];
    $out[] = [
      'playerId' => $id,
      'playerName' => (string)($player['name'] ?? $injury['playerName'] ?? 'Player'),
      'reason' => (string)($injury['reason'] ?? 'Injured'),
      'remaining' => (int)($injury['remaining'] ?? 0),
    ];
  }
  return $out;
}

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

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

function public_manager_injury_report(array $state, array $season, string $viewer): array {
  $active = [];
  foreach (manager_active_injuries($season, $viewer) as $injury) {
    $active[] = [
      'playerId' => (string)($injury['playerId'] ?? ''),
      'playerName' => (string)($injury['playerName'] ?? 'Player'),
      'reason' => (string)($injury['reason'] ?? 'Injured'),
      'remaining' => (int)($injury['remaining'] ?? 0),
    ];
  }
  $blocked = manager_injured_starters($state, $season, $viewer);
  return [
    'active' => public_manager_injury_rows($active),
    'blockedStarters' => public_manager_injury_rows($blocked),
    'blocked' => count($blocked) > 0,
  ];
}

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

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

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

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

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

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

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

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

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

function manager_start_balance(int $level): int {
  return match (clamp_int($level, 1, 5)) {
    1 => 45000000,
    2 => 12000000,
    3 => 5500000,
    4 => 2500000,
    default => 900000,
  };
}

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

function manager_base_arena_capacity(int $level): int {
  return match (clamp_int($level, 1, 5)) {
    1 => 32000,
    2 => 18000,
    3 => 10500,
    4 => 6500,
    default => 3500,
  };
}

function manager_arena_expansion_option(array $finance): ?array {
  $level = clamp_int((int)($finance['arenaLevel'] ?? 0), 0, 10);
  if ($level >= 10) return null;
  $next = $level + 1;
  $seats = 500 + ($next - 1) * 250;
  $cost = manager_round_money((int)round($seats * (70 + $next * 35)), 5000);
  return [
    'level' => $next,
    'seats' => $seats,
    'cost' => $cost,
    'rounds' => $next * 5,
  ];
}

function manager_attendance_for_match(int $capacity, int $level, int $rank, int $teams, int $gf, int $ga, string $phase): int {
  $capacity = max(500, $capacity);
  $teams = max(1, $teams);
  $rank = clamp_int($rank, 1, $teams);
  $success = $teams > 1 ? ($teams - $rank) / ($teams - 1) : 0.5;
  $rate = 0.42 + $success * 0.32;
  if ($gf > $ga) $rate += 0.05;
  elseif ($gf < $ga) $rate -= 0.04;
  if ($phase === 'cup' || $phase === 'playoff') $rate += 0.08;
  $rate += mt_rand(-4, 4) / 100;
  return clamp_int((int)round($capacity * clamp_float($rate, 0.24, 0.96)), 150, $capacity);
}

function manager_match_income_breakdown(int $level, int $gf, int $ga, string $phase = 'league', int $capacity = 0, int $rank = 12, int $teams = 24, bool $homeGame = true): array {
  $level = clamp_int($level, 1, 5);
  [$base, $win, $draw] = match (clamp_int($level, 1, 5)) {
    1 => [760000, 360000, 140000],
    2 => [220000, 105000, 42000],
    3 => [82000, 42000, 18000],
    4 => [42000, 22000, 9000],
    default => [7500, 3500, 1400],
  };
  [$goalBonus, $swing] = match (clamp_int($level, 1, 5)) {
    1 => [35000, 90000],
    2 => [12000, 26000],
    3 => [4500, 9500],
    4 => [2500, 5200],
    default => [500, 1600],
  };
  $cupExtra = match ($level) {
    1 => 190000,
    2 => 75000,
    3 => 28000,
    4 => 12000,
    default => 6000,
  };
  $phase = strtolower(trim($phase));
  $isCup = $phase === 'cup' || $phase === 'playoff';
  $resultBonus = $gf > $ga ? $win : ($gf === $ga ? $draw : 0);
  $goalMoney = min(4, max(0, $gf)) * $goalBonus;
  $variance = mt_rand(-$swing, $swing);
  $attendance = $homeGame ? manager_attendance_for_match($capacity > 0 ? $capacity : manager_base_arena_capacity($level), $level, $rank, $teams, $gf, $ga, $phase) : 0;
  $ticketPrice = match ($level) {
    1 => 36,
    2 => 25,
    3 => 17,
    4 => 12,
    default => 7,
  };
  $gateBase = $homeGame ? ($attendance * $ticketPrice) : (int)round($base * 0.18);
  $gate = manager_round_money(max((int)round($base * ($homeGame ? 0.30 : 0.12)), $gateBase + $variance + ($homeGame && $isCup ? (int)round($cupExtra * 0.25) : 0)));
  $merch = manager_round_money(max(0, (int)round($attendance * ($level <= 2 ? 3.2 : ($level <= 4 ? 1.8 : 1.1)) + max(0, $gf) * $goalBonus * 0.28 + ($gf > $ga ? $base * 0.035 : 0))));
  $bonus = manager_round_money(max(0, $resultBonus + (int)round($goalMoney * 0.65)));
  $cup = $isCup ? manager_round_money($cupExtra) : 0;
  return [
    'attendance' => $attendance,
    'gateIncome' => $gate,
    'merchIncome' => $merch,
    'bonusIncome' => $bonus,
    'cupIncome' => $cup,
  ];
}

function manager_match_income(int $level, int $gf, int $ga, string $phase = 'league'): int {
  $breakdown = manager_match_income_breakdown($level, $gf, $ga, $phase);
  return (int)($breakdown['gateIncome'] ?? 0)
    + (int)($breakdown['merchIncome'] ?? 0)
    + (int)($breakdown['bonusIncome'] ?? 0)
    + (int)($breakdown['cupIncome'] ?? 0);
}

function manager_user_rank_in_season(array $season, string $user): int {
  foreach (manager_ranked_rows($season) as $index => $row) {
    if ((string)($row['user'] ?? '') === $user) return $index + 1;
  }
  return count($season['table'] ?? []) ?: 1;
}

function manager_season_finance_award(int $level, int $rank, int $teams, string $outcome): int {
  $level = clamp_int($level, 1, 5);
  $teams = max(1, $teams);
  $rank = clamp_int($rank, 1, $teams);
  $placeRate = $teams > 1 ? ($teams - $rank) / ($teams - 1) : 1.0;
  [$base, $placeMax, $promotion] = match ($level) {
    1 => [18000000, 22000000, 0],
    2 => [4500000, 5500000, 2500000],
    3 => [900000, 1200000, 650000],
    4 => [450000, 650000, 300000],
    default => [95000, 65000, 75000],
  };
  $award = $base + (int)round($placeMax * $placeRate);
  if ($outcome === 'promoted') $award += $promotion;
  return max(0, (int)round($award / 5000) * 5000);
}

function ensure_manager_finance(array &$user): void {
  $level = clamp_int((int)($user['managerDivisionLevel'] ?? 5), 1, 5);
  if (!isset($user['managerFinance']) || !is_array($user['managerFinance']) || (int)($user['managerFinance']['version'] ?? 0) < 2) {
    $user['managerFinance'] = [
      'version' => 2,
      'balance' => manager_start_balance($level),
      'income' => 0,
      'seasonAwards' => 0,
      'gateIncome' => 0,
      'merchIncome' => 0,
      'bonusIncome' => 0,
      'cupIncome' => 0,
      'arenaLevel' => 0,
      'arenaCapacity' => manager_base_arena_capacity($level),
      'arenaLastAttendance' => 0,
      'arenaTotalAttendance' => 0,
      'arenaHomeMatches' => 0,
      'arenaSpent' => 0,
      'arenaBuild' => null,
      'wages' => 0,
      'transferSpent' => 0,
      'transferIncome' => 0,
    ];
  }
  $user['managerFinance'] += ['version' => 2, 'balance' => manager_start_balance($level), 'income' => 0, 'seasonAwards' => 0, 'gateIncome' => 0, 'merchIncome' => 0, 'bonusIncome' => 0, 'cupIncome' => 0, 'arenaLevel' => 0, 'arenaCapacity' => manager_base_arena_capacity($level), 'arenaLastAttendance' => 0, 'arenaTotalAttendance' => 0, 'arenaHomeMatches' => 0, 'arenaSpent' => 0, 'arenaBuild' => null, 'wages' => 0, 'transferSpent' => 0, 'transferIncome' => 0];
  if ((int)($user['managerFinance']['arenaCapacity'] ?? 0) <= 0) $user['managerFinance']['arenaCapacity'] = manager_base_arena_capacity($level);
  if (!is_array($user['managerFinance']['arenaBuild'] ?? null)) $user['managerFinance']['arenaBuild'] = null;
  if (empty($user['managerDivisionLevel'])) $user['managerDivisionLevel'] = 5;
}

function manager_division_name(int $level): string {
  return match (clamp_int($level, 1, 5)) {
    1 => 'Premier League',
    2 => 'Championship',
    3 => 'League One',
    4 => 'League Two',
    default => 'National League',
  };
}

function public_finance(array $user): array {
  $finance = $user['managerFinance'] ?? [];
  $level = clamp_int((int)($user['managerDivisionLevel'] ?? 5), 1, 5);
  $squad = $user['squad'] ?? ['starters' => [], 'bench' => []];
  $build = is_array($finance['arenaBuild'] ?? null) ? $finance['arenaBuild'] : null;
  $homeMatches = max(0, (int)($finance['arenaHomeMatches'] ?? 0));
  $nextArena = manager_arena_expansion_option($finance);
  return [
    'balance' => (int)($finance['balance'] ?? 0),
    'income' => (int)($finance['income'] ?? 0),
    'seasonAwards' => (int)($finance['seasonAwards'] ?? 0),
    'gateIncome' => (int)($finance['gateIncome'] ?? 0),
    'merchIncome' => (int)($finance['merchIncome'] ?? 0),
    'bonusIncome' => (int)($finance['bonusIncome'] ?? 0),
    'cupIncome' => (int)($finance['cupIncome'] ?? 0),
    'arenaLevel' => clamp_int((int)($finance['arenaLevel'] ?? 0), 0, 10),
    'arenaMaxLevel' => 10,
    'arenaCapacity' => (int)($finance['arenaCapacity'] ?? manager_base_arena_capacity($level)),
    'arenaLastAttendance' => (int)($finance['arenaLastAttendance'] ?? 0),
    'arenaAverageAttendance' => $homeMatches > 0 ? (int)round((int)($finance['arenaTotalAttendance'] ?? 0) / $homeMatches) : 0,
    'arenaHomeMatches' => $homeMatches,
    'arenaSpent' => (int)($finance['arenaSpent'] ?? 0),
    'arenaBuild' => $build,
    'arenaNext' => $nextArena,
    'wages' => (int)($finance['wages'] ?? 0),
    'transferSpent' => (int)($finance['transferSpent'] ?? 0),
    'transferIncome' => (int)($finance['transferIncome'] ?? 0),
    'weeklyWages' => squad_wages($squad),
    'squadValue' => squad_value($squad),
    'startersValue' => squad_value($squad, 'starters'),
    'benchValue' => squad_value($squad, 'bench'),
    'divisionLevel' => $level,
    'divisionName' => manager_division_name($level),
  ];
}

function legacy_player_market_pool(): array {
  static $pool = null;
  if ($pool !== null) return $pool;
  $cache = legacy_player_name_cache();
  $teamsBySlug = [];
  foreach (training_divisions() as $division) {
    foreach (($division['teams'] ?? []) as $team) {
      $sourceId = source_team_id((string)($team['sourceId'] ?? $team['id'] ?? ''));
      foreach (legacy_team_slug_candidates($sourceId) as $slug) {
        $teamsBySlug[$slug] = [
          'teamId' => $sourceId,
          'teamName' => (string)($team['name'] ?? strtoupper($sourceId)),
          'divisionName' => (string)($division['name'] ?? ''),
          'baseRating' => (int)($team['baseRating'] ?? team_base_rating($sourceId)),
        ];
      }
    }
  }
  $pool = [];
  foreach ($cache as $slug => $players) {
    if (empty($teamsBySlug[$slug]) || !is_array($players)) continue;
    foreach ($players as $index => $player) {
      if (empty($player['name'])) continue;
      $pool[] = $teamsBySlug[$slug] + [
        'sourceSlug' => (string)$slug,
        'sourceIndex' => (int)$index,
        'name' => (string)$player['name'],
        'shirtName' => (string)($player['shirtName'] ?? $player['name']),
        'legacyRole' => (string)($player['legacyRole'] ?? ''),
        'legacyNumber' => isset($player['legacyNumber']) ? (int)$player['legacyNumber'] : null,
      ];
    }
  }
  return $pool;
}

function market_role_from_legacy(string $legacyRole, int $seed): string {
  $role = strtoupper($legacyRole);
  if (str_contains($role, 'GOAL') || $role === 'GK') return 'GK';
  if (str_contains($role, 'ATTACK') || str_contains($role, 'FORWARD') || $role === 'FW') return 'ST';
  if (str_contains($role, 'MID') || $role === 'MF') {
    return ['CM', 'RM', 'LM'][abs($seed) % 3];
  }
  if (str_contains($role, 'DEF') || $role === 'DF') {
    return ['CB', 'RB', 'LB'][abs($seed) % 3];
  }
  return ['CM', 'CB', 'ST', 'RM', 'LM'][abs($seed) % 5];
}

function market_player(int $index): array {
  $pool = legacy_player_market_pool();
  $source = $pool ? $pool[(int)(crc32('market-source-' . $index . '-' . time()) % count($pool))] : null;
  $seed = (int)crc32(($source['sourceSlug'] ?? 'market') . '-' . ($source['sourceIndex'] ?? $index) . '-' . time());
  $role = $source ? market_role_from_legacy((string)($source['legacyRole'] ?? ''), $seed) : ['GK', 'CB', 'RB', 'LB', 'CM', 'RM', 'LM', 'ST'][$index % 8];
  $base = $source
    ? clamp_int((int)($source['baseRating'] ?? 50) + (int)($seed % 13) - 6, 30, 92)
    : clamp_int(36 + (int)(crc32('market-' . $index . '-' . time()) % 45), 30, 88);
  $stats = [];
  foreach (player_stat_keys($role === 'GK') as $stat) {
    $stats[$stat] = stat_value($base, $role, $stat);
  }
  $player = [
    'id' => 'M' . bin2hex(random_bytes(4)),
    'name' => $source ? (string)$source['name'] : player_name('home', $index + 30, $role),
    'shirtName' => $source ? (string)$source['shirtName'] : '',
    'sourceTeam' => $source ? (string)$source['teamName'] : 'FREE AGENT',
    'sourceTeamId' => $source ? (string)$source['teamId'] : '',
    'sourceDivision' => $source ? (string)$source['divisionName'] : '',
	    'legacyNumber' => $source['legacyNumber'] ?? null,
	    'role' => $role,
	    'naturalRole' => $role,
	    'slotRole' => $role,
	    'rating' => average_stats($stats),
    'stats' => $stats,
    'homeX' => 0.5,
    'homeY' => 0.62,
    'x' => 0.5,
	    'y' => 0.62,
	  ];
  ensure_player_stats($player);
	  $player['price'] = player_value($player);
	  return $player;
}

function ensure_manager_market(array &$state): void {
  if (!isset($state['managerMarket']) || !is_array($state['managerMarket'])) $state['managerMarket'] = [];
  $target = MANAGER_MARKET_TARGET_PLAYERS;
  if ((int)($state['managerMarketVersion'] ?? 0) < MANAGER_MARKET_VERSION || count($state['managerMarket']) < $target) {
    $index = count($state['managerMarket']);
    $guard = 0;
    while (count($state['managerMarket']) < $target && $guard++ < $target * 3) {
      $state['managerMarket'][] = market_player($index + $guard);
    }
    $state['managerMarketVersion'] = MANAGER_MARKET_VERSION;
  }
}

function public_player(array $player): array {
  ensure_player_stats($player);
  $baseRating = (int)($player['rating'] ?? 50);
  $slotRole = player_slot_role($player);
  $player['baseRating'] = $baseRating;
  $player['effectiveRating'] = player_effective_rating($player);
  $player['rating'] = $player['effectiveRating'];
  $player['naturalRole'] = player_natural_role($player);
  $player['displayRole'] = player_display_role($player);
  $player['role'] = $player['naturalRole'];
  $player['slotRole'] = $slotRole;
  $player['foot'] = player_preferred_foot($player);
  $player['footBonus'] = player_foot_slot_bonus($player, $slotRole);
  $player['positionPenalty'] = player_position_penalty_percent($player, $slotRole);
  return $player;
}

function public_manager_market(array $state, ?array $season = null): array {
  $out = [];
  foreach (array_values($state['managerMarket'] ?? []) as $player) {
    $sourceTeamId = (string)($player['sourceTeamId'] ?? '');
    if ($sourceTeamId !== '' && empty($player['marketListedByUser']) && manager_team_controlled_by_human($state, $season, $sourceTeamId)) continue;
    $out[] = public_player($player);
  }
  return $out;
}

function public_squad(array $user, ?array $squadOverride = null): array {
  $squad = $squadOverride ?? ($user['squad'] ?? []);
  $out = ['starters' => [], 'bench' => [], 'formation' => manager_user_formation($user), 'formationOptions' => manager_formation_options()];
	foreach (['starters', 'bench'] as $group) {
	    foreach (($squad[$group] ?? []) as $player) {
	      $player = public_player($player);
	      $player['value'] = player_value($player);
	      $player['wage'] = player_wage($player);
      $out[$group][] = $player;
    }
  }
  return $out;
}

function public_manager_context_squad(array &$state, string $user, ?array $season = null): array {
  if (empty($state['users'][$user]) || !is_array($state['users'][$user])) return ['starters' => [], 'bench' => [], 'formation' => '4-4-2', 'formationOptions' => manager_formation_options()];
  ensure_user_squad($state['users'][$user]);
  $squad = $state['users'][$user]['squad'] ?? ['starters' => [], 'bench' => []];
  if ($season !== null && manager_participant($season, $user) !== null) {
    $side = [
      'squad' => $squad,
      'strength' => squad_skill_points($squad),
      'matchStrength' => squad_match_skill_points($squad),
    ];
    $side = manager_apply_next_form_to_side_data($season, $user, $side);
    $squad = manager_annotate_squad_injuries($side['squad'] ?? $squad, $season, $user);
    $squad = manager_annotate_squad_cards($squad, $season, $user, $state);
  }
  return public_squad($state['users'][$user], $squad);
}

function find_squad_player(array $squad, string $playerId): ?array {
  foreach (['starters', 'bench'] as $group) {
    foreach (($squad[$group] ?? []) as $index => $player) {
      if ((string)($player['id'] ?? '') === $playerId) return ['group' => $group, 'index' => $index, 'player' => $player];
    }
  }
  return null;
}

function manager_transfer_cash_offer(array $data, int $basePrice): int {
  $cash = array_key_exists('cashOffer', $data) ? (int)$data['cashOffer'] : (array_key_exists('bidPrice', $data) ? (int)$data['bidPrice'] : $basePrice);
  return max(0, (int)round($cash));
}

function manager_player_sale_base(array $player): int {
  $saleBase = player_value($player);
  if (!empty($player['purchasePrice']) && (int)$player['purchasePrice'] < $saleBase * 0.6) {
    $saleBase = (int)$player['purchasePrice'];
  }
  return max(25000, (int)round($saleBase * 0.62 / 25000) * 25000);
}

function manager_sale_chance(int $askPrice, int $basePrice): int {
  $basePrice = max(1, $basePrice);
  $ratio = $askPrice / $basePrice;
  return clamp_int((int)round(78 - (($ratio - 1.0) * 95)), 5, 96);
}

function manager_cpu_market_offer_cash(int $askPrice, int $basePrice): int {
  $askPrice = max(1000, $askPrice);
  $basePrice = max(1, $basePrice);
  $ratio = $askPrice / $basePrice;
  if ($ratio <= 1.05) {
    [$minPct, $maxPct] = [90, 100];
  } elseif ($ratio <= 1.25) {
    [$minPct, $maxPct] = [84, 98];
  } elseif ($ratio <= 1.60) {
    [$minPct, $maxPct] = [72, 94];
  } else {
    [$minPct, $maxPct] = [55, 86];
  }
  $offer = $askPrice * (mt_rand($minPct, $maxPct) / 100);
  return max(1000, (int)round($offer / 5000) * 5000);
}

function manager_user_played_match_count(?array $season, string $user): int {
  $user = clean_name($user);
  if ($user === '' || !is_array($season)) return 0;
  $played = 0;
  foreach (($season['rounds'] ?? []) as $round) {
    foreach (($round['fixtures'] ?? []) as $fixture) {
      if (($fixture['status'] ?? '') !== 'played') continue;
      if (($fixture['home'] ?? '') === $user || ($fixture['away'] ?? '') === $user) $played++;
    }
  }
  return $played;
}

function manager_schedule_cpu_market_offer(array &$player, ?array $season, string $seller, int $minMatches = 1, int $maxMatches = 10): void {
  $minMatches = max(1, $minMatches);
  $maxMatches = max($minMatches, $maxMatches);
  $played = manager_user_played_match_count($season, $seller);
  $delay = mt_rand($minMatches, $maxMatches);
  if (!isset($player['marketCpuOfferListedMatches'])) $player['marketCpuOfferListedMatches'] = $played;
  $player['marketCpuOfferDelayMatches'] = $delay;
  $player['marketCpuOfferAfterMatches'] = $played + $delay;
}

function manager_reschedule_cpu_market_offer(array &$state, ?array $season, string $seller, string $marketPlayerId, int $minMatches = 1, int $maxMatches = 4): void {
  $seller = clean_name($seller);
  if ($seller === '' || $marketPlayerId === '' || empty($state['managerMarket']) || !is_array($state['managerMarket'])) return;
  foreach ($state['managerMarket'] as &$player) {
    if (!is_array($player) || (string)($player['id'] ?? '') !== $marketPlayerId) continue;
    if (clean_name((string)($player['marketListedByUser'] ?? '')) !== $seller) continue;
    manager_schedule_cpu_market_offer($player, $season, $seller, $minMatches, $maxMatches);
    break;
  }
  unset($player);
}

function manager_add_sold_player_to_market(array &$state, string $seller, array $sellerUser, array $player, int $price, int $now, bool $payoutSettled = true, ?array $season = null): array {
  if (!isset($state['managerMarket']) || !is_array($state['managerMarket'])) $state['managerMarket'] = [];
  ensure_player_stats($player);
  $sellerPlayerId = (string)($player['id'] ?? '');
  $team = has_team($sellerUser) ? user_team($sellerUser) : ['id' => $seller, 'name' => strtoupper($seller)];
  $division = (string)($sellerUser['managerFinance']['divisionName'] ?? manager_division_name((int)($sellerUser['managerDivisionLevel'] ?? 5)));
  $player['sourceTeam'] = (string)($team['name'] ?? strtoupper($seller));
  $player['sourceTeamId'] = (string)($team['id'] ?? $seller);
  $player['sourceDivision'] = $division;
  $player['marketListedByUser'] = $seller;
  $player['marketListedAt'] = $now;
  $player['marketSalePrice'] = $price;
  $player['marketPayoutSettled'] = $payoutSettled;
  $player['sellerPlayerId'] = $sellerPlayerId;
  $player['price'] = max(1000, $price);
  if (!$payoutSettled) manager_schedule_cpu_market_offer($player, $season, $seller, 1, 10);
  unset($player['soldTo'], $player['soldToId'], $player['soldToDivision'], $player['soldAt'], $player['injury'], $player['injuryReason'], $player['injuryRemaining'], $player['dailyForm']);

  $state['managerMarket'] = array_values(array_filter($state['managerMarket'], function ($marketPlayer) use ($seller, $sellerPlayerId) {
    if (!is_array($marketPlayer)) return false;
    if ((string)($marketPlayer['marketListedByUser'] ?? '') !== $seller) return true;
    if ($sellerPlayerId === '') return true;
    return (string)($marketPlayer['sellerPlayerId'] ?? $marketPlayer['id'] ?? '') !== $sellerPlayerId;
  }));

  $ids = [];
  foreach ($state['managerMarket'] as $marketPlayer) {
    $id = (string)($marketPlayer['id'] ?? '');
    if ($id !== '') $ids[$id] = true;
  }
  $baseId = (string)($player['id'] ?? '');
  if ($baseId === '') $baseId = 'S' . bin2hex(random_bytes(4));
  $id = $baseId;
  for ($i = 1; isset($ids[$id]); $i++) $id = $baseId . '_sale' . $i;
  $player['id'] = $id;

  array_unshift($state['managerMarket'], $player);
  return $player;
}

function manager_cpu_market_offer_exists(array $state, string $marketPlayerId): bool {
  if ($marketPlayerId === '') return true;
  foreach (($state['managerTransferOffers'] ?? []) as $offer) {
    if (!is_array($offer) || ($offer['status'] ?? 'pending') !== 'pending') continue;
    if ((string)($offer['marketPlayerId'] ?? '') !== $marketPlayerId) continue;
    if (is_cpu_manager((string)($offer['buyer'] ?? ''))) return true;
  }
  return false;
}

function manager_cpu_market_buyer(array $state, ?array $season, array $player, string $seller): ?array {
  $humanKeys = manager_human_team_keys($state, $season);
  $sellerLevel = 5;
  $candidates = [];
  if (is_array($season)) {
    $sellerParticipant = manager_participant($season, $seller);
    if (is_array($sellerParticipant)) $sellerLevel = clamp_int((int)($sellerParticipant['divisionLevel'] ?? 5), 1, 5);
    foreach (($season['participants'] ?? []) as $participant) {
      if (!is_array($participant) || !is_cpu_manager((string)($participant['user'] ?? ''))) continue;
      $team = is_array($participant['team'] ?? null) ? $participant['team'] : [];
      $teamKey = source_team_id((string)($team['sourceId'] ?? $team['id'] ?? ''));
      if ($teamKey !== '' && isset($humanKeys[$teamKey])) continue;
      $candidates[] = [
        'buyer' => (string)$participant['user'],
        'team' => $team,
        'divisionName' => (string)($participant['divisionName'] ?? manager_division_name((int)($participant['divisionLevel'] ?? $sellerLevel))),
      ];
    }
  }
  if (!$candidates) {
    $levels = array_values(array_unique([$sellerLevel, max(1, $sellerLevel - 1), min(5, $sellerLevel + 1), 5]));
    foreach ($levels as $level) {
      foreach (manager_division_teams((int)$level) as $team) {
        $teamKey = source_team_id((string)($team['sourceId'] ?? $team['id'] ?? ''));
        if ($teamKey !== '' && isset($humanKeys[$teamKey])) continue;
        $candidates[] = [
          'buyer' => manager_cpu_id((string)$team['id']),
          'team' => $team,
          'divisionName' => manager_division_name((int)$level),
        ];
      }
      if ($candidates) break;
    }
  }
  if (!$candidates) return null;
  return $candidates[array_rand($candidates)];
}

function manager_generate_cpu_market_offers(array &$state, ?array $season, int $now, string $sellerFilter = ''): int {
  if (!isset($state['managerTransferOffers']) || !is_array($state['managerTransferOffers'])) $state['managerTransferOffers'] = [];
  if (empty($state['managerMarket']) || !is_array($state['managerMarket'])) return 0;
  $created = 0;
  $sellerFilter = clean_name($sellerFilter);
  foreach ($state['managerMarket'] as &$player) {
    if (!is_array($player) || empty($player['marketListedByUser'])) continue;
    if (!empty($player['marketPayoutSettled'])) continue;
    $seller = clean_name((string)$player['marketListedByUser']);
    if ($seller === '' || ($sellerFilter !== '' && $seller !== $sellerFilter)) continue;
    if (!isset($state['users'][$seller]) || !has_team($state['users'][$seller])) continue;
    $marketPlayerId = (string)($player['id'] ?? '');
    if ($marketPlayerId === '' || manager_cpu_market_offer_exists($state, $marketPlayerId)) continue;
    if (!isset($player['marketCpuOfferAfterMatches'])) manager_schedule_cpu_market_offer($player, $season, $seller, 1, 10);
    $playedMatches = manager_user_played_match_count($season, $seller);
    if ($playedMatches < (int)($player['marketCpuOfferAfterMatches'] ?? 0)) continue;
    $basePrice = max(1000, (int)($player['price'] ?? $player['marketSalePrice'] ?? player_value($player)));
    $saleBase = manager_player_sale_base($player);
    $chance = manager_sale_chance($basePrice, $saleBase);
    if (mt_rand(1, 100) > $chance) {
      manager_log($state, 'cpu_market_transfer_no_offer', ['seller' => $seller, 'player' => $player['name'] ?? '', 'ask' => $basePrice, 'base' => $saleBase, 'chance' => $chance]);
      manager_schedule_cpu_market_offer($player, $season, $seller, 1, 4);
      continue;
    }
    $buyer = manager_cpu_market_buyer($state, $season, $player, $seller);
    if (!$buyer) continue;
    $cashOffer = manager_cpu_market_offer_cash($basePrice, $saleBase);
    $offerId = 'c' . bin2hex(random_bytes(6));
    $sellerTeam = user_team($state['users'][$seller]);
    $state['managerTransferOffers'][$offerId] = [
      'id' => $offerId,
      'status' => 'pending',
      'seasonId' => (string)($season['id'] ?? ''),
      'buyer' => (string)$buyer['buyer'],
      'seller' => $seller,
      'decisionUser' => $seller,
      'buyerTeam' => $buyer['team'],
      'sellerTeam' => $sellerTeam,
      'divisionName' => (string)($buyer['divisionName'] ?? $player['sourceDivision'] ?? ''),
      'marketPlayerId' => $marketPlayerId,
      'marketPayoutSettled' => !empty($player['marketPayoutSettled']),
      'playerId' => (string)($player['sellerPlayerId'] ?? $player['id'] ?? ''),
      'player' => $player,
      'basePrice' => $basePrice,
      'cashOffer' => $cashOffer,
      'offerPlayerId' => '',
      'offerPlayer' => null,
      'offerValue' => 0,
      'offerTotal' => $cashOffer,
      'cpuBuyer' => true,
      'created' => $now,
      'expires' => $now + 604800,
    ];
    $created++;
    manager_log($state, 'cpu_market_transfer_offer', ['offerId' => $offerId, 'buyer' => (string)$buyer['buyer'], 'seller' => $seller, 'player' => $player['name'] ?? '', 'cash' => $cashOffer, 'ask' => $basePrice]);
  }
  unset($player);
  return $created;
}

function manager_remove_market_listing(array &$state, string $user, string $playerId, int $now): array {
  $user = clean_name($user);
  $playerId = trim($playerId);
  if ($user === '' || $playerId === '') reply(['ok' => false, 'error' => 'bad_player'], 400);
  $season = current_manager_season($state, $user);
  $marketIndex = null;
  $marketPlayer = null;
  foreach (($state['managerMarket'] ?? []) as $index => $player) {
    if (!is_array($player) || (string)($player['id'] ?? '') !== $playerId) continue;
    if (clean_name((string)($player['marketListedByUser'] ?? '')) !== $user) reply(['ok' => false, 'error' => 'forbidden'], 403);
    $marketIndex = $index;
    $marketPlayer = $player;
    break;
  }
  if ($marketIndex === null || !is_array($marketPlayer)) reply(['ok' => false, 'error' => 'bad_player'], 404);
  if (!empty($marketPlayer['marketPayoutSettled'])) reply(['ok' => false, 'error' => 'listing_locked'], 409);
  array_splice($state['managerMarket'], (int)$marketIndex, 1);
  $removeOffers = [];
  foreach (($state['managerTransferOffers'] ?? []) as $offerId => $offer) {
    if (!is_array($offer) || ($offer['status'] ?? 'pending') !== 'pending') continue;
    if ((string)($offer['marketPlayerId'] ?? '') !== $playerId) continue;
    if (!is_cpu_manager((string)($offer['buyer'] ?? ''))) manager_add_transfer_notice($state, (string)($offer['buyer'] ?? ''), 'rejected', $offer, $now);
    $removeOffers[] = $offerId;
  }
  foreach ($removeOffers as $offerId) unset($state['managerTransferOffers'][$offerId]);
  manager_log($state, 'remove_market_listing', ['user' => $user, 'player' => $marketPlayer['name'] ?? '', 'playerId' => $playerId]);
  return [
    'status' => 'removed',
    'managerSeason' => public_manager_season($season, $state, $user),
    'managerFinance' => public_finance($state['users'][$user]),
    'managerSquad' => public_manager_context_squad($state, $user, $season),
    'managerMarket' => public_manager_market($state, $season),
    'managerTransferOffers' => public_manager_transfer_offers($state, $user),
  ];
}

function manager_transfer_offer_player(array $user, string $playerId): ?array {
  $playerId = trim($playerId);
  if ($playerId === '') return null;
  $found = find_squad_player($user['squad'] ?? [], $playerId);
  if (!$found) return null;
  $player = $found['player'];
  ensure_player_stats($player);
  $player['value'] = player_value($player);
  return ['group' => $found['group'], 'index' => $found['index'], 'player' => $player, 'value' => (int)$player['value']];
}

function manager_transfer_offer_player_ids(array $data, int $maxPlayers = 3): array {
  $raw = [];
  if (isset($data['offerPlayerIds']) && is_array($data['offerPlayerIds'])) {
    $raw = $data['offerPlayerIds'];
  }
  if (!$raw && isset($data['offerPlayerId'])) {
    $raw = [(string)$data['offerPlayerId']];
  }
  $ids = [];
  foreach ($raw as $id) {
    $id = trim((string)$id);
    if ($id === '' || in_array($id, $ids, true)) continue;
    $ids[] = $id;
  }
  if (count($ids) > $maxPlayers) reply(['ok' => false, 'error' => 'too_many_offer_players', 'maxPlayers' => $maxPlayers], 409);
  return $ids;
}

function manager_transfer_offer_players(array $user, array $playerIds): array {
  $players = [];
  foreach ($playerIds as $playerId) {
    $found = manager_transfer_offer_player($user, (string)$playerId);
    if (!$found) reply(['ok' => false, 'error' => 'bad_offer_player'], 404);
    $players[] = $found;
  }
  return $players;
}

function manager_transfer_offer_players_value(array $offerPlayers): int {
  $value = 0;
  foreach ($offerPlayers as $offerPlayer) $value += (int)($offerPlayer['value'] ?? 0);
  return $value;
}

function manager_ensure_browse_trade_room(array $squad, array $offerPlayers): void {
  $bench = manager_squad_bench_count($squad);
  $outgoing = count($offerPlayers);
  if ($outgoing > $bench) reply(['ok' => false, 'error' => 'no_replacement'], 409);
  $projected = max(0, $bench - $outgoing) + 1;
  if ($projected > MANAGER_MAX_BENCH_PLAYERS) {
    reply([
      'ok' => false,
      'error' => 'bench_full',
      'bench' => $bench,
      'projectedBench' => $projected,
      'maxBench' => MANAGER_MAX_BENCH_PLAYERS,
    ], 409);
  }
}

function manager_squad_bench_count(array $squad): int {
  return count(is_array($squad['bench'] ?? null) ? $squad['bench'] : []);
}

function manager_projected_bench_after_receive(array $squad, ?array $outgoingFound = null): int {
  $bench = manager_squad_bench_count($squad);
  if ($outgoingFound) {
    $group = (string)($outgoingFound['group'] ?? '');
    if ($group === 'bench') $bench--;
    elseif ($group === 'starters' && $bench > 0) $bench--;
  }
  return max(0, $bench) + 1;
}

function manager_ensure_bench_room_for_receive(array $squad, ?array $outgoingFound = null): void {
  $bench = manager_squad_bench_count($squad);
  $projected = manager_projected_bench_after_receive($squad, $outgoingFound);
  if ($projected > MANAGER_MAX_BENCH_PLAYERS) {
    reply([
      'ok' => false,
      'error' => 'bench_full',
      'bench' => $bench,
      'projectedBench' => $projected,
      'maxBench' => MANAGER_MAX_BENCH_PLAYERS,
    ], 409);
  }
}

function manager_take_transfer_offer_player(array &$user, string $playerId, string $toTeam, string $toTeamId, string $toDivision, int $now): ?array {
  $playerId = trim($playerId);
  if ($playerId === '') return null;
  ensure_user_squad($user);
  $found = find_squad_player($user['squad'] ?? [], $playerId);
  if (!$found) reply(['ok' => false, 'error' => 'bad_offer_player'], 404);
  $group = (string)$found['group'];
  $index = (int)$found['index'];
  if ($group === 'starters' && empty($user['squad']['bench'])) reply(['ok' => false, 'error' => 'no_replacement'], 409);
  $player = $found['player'];
  ensure_player_stats($player);
  $player['value'] = player_value($player);
  array_splice($user['squad'][$group], $index, 1);
  if ($group === 'starters') {
    $replacement = array_shift($user['squad']['bench']);
    if ($replacement) {
      $replacement['homeX'] = $player['homeX'] ?? $replacement['homeX'] ?? 0.5;
      $replacement['homeY'] = $player['homeY'] ?? $replacement['homeY'] ?? 0.5;
      $replacement['x'] = $replacement['homeX'];
      $replacement['y'] = $replacement['homeY'];
      array_splice($user['squad']['starters'], $index, 0, [$replacement]);
    }
  }
  $player['soldTo'] = $toTeam;
  $player['soldToId'] = $toTeamId;
  $player['soldToDivision'] = $toDivision;
  $player['soldAt'] = $now;
  ensure_squad_stats($user['squad']);
  return $player;
}

function manager_add_trade_player_to_target(array &$state, string $buyer, string $teamUser, array $player, int $now): void {
  if (!$player) return;
  $seasonId = (string)(current_manager_season_id($state, $buyer) ?? '');
  if ($seasonId === '' || empty($state['managerSeasons'][$seasonId]['participants'])) return;
  $buyerTeam = isset($state['users'][$buyer]) && has_team($state['users'][$buyer]) ? user_team($state['users'][$buyer]) : ['name' => strtoupper($buyer), 'id' => $buyer];
  foreach ($state['managerSeasons'][$seasonId]['participants'] as &$participant) {
    if ((string)($participant['user'] ?? '') !== $teamUser) continue;
    if (!isset($participant['squad']) || !is_array($participant['squad'])) $participant['squad'] = ['starters' => [], 'bench' => []];
    ensure_player_id_unique_for_squad($player, $participant['squad']);
    $player['sourceTeam'] = (string)($buyerTeam['name'] ?? strtoupper($buyer));
    $player['sourceTeamId'] = (string)($buyerTeam['id'] ?? $buyer);
    $player['sourceDivision'] = (string)($state['users'][$buyer]['managerFinance']['divisionName'] ?? manager_division_name((int)($state['users'][$buyer]['managerDivisionLevel'] ?? 5)));
    $player['purchasedFrom'] = $player['sourceTeam'];
    $player['purchasedFromId'] = $player['sourceTeamId'];
    $player['purchasedFromDivision'] = $player['sourceDivision'];
    $player['purchasedAt'] = $now;
    $player['purchasePrice'] = 0;
    unset($player['soldTo'], $player['soldToId'], $player['soldToDivision'], $player['soldAt']);
    $participant['squad']['bench'][] = $player;
    ensure_squad_stats($participant['squad']);
    $participant['rating'] = squad_skill_rating($participant['squad']);
    $participant['strength'] = squad_skill_points($participant['squad']);
    unset($participant);
    return;
  }
  unset($participant);
}

function swap_squad_players(array &$user, string $firstId, string $secondId): bool {
  if ($firstId === '' || $secondId === '' || $firstId === $secondId) return false;
  ensure_user_squad($user);
  $first = find_squad_player($user['squad'] ?? [], $firstId);
  $second = find_squad_player($user['squad'] ?? [], $secondId);
  if (!$first || !$second) return false;

  $a =& $user['squad'][$first['group']][$first['index']];
  $b =& $user['squad'][$second['group']][$second['index']];
  ensure_player_roles($a);
  ensure_player_roles($b);
  $aPos = [
    'slotRole' => $a['slotRole'] ?? ($a['role'] ?? null),
    'homeX' => $a['homeX'] ?? null,
    'homeY' => $a['homeY'] ?? null,
    'x' => $a['x'] ?? null,
    'y' => $a['y'] ?? null,
  ];
  $bPos = [
    'slotRole' => $b['slotRole'] ?? ($b['role'] ?? null),
    'homeX' => $b['homeX'] ?? null,
    'homeY' => $b['homeY'] ?? null,
    'x' => $b['x'] ?? null,
    'y' => $b['y'] ?? null,
  ];

  [$a, $b] = [$b, $a];

  foreach ($aPos as $key => $value) {
    if ($value !== null) $a[$key] = $value;
  }
  foreach ($bPos as $key => $value) {
    if ($value !== null) $b[$key] = $value;
  }
  $a['role'] = player_natural_role($a);
  $b['role'] = player_natural_role($b);
  unset($a, $b);
  ensure_squad_stats($user['squad']);
  return true;
}

function manager_player_goal_map(array $season, string $user): array {
  $goals = [];
  foreach (($season['scorers'] ?? []) as $scorer) {
    if ((string)($scorer['user'] ?? '') !== $user) continue;
    $goals[(string)($scorer['player'] ?? 'Player')] = (int)($scorer['goals'] ?? 0);
  }
  return $goals;
}

function public_manager_squad(array $squad, array $goals): array {
  $out = ['starters' => [], 'bench' => []];
  foreach (['starters', 'bench'] as $group) {
    foreach (($squad[$group] ?? []) as $player) {
      $player = public_player($player);
      $name = (string)($player['name'] ?? 'Player');
      $player['value'] = player_value($player);
      $player['wage'] = player_wage($player);
      $player['goals'] = (int)($goals[$name] ?? 0);
      $out[$group][] = $player;
    }
  }
  return $out;
}

function public_manager_team_details(array &$state, array $season, string $target): array {
  if (manager_participant($season, $target) === null) reply(['ok' => false, 'error' => 'bad_team'], 404);
  $side = manager_side_data($state, $season, $target);
  $side = manager_apply_next_form_to_side_data($season, $target, $side);
  $side['squad'] = manager_annotate_squad_injuries($side['squad'] ?? ['starters' => [], 'bench' => []], $season, $target);
  $side['squad'] = manager_annotate_squad_cards($side['squad'], $season, $target, $state);
  $goals = manager_player_goal_map($season, $target);
  return [
    'id' => $target,
    'team' => $side['team'],
    'trophies' => public_team_trophies($state, $side['team'], manager_trophy_season_ids($state, $season)),
    'managerType' => !empty($side['human']) ? 'human' : 'cpu',
    'rating' => (int)$side['rating'],
    'rawStrength' => (int)$side['strength'],
    'strength' => manager_display_skill((int)$side['strength']),
	    'formation' => valid_manager_formation((string)($side['formation'] ?? ((!is_cpu_manager($target) && isset($state['users'][$target])) ? manager_user_formation($state['users'][$target]) : '4-4-2'))),
	    'formationOptions' => manager_formation_options(),
	    'dailyForm' => public_daily_form_summary($side['squad'], $side['dailyForm'] ?? []),
	    'squad' => public_manager_squad($side['squad'], $goals),
	  ];
	}

function public_manager_divisions(?array $state = null, ?array $season = null): array {
  $out = [];
  $humanByLevel = [];
  $humanTeamKeys = ($state !== null) ? manager_human_team_keys($state, $season) : [];
  if ($state !== null && $season !== null) {
    foreach (($season['humanManagers'] ?? []) as $name) {
      $name = clean_name((string)$name);
      if ($name === '' || !isset($state['users'][$name]) || !has_team($state['users'][$name])) continue;
      ensure_user_squad($state['users'][$name]);
      $team = user_team($state['users'][$name]);
      $squad = $state['users'][$name]['squad'] ?? ['starters' => [], 'bench' => []];
      ensure_squad_stats($squad);
      $participant = manager_participant($season, $name);
      $level = clamp_int((int)($participant['divisionLevel'] ?? $state['users'][$name]['managerDivisionLevel'] ?? 5), 1, 5);
      $humanByLevel[$level][] = [
        'id' => $name,
        'managerType' => 'human',
        'team' => $team,
        'rating' => squad_skill_rating($squad),
        'rawStrength' => squad_skill_points($squad),
        'strength' => manager_display_skill(squad_skill_points($squad)),
      ];
    }
  }
  for ($level = 1; $level <= 5; $level++) {
    $teams = [];
    foreach (($humanByLevel[$level] ?? []) as $humanTeam) $teams[] = $humanTeam;
    foreach (manager_division_teams($level) as $team) {
      $teamKey = source_team_id((string)($team['sourceId'] ?? $team['id'] ?? ''));
      if ($teamKey !== '' && isset($humanTeamKeys[$teamKey])) continue;
      $participant = cpu_manager_participant($team, $level);
      $rawStrength = (int)$participant['strength'];
      $teams[] = [
        'id' => (string)$participant['user'],
        'managerType' => 'cpu',
        'team' => $participant['team'],
        'rating' => (int)$participant['rating'],
        'rawStrength' => $rawStrength,
        'strength' => manager_display_skill($rawStrength),
      ];
    }
    usort($teams, fn($a, $b) => ((int)$b['rawStrength']) <=> ((int)$a['rawStrength']));
    $out[] = [
      'level' => $level,
      'id' => manager_division_id($level),
      'name' => manager_division_name($level),
      'teams' => $teams,
    ];
  }
  return $out;
}

function public_manager_division_team_details(string $target, ?array $state = null): array {
  for ($level = 1; $level <= 5; $level++) {
    foreach (manager_division_teams($level) as $team) {
      $participant = cpu_manager_participant($team, $level);
      if ((string)$participant['user'] !== $target) continue;
      $rawStrength = (int)$participant['strength'];
      return [
        'id' => $target,
        'team' => $participant['team'],
        'trophies' => $state !== null ? public_team_trophies($state, $participant['team']) : [],
        'managerType' => 'cpu',
        'divisionName' => manager_division_name($level),
        'rating' => (int)$participant['rating'],
        'rawStrength' => $rawStrength,
        'strength' => manager_display_skill($rawStrength),
        'formation' => valid_manager_formation((string)($participant['formation'] ?? '4-4-2')),
        'formationOptions' => manager_formation_options(),
        'squad' => public_manager_squad($participant['squad'], []),
      ];
    }
  }
  reply(['ok' => false, 'error' => 'bad_team'], 404);
}

function manager_browse_transfer_key(string $teamUser, string $playerId): string {
  return $teamUser . ':' . $playerId;
}

function manager_browse_team_level(string $teamUser): int {
  for ($level = 1; $level <= 5; $level++) {
    foreach (manager_division_teams($level) as $team) {
      if (manager_cpu_id((string)$team['id']) === $teamUser) return $level;
    }
  }
  return 5;
}

function manager_browse_team_details(array &$state, string $target, string $user = ''): array {
  $season = current_manager_season($state, $user);
  if (!is_cpu_manager($target) && (!$season || !in_array($target, $season['humanManagers'] ?? [], true) || $target === $user)) {
    reply(['ok' => false, 'error' => 'human_transfer_locked'], 409);
  }
  $participant = $season ? manager_participant($season, $target) : null;
  if ($participant !== null) {
    $details = public_manager_team_details($state, $season, $target);
    $details['divisionName'] = (string)($participant['divisionName'] ?? manager_division_name((int)($participant['divisionLevel'] ?? 5)));
  } else {
    $details = public_manager_division_team_details($target, $state);
  }

  $team = $details['team'] ?? [];
  $divisionName = (string)($details['divisionName'] ?? manager_division_name(manager_browse_team_level($target)));
  $sold = is_array($state['managerBrowseSold'] ?? null) ? $state['managerBrowseSold'] : [];
  $out = ['starters' => [], 'bench' => []];
  foreach (['starters', 'bench'] as $group) {
    foreach (($details['squad'][$group] ?? []) as $player) {
      $playerId = (string)($player['id'] ?? '');
      if ($playerId === '' || !empty($sold[manager_browse_transfer_key($target, $playerId)])) continue;
      $player = public_player($player);
      $player['sourceTeam'] = (string)($team['name'] ?? strtoupper($target));
      $player['sourceTeamId'] = $target;
      $player['sourceDivision'] = $divisionName;
      $player['value'] = player_value($player);
      $player['price'] = $player['value'];
      $out[$group][] = $player;
    }
  }
  $details['squad'] = $out;
  $details['divisionName'] = $divisionName;
  $details['rawStrength'] = squad_skill_points($out);
  $details['strength'] = manager_display_skill((int)$details['rawStrength']);
  $details['rating'] = squad_skill_rating($out);
  return $details;
}

function manager_browse_find_player(array $details, string $playerId): ?array {
  foreach (['starters', 'bench'] as $group) {
    foreach (($details['squad'][$group] ?? []) as $player) {
      if ((string)($player['id'] ?? '') === $playerId) return ['group' => $group, 'player' => $player];
    }
  }
  return null;
}

function public_manager_transfer_offer(array $offer, string $viewer): array {
  $player = is_array($offer['player'] ?? null) ? public_player($offer['player']) : [];
  $offerPlayer = is_array($offer['offerPlayer'] ?? null) ? public_player($offer['offerPlayer']) : null;
  return [
    'id' => (string)($offer['id'] ?? ''),
    'status' => (string)($offer['status'] ?? 'pending'),
    'direction' => clean_name((string)($offer['decisionUser'] ?? $offer['seller'] ?? '')) === clean_name($viewer) ? 'incoming' : 'outgoing',
    'decisionUser' => clean_name((string)($offer['decisionUser'] ?? $offer['seller'] ?? '')),
    'countered' => !empty($offer['countered']),
    'counterBy' => (string)($offer['counterBy'] ?? ''),
    'counterAt' => (int)($offer['counterAt'] ?? 0),
    'buyer' => (string)($offer['buyer'] ?? ''),
    'seller' => (string)($offer['seller'] ?? ''),
    'buyerTeam' => is_array($offer['buyerTeam'] ?? null) ? $offer['buyerTeam'] : [],
    'sellerTeam' => is_array($offer['sellerTeam'] ?? null) ? $offer['sellerTeam'] : [],
    'divisionName' => (string)($offer['divisionName'] ?? ''),
    'player' => $player,
    'basePrice' => (int)($offer['basePrice'] ?? 0),
    'cashOffer' => (int)($offer['cashOffer'] ?? 0),
    'offerPlayer' => $offerPlayer,
    'offerPlayerId' => (string)($offer['offerPlayerId'] ?? ''),
    'offerValue' => (int)($offer['offerValue'] ?? 0),
    'offerTotal' => (int)($offer['offerTotal'] ?? 0),
    'created' => (int)($offer['created'] ?? 0),
    'expires' => (int)($offer['expires'] ?? 0),
  ];
}

function manager_add_transfer_notice(array &$state, string $user, string $type, array $offer, int $now): void {
  $user = clean_name($user);
  if ($user === '') return;
  if (!isset($state['managerTransferNotices']) || !is_array($state['managerTransferNotices'])) $state['managerTransferNotices'] = [];
  $id = 'tn' . bin2hex(random_bytes(6));
  $state['managerTransferNotices'][$id] = [
    'id' => $id,
    'user' => $user,
    'type' => $type,
    'buyer' => (string)($offer['buyer'] ?? ''),
    'seller' => (string)($offer['seller'] ?? ''),
    'buyerTeam' => is_array($offer['buyerTeam'] ?? null) ? $offer['buyerTeam'] : [],
    'sellerTeam' => is_array($offer['sellerTeam'] ?? null) ? $offer['sellerTeam'] : [],
    'player' => is_array($offer['player'] ?? null) ? $offer['player'] : [],
    'cashOffer' => (int)($offer['cashOffer'] ?? 0),
    'offerPlayer' => is_array($offer['offerPlayer'] ?? null) ? $offer['offerPlayer'] : null,
    'offerValue' => (int)($offer['offerValue'] ?? 0),
    'offerTotal' => (int)($offer['offerTotal'] ?? 0),
    'created' => $now,
  ];
}

function public_manager_transfer_notice(array $notice): array {
  return [
    'id' => (string)($notice['id'] ?? ''),
    'type' => (string)($notice['type'] ?? ''),
    'buyer' => (string)($notice['buyer'] ?? ''),
    'seller' => (string)($notice['seller'] ?? ''),
    'buyerTeam' => is_array($notice['buyerTeam'] ?? null) ? $notice['buyerTeam'] : [],
    'sellerTeam' => is_array($notice['sellerTeam'] ?? null) ? $notice['sellerTeam'] : [],
    'player' => is_array($notice['player'] ?? null) ? public_player($notice['player']) : [],
    'cashOffer' => (int)($notice['cashOffer'] ?? 0),
    'offerPlayer' => is_array($notice['offerPlayer'] ?? null) ? public_player($notice['offerPlayer']) : null,
    'offerValue' => (int)($notice['offerValue'] ?? 0),
    'offerTotal' => (int)($notice['offerTotal'] ?? 0),
    'created' => (int)($notice['created'] ?? 0),
  ];
}

function public_manager_transfer_offers(array $state, string $user): array {
  $out = ['incoming' => [], 'outgoing' => [], 'notices' => []];
  $user = clean_name($user);
  foreach (($state['managerTransferOffers'] ?? []) as $offer) {
    if (!is_array($offer) || ($offer['status'] ?? 'pending') !== 'pending') continue;
    $buyer = clean_name((string)($offer['buyer'] ?? ''));
    $seller = clean_name((string)($offer['seller'] ?? ''));
    $decisionUser = clean_name((string)($offer['decisionUser'] ?? $seller));
    if ($decisionUser === '' || ($decisionUser !== $buyer && $decisionUser !== $seller)) $decisionUser = $seller;
    if ($user === $decisionUser) $out['incoming'][] = public_manager_transfer_offer($offer, $user);
    elseif ($user === $buyer || $user === $seller) $out['outgoing'][] = public_manager_transfer_offer($offer, $user);
  }
  foreach (($state['managerTransferNotices'] ?? []) as $notice) {
    if (!is_array($notice) || (string)($notice['user'] ?? '') !== $user) continue;
    $out['notices'][] = public_manager_transfer_notice($notice);
  }
  usort($out['incoming'], fn($a, $b) => (int)($b['created'] ?? 0) <=> (int)($a['created'] ?? 0));
  usort($out['outgoing'], fn($a, $b) => (int)($b['created'] ?? 0) <=> (int)($a['created'] ?? 0));
  usort($out['notices'], fn($a, $b) => (int)($b['created'] ?? 0) <=> (int)($a['created'] ?? 0));
  return $out;
}

function manager_add_purchased_player_to_user(array &$user, array $player, string $fromTeam, string $fromTeamId, string $fromDivision, int $cashPrice, int $now, ?array $outgoingFound = null): array {
  ensure_user_squad($user);
  manager_ensure_bench_room_for_receive($user['squad'] ?? [], $outgoingFound);
  ensure_player_id_unique_for_squad($player, $user['squad'] ?? []);
  $player['sourceTeam'] = $fromTeam;
  $player['sourceTeamId'] = $fromTeamId;
  $player['sourceDivision'] = $fromDivision;
  $player['purchasedFrom'] = $fromTeam;
  $player['purchasedFromId'] = $fromTeamId;
  $player['purchasedFromDivision'] = $fromDivision;
  $player['purchasedAt'] = $now;
  $player['purchasePrice'] = $cashPrice;
  unset(
    $player['price'],
    $player['soldTo'],
    $player['soldToId'],
    $player['soldToDivision'],
    $player['soldAt'],
    $player['marketListedByUser'],
    $player['marketListedAt'],
    $player['marketSalePrice'],
    $player['marketPayoutSettled'],
    $player['sellerPlayerId']
  );
  $user['squad']['bench'][] = $player;
  ensure_squad_stats($user['squad']);
  return $player;
}

function manager_find_user_squad_player(array $user, string $playerId): ?array {
  ensure_user_squad($user);
  return find_squad_player($user['squad'] ?? [], $playerId);
}

function manager_create_human_transfer_offer(array &$state, array $season, string $buyer, string $seller, array $details, array $player, array $data, int $now): array {
  if ($buyer === $seller || !isset($state['users'][$seller]) || !has_team($state['users'][$seller])) reply(['ok' => false, 'error' => 'human_transfer_locked'], 409);
  ensure_user_squad($state['users'][$buyer]);
  ensure_user_squad($state['users'][$seller]);
  ensure_manager_finance($state['users'][$buyer]);
  $team = $details['team'] ?? user_team($state['users'][$seller]);
  $buyerTeam = user_team($state['users'][$buyer]);
  $divisionName = (string)($details['divisionName'] ?? $season['divisionName'] ?? '');
  $basePrice = (int)($player['price'] ?? $player['value'] ?? player_value($player));
  $cashOffer = manager_transfer_cash_offer($data, $basePrice);
  if ((int)$state['users'][$buyer]['managerFinance']['balance'] < $cashOffer) reply(['ok' => false, 'error' => 'no_money'], 409);
  $offerPlayerId = trim((string)($data['offerPlayerId'] ?? ''));
  $offerPlayer = manager_transfer_offer_player($state['users'][$buyer], $offerPlayerId);
  if ($offerPlayerId !== '' && !$offerPlayer) reply(['ok' => false, 'error' => 'bad_offer_player'], 404);
  manager_ensure_bench_room_for_receive($state['users'][$buyer]['squad'] ?? [], $offerPlayer);
  $offerValue = $offerPlayer ? (int)$offerPlayer['value'] : 0;
  $offerId = 'h' . bin2hex(random_bytes(6));
  if (!isset($state['managerTransferOffers']) || !is_array($state['managerTransferOffers'])) $state['managerTransferOffers'] = [];
  $state['managerTransferOffers'][$offerId] = [
    'id' => $offerId,
    'status' => 'pending',
	    'seasonId' => (string)($season['id'] ?? ''),
	    'buyer' => $buyer,
	    'seller' => $seller,
	    'decisionUser' => $seller,
	    'buyerTeam' => $buyerTeam,
	    'sellerTeam' => $team,
    'divisionName' => $divisionName,
    'playerId' => (string)($player['id'] ?? ''),
    'player' => $player,
    'basePrice' => $basePrice,
    'cashOffer' => $cashOffer,
    'offerPlayerId' => $offerPlayerId,
    'offerPlayer' => $offerPlayer ? $offerPlayer['player'] : null,
    'offerValue' => $offerValue,
    'offerTotal' => $cashOffer + $offerValue,
    'created' => $now,
    'expires' => $now + 604800,
  ];
  manager_log($state, 'human_transfer_offer', ['offerId' => $offerId, 'buyer' => $buyer, 'seller' => $seller, 'player' => $player['name'] ?? '', 'cash' => $cashOffer, 'offerValue' => $offerValue]);
  return [
    'status' => 'human_offer',
    'offerId' => $offerId,
    'player' => public_player($player),
    'team' => $team,
    'basePrice' => $basePrice,
    'cashPrice' => $cashOffer,
    'offerValue' => $offerValue,
    'offerTotal' => $cashOffer + $offerValue,
    'managerTransferOffers' => public_manager_transfer_offers($state, $buyer),
    'message' => 'Offer sent. Waiting for manager approval.',
  ];
}

function manager_create_market_transfer_offer(array &$state, ?array $season, string $buyer, array $player, array $data, int $now): array {
  $seller = clean_name((string)($player['marketListedByUser'] ?? ''));
  if ($seller === '' || $buyer === $seller || !isset($state['users'][$seller]) || !has_team($state['users'][$seller])) reply(['ok' => false, 'error' => 'human_transfer_locked'], 409);
  ensure_user_squad($state['users'][$buyer]);
  ensure_user_squad($state['users'][$seller]);
  ensure_manager_finance($state['users'][$buyer]);
  $sellerTeam = has_team($state['users'][$seller]) ? user_team($state['users'][$seller]) : ['id' => $seller, 'name' => strtoupper($seller)];
  $buyerTeam = user_team($state['users'][$buyer]);
  $divisionName = (string)($player['sourceDivision'] ?? $season['divisionName'] ?? '');
  $basePrice = (int)($player['price'] ?? $player['marketSalePrice'] ?? $player['value'] ?? player_value($player));
  $cashOffer = manager_transfer_cash_offer($data, $basePrice);
  if ((int)$state['users'][$buyer]['managerFinance']['balance'] < $cashOffer) reply(['ok' => false, 'error' => 'no_money'], 409);
  $offerPlayerId = trim((string)($data['offerPlayerId'] ?? ''));
  $offerPlayer = manager_transfer_offer_player($state['users'][$buyer], $offerPlayerId);
  if ($offerPlayerId !== '' && !$offerPlayer) reply(['ok' => false, 'error' => 'bad_offer_player'], 404);
  manager_ensure_bench_room_for_receive($state['users'][$buyer]['squad'] ?? [], $offerPlayer);
  $offerValue = $offerPlayer ? (int)$offerPlayer['value'] : 0;
  $offerId = 'm' . bin2hex(random_bytes(6));
  $payoutSettled = array_key_exists('marketPayoutSettled', $player) ? !empty($player['marketPayoutSettled']) : true;
  if (!isset($state['managerTransferOffers']) || !is_array($state['managerTransferOffers'])) $state['managerTransferOffers'] = [];
  $state['managerTransferOffers'][$offerId] = [
    'id' => $offerId,
    'status' => 'pending',
    'seasonId' => (string)($season['id'] ?? ''),
    'buyer' => $buyer,
    'seller' => $seller,
    'decisionUser' => $seller,
    'buyerTeam' => $buyerTeam,
    'sellerTeam' => $sellerTeam,
    'divisionName' => $divisionName,
    'marketPlayerId' => (string)($player['id'] ?? ''),
    'marketPayoutSettled' => $payoutSettled,
    'playerId' => (string)($player['sellerPlayerId'] ?? $player['id'] ?? ''),
    'player' => $player,
    'basePrice' => $basePrice,
    'cashOffer' => $cashOffer,
    'offerPlayerId' => $offerPlayerId,
    'offerPlayer' => $offerPlayer ? $offerPlayer['player'] : null,
    'offerValue' => $offerValue,
    'offerTotal' => $cashOffer + $offerValue,
    'created' => $now,
    'expires' => $now + 604800,
  ];
  manager_log($state, 'market_human_transfer_offer', ['offerId' => $offerId, 'buyer' => $buyer, 'seller' => $seller, 'player' => $player['name'] ?? '', 'cash' => $cashOffer, 'offerValue' => $offerValue]);
  return [
    'status' => 'human_offer',
    'offerId' => $offerId,
    'player' => public_player($player),
    'team' => $sellerTeam,
    'basePrice' => $basePrice,
    'cashPrice' => $cashOffer,
    'offerValue' => $offerValue,
    'offerTotal' => $cashOffer + $offerValue,
    'managerFinance' => public_finance($state['users'][$buyer]),
    'managerMarket' => public_manager_market($state, $season),
    'managerTransferOffers' => public_manager_transfer_offers($state, $buyer),
    'message' => 'Offer sent. Waiting for manager approval.',
  ];
}

function manager_accept_cpu_market_offer(array &$state, string $user, string $offerId, int $now): array {
  $user = clean_name($user);
  if ($offerId === '' || empty($state['managerTransferOffers'][$offerId]) || !is_array($state['managerTransferOffers'][$offerId])) reply(['ok' => false, 'error' => 'bad_offer'], 404);
  $offer = $state['managerTransferOffers'][$offerId];
  $seller = clean_name((string)($offer['seller'] ?? ''));
  $buyer = clean_name((string)($offer['buyer'] ?? ''));
  if ($user === '' || $seller !== $user || !is_cpu_manager($buyer)) reply(['ok' => false, 'error' => 'bad_offer'], 404);
  if (!isset($state['users'][$seller])) reply(['ok' => false, 'error' => 'bad_offer'], 404);
  ensure_user_squad($state['users'][$seller]);
  ensure_manager_finance($state['users'][$seller]);

  $marketPlayerId = (string)($offer['marketPlayerId'] ?? '');
  $marketIndex = null;
  $marketPlayer = null;
  foreach (($state['managerMarket'] ?? []) as $index => $candidate) {
    if (!is_array($candidate) || (string)($candidate['id'] ?? '') !== $marketPlayerId) continue;
    if (clean_name((string)($candidate['marketListedByUser'] ?? '')) !== $seller) continue;
    $marketIndex = $index;
    $marketPlayer = $candidate;
    break;
  }
  if ($marketIndex === null || !is_array($marketPlayer)) reply(['ok' => false, 'error' => 'bad_player'], 404);

  $cash = max(0, (int)($offer['cashOffer'] ?? 0));
  $payoutSettled = array_key_exists('marketPayoutSettled', $offer) ? !empty($offer['marketPayoutSettled']) : !empty($marketPlayer['marketPayoutSettled']);
  $sellerPlayerId = (string)($marketPlayer['sellerPlayerId'] ?? $offer['playerId'] ?? '');
  $sellerFound = null;
  if (!$payoutSettled) {
    if ($sellerPlayerId === '') reply(['ok' => false, 'error' => 'bad_player'], 404);
    $sellerFound = manager_find_user_squad_player($state['users'][$seller], $sellerPlayerId);
    if (!$sellerFound) reply(['ok' => false, 'error' => 'bad_player'], 404);
    if (($sellerFound['group'] ?? '') === 'starters' && empty($state['users'][$seller]['squad']['bench'])) reply(['ok' => false, 'error' => 'no_replacement'], 409);
  }

  $buyerTeam = is_array($offer['buyerTeam'] ?? null) ? $offer['buyerTeam'] : ['id' => $buyer, 'name' => strtoupper($buyer)];
  $divisionName = (string)($offer['divisionName'] ?? $marketPlayer['sourceDivision'] ?? '');
  $soldPlayer = $sellerFound
    ? manager_take_transfer_offer_player($state['users'][$seller], $sellerPlayerId, (string)($buyerTeam['name'] ?? strtoupper($buyer)), $buyer, $divisionName, $now)
    : $marketPlayer;
  if (!$payoutSettled) {
    $state['users'][$seller]['managerFinance']['balance'] += $cash;
    $state['users'][$seller]['managerFinance']['transferIncome'] += $cash;
  }

  array_splice($state['managerMarket'], (int)$marketIndex, 1);
  $removeOffers = [];
  foreach (($state['managerTransferOffers'] ?? []) as $id => $other) {
    if (!is_array($other) || ($other['status'] ?? 'pending') !== 'pending') continue;
    if ((string)($other['marketPlayerId'] ?? '') !== $marketPlayerId) continue;
    if ($id !== $offerId && !is_cpu_manager((string)($other['buyer'] ?? ''))) {
      manager_add_transfer_notice($state, (string)($other['buyer'] ?? ''), 'rejected', $other, $now);
    }
    $removeOffers[] = $id;
  }
  foreach ($removeOffers as $id) unset($state['managerTransferOffers'][$id]);
  ensure_manager_market($state);

  manager_log($state, 'cpu_market_transfer_accept', ['offerId' => $offerId, 'buyer' => $buyer, 'seller' => $seller, 'player' => $soldPlayer['name'] ?? '', 'cash' => $cash]);
  $season = current_manager_season($state, $user);
  return [
    'status' => 'accepted',
    'managerSeason' => public_manager_season($season, $state, $user),
    'managerFinance' => public_finance($state['users'][$user]),
    'managerSquad' => public_manager_context_squad($state, $user, $season),
    'managerMarket' => public_manager_market($state, $season),
    'managerTransferOffers' => public_manager_transfer_offers($state, $user),
    'acceptedOffer' => public_player($soldPlayer),
    'cashPrice' => $cash,
    'offerValue' => 0,
  ];
}

function manager_transfer_offer_counter(array &$state, string $user, string $offerId, array $data, int $now): array {
  $user = clean_name($user);
  if ($offerId === '' || empty($state['managerTransferOffers'][$offerId]) || !is_array($state['managerTransferOffers'][$offerId])) reply(['ok' => false, 'error' => 'bad_offer'], 404);
  $offer =& $state['managerTransferOffers'][$offerId];
  if (($offer['status'] ?? 'pending') !== 'pending') reply(['ok' => false, 'error' => 'offer_closed'], 409);
  $seller = clean_name((string)($offer['seller'] ?? ''));
  $buyer = clean_name((string)($offer['buyer'] ?? ''));
  $decisionUser = clean_name((string)($offer['decisionUser'] ?? $seller));
  if ($user === '' || $user !== $seller || $decisionUser !== $seller) reply(['ok' => false, 'error' => 'forbidden'], 403);
  if ((int)($offer['expires'] ?? 0) > 0 && (int)$offer['expires'] < $now) {
    unset($state['managerTransferOffers'][$offerId]);
    unset($offer);
    reply(['ok' => false, 'error' => 'offer_expired'], 409);
  }
  $currentCash = (int)($offer['cashOffer'] ?? 0);
  $counterCash = manager_transfer_cash_offer($data, $currentCash);
  if ($counterCash <= $currentCash) reply(['ok' => false, 'error' => 'counter_too_low', 'cashOffer' => $currentCash], 409);
  $offerValue = (int)($offer['offerValue'] ?? 0);
  $offer['cashOffer'] = $counterCash;
  $offer['offerTotal'] = $counterCash + $offerValue;
  $offer['countered'] = true;
  $offer['counterBy'] = $user;
  $offer['counterAt'] = $now;
  $offer['counterCount'] = (int)($offer['counterCount'] ?? 0) + 1;
  if (is_cpu_manager($buyer) && (string)($offer['marketPlayerId'] ?? '') !== '') {
    $marketPlayerId = (string)($offer['marketPlayerId'] ?? '');
    $basePrice = max(1, (int)($offer['basePrice'] ?? $currentCash));
    $ratio = $counterCash / $basePrice;
    $chance = clamp_int((int)round(86 - (max(0, $ratio - 1.0) * 150)), 12, 92);
    manager_log($state, 'cpu_market_transfer_counter', ['offerId' => $offerId, 'buyer' => $buyer, 'seller' => $seller, 'player' => $offer['player']['name'] ?? '', 'cash' => $counterCash, 'chance' => $chance]);
    unset($offer);
    if (mt_rand(1, 100) <= $chance) return manager_accept_cpu_market_offer($state, $user, $offerId, $now);
    manager_reschedule_cpu_market_offer($state, current_manager_season($state, $user), $user, $marketPlayerId, 1, 4);
    unset($state['managerTransferOffers'][$offerId]);
    return [
      'status' => 'counter_rejected',
      'managerSeason' => public_manager_season(current_manager_season($state, $user), $state, $user),
      'managerFinance' => public_finance($state['users'][$user]),
      'managerSquad' => public_manager_context_squad($state, $user, current_manager_season($state, $user)),
      'managerMarket' => public_manager_market($state, current_manager_season($state, $user)),
      'managerTransferOffers' => public_manager_transfer_offers($state, $user),
    ];
  }
  $offer['decisionUser'] = $buyer;
  manager_log($state, 'human_transfer_counter', ['offerId' => $offerId, 'buyer' => $buyer, 'seller' => $seller, 'player' => $offer['player']['name'] ?? '', 'cash' => $counterCash, 'offerValue' => $offerValue]);
  unset($offer);
  return [
    'status' => 'countered',
    'managerTransferOffers' => public_manager_transfer_offers($state, $user),
  ];
}

function manager_transfer_offer_response(array &$state, string $user, string $offerId, string $decision, int $now): array {
  $user = clean_name($user);
  if ($offerId === '' || empty($state['managerTransferOffers'][$offerId]) || !is_array($state['managerTransferOffers'][$offerId])) reply(['ok' => false, 'error' => 'bad_offer'], 404);
  $offer =& $state['managerTransferOffers'][$offerId];
  if (($offer['status'] ?? 'pending') !== 'pending') reply(['ok' => false, 'error' => 'offer_closed'], 409);
  $buyer = clean_name((string)($offer['buyer'] ?? ''));
  $seller = clean_name((string)($offer['seller'] ?? ''));
  $decisionUser = clean_name((string)($offer['decisionUser'] ?? $seller));
  if ($decisionUser === '' || ($decisionUser !== $buyer && $decisionUser !== $seller)) $decisionUser = $seller;
  if ($user === '' || $decisionUser !== $user) reply(['ok' => false, 'error' => 'forbidden'], 403);
  if ((int)($offer['expires'] ?? 0) > 0 && (int)$offer['expires'] < $now) {
    unset($state['managerTransferOffers'][$offerId]);
    unset($offer);
    reply(['ok' => false, 'error' => 'offer_expired'], 409);
  }
  if ($decision !== 'accept') {
    $offerForNotice = $offer;
    $offer['status'] = 'rejected';
    $offer['decidedAt'] = $now;
    $offer['decidedBy'] = $user;
    $noticeUser = $user === $buyer ? $seller : $buyer;
    $noticeType = $user === $buyer ? 'counter_rejected' : 'rejected';
    if (!is_cpu_manager($noticeUser)) manager_add_transfer_notice($state, $noticeUser, $noticeType, $offerForNotice, $now);
    elseif ((string)($offerForNotice['marketPlayerId'] ?? '') !== '') manager_reschedule_cpu_market_offer($state, current_manager_season($state, $seller), $seller, (string)$offerForNotice['marketPlayerId'], 1, 4);
    unset($state['managerTransferOffers'][$offerId]);
    unset($offer);
    return ['managerTransferOffers' => public_manager_transfer_offers($state, $user), 'status' => 'rejected'];
  }

  if (is_cpu_manager($buyer) && (string)($offer['marketPlayerId'] ?? '') !== '') {
    unset($offer);
    return manager_accept_cpu_market_offer($state, $user, $offerId, $now);
  }

  if ($buyer === '' || $seller === '' || !isset($state['users'][$buyer], $state['users'][$seller])) reply(['ok' => false, 'error' => 'bad_offer'], 404);
  ensure_user_squad($state['users'][$buyer]);
  ensure_user_squad($state['users'][$seller]);
  ensure_manager_finance($state['users'][$buyer]);
  ensure_manager_finance($state['users'][$seller]);
  $cash = (int)($offer['cashOffer'] ?? 0);
  if ((int)$state['users'][$buyer]['managerFinance']['balance'] < $cash) reply(['ok' => false, 'error' => 'no_money'], 409);

  $marketPlayerId = (string)($offer['marketPlayerId'] ?? '');
  if ($marketPlayerId !== '') {
    $marketIndex = null;
    $marketPlayer = null;
    foreach (($state['managerMarket'] ?? []) as $index => $candidate) {
      if (!is_array($candidate) || (string)($candidate['id'] ?? '') !== $marketPlayerId) continue;
      if ((string)($candidate['marketListedByUser'] ?? '') !== $seller) continue;
      $marketIndex = $index;
      $marketPlayer = $candidate;
      break;
    }
    if ($marketIndex === null || !is_array($marketPlayer)) reply(['ok' => false, 'error' => 'bad_player'], 404);

    $offerPlayerId = (string)($offer['offerPlayerId'] ?? '');
    $buyerOfferFound = $offerPlayerId !== '' ? manager_find_user_squad_player($state['users'][$buyer], $offerPlayerId) : null;
    if ($offerPlayerId !== '' && !$buyerOfferFound) reply(['ok' => false, 'error' => 'bad_offer_player'], 404);

    $payoutSettled = array_key_exists('marketPayoutSettled', $offer) ? !empty($offer['marketPayoutSettled']) : (array_key_exists('marketPayoutSettled', $marketPlayer) ? !empty($marketPlayer['marketPayoutSettled']) : true);
    $sellerPlayerId = (string)($marketPlayer['sellerPlayerId'] ?? $offer['playerId'] ?? '');
    $sellerFound = null;
    if (!$payoutSettled && $sellerPlayerId !== '') {
      $sellerFound = manager_find_user_squad_player($state['users'][$seller], $sellerPlayerId);
      if (!$sellerFound) reply(['ok' => false, 'error' => 'bad_player'], 404);
      if (($sellerFound['group'] ?? '') === 'starters' && empty($state['users'][$seller]['squad']['bench']) && $offerPlayerId === '') reply(['ok' => false, 'error' => 'no_replacement'], 409);
    }
    manager_ensure_bench_room_for_receive($state['users'][$buyer]['squad'] ?? [], $buyerOfferFound);
    if ($offerPlayerId !== '') manager_ensure_bench_room_for_receive($state['users'][$seller]['squad'] ?? [], $sellerFound);

    $buyerBefore = $state['users'][$buyer]['squad'] ?? ['starters' => [], 'bench' => []];
    $sellerTeam = user_team($state['users'][$seller]);
    $buyerTeam = user_team($state['users'][$buyer]);
    $divisionName = (string)($offer['divisionName'] ?? $marketPlayer['sourceDivision'] ?? '');
    $fromTeam = (string)($marketPlayer['sourceTeam'] ?? $sellerTeam['name'] ?? strtoupper($seller));
    $salePlayer = $sellerFound ? $sellerFound['player'] : $marketPlayer;
    $boughtPlayer = manager_add_purchased_player_to_user($state['users'][$buyer], $salePlayer, $fromTeam, $seller, $divisionName, $cash, $now, $buyerOfferFound);

    $tradedPlayer = null;
    if ($offerPlayerId !== '') {
      $tradedPlayer = manager_take_transfer_offer_player($state['users'][$buyer], $offerPlayerId, (string)($sellerTeam['name'] ?? strtoupper($seller)), $seller, $divisionName, $now);
      if ($tradedPlayer) {
        manager_add_purchased_player_to_user($state['users'][$seller], $tradedPlayer, (string)($buyerTeam['name'] ?? strtoupper($buyer)), $buyer, (string)($state['users'][$buyer]['managerFinance']['divisionName'] ?? ''), 0, $now, $sellerFound);
      }
    }
    if ($sellerFound) {
      manager_take_transfer_offer_player($state['users'][$seller], $sellerPlayerId, (string)($buyerTeam['name'] ?? strtoupper($buyer)), $buyer, (string)($state['users'][$buyer]['managerFinance']['divisionName'] ?? ''), $now);
    }

    $state['users'][$buyer]['managerFinance']['balance'] -= $cash;
    $state['users'][$buyer]['managerFinance']['transferSpent'] += $cash;
    if (!$payoutSettled) {
      $state['users'][$seller]['managerFinance']['balance'] += $cash;
      $state['users'][$seller]['managerFinance']['transferIncome'] += $cash;
    }

    array_splice($state['managerMarket'], (int)$marketIndex, 1);
    ensure_manager_market($state);

    $offer['status'] = 'accepted';
    $offer['decidedAt'] = $now;
    $offer['decidedBy'] = $user;
    $offerForNotice = $offer;
    manager_add_transfer_notice($state, $user === $buyer ? $seller : $buyer, $user === $buyer ? 'counter_accepted' : 'accepted', $offerForNotice, $now);
    foreach (($state['managerTransferOffers'] ?? []) as $id => &$other) {
      if ($id === $offerId || !is_array($other) || ($other['status'] ?? 'pending') !== 'pending') continue;
      if ((string)($other['marketPlayerId'] ?? '') === $marketPlayerId) {
        if (!is_cpu_manager((string)($other['buyer'] ?? ''))) manager_add_transfer_notice($state, (string)($other['buyer'] ?? ''), 'rejected', $other, $now);
        $other['status'] = 'superseded';
        $other['decidedAt'] = $now;
      }
    }
    unset($other, $offer);
    unset($state['managerTransferOffers'][$offerId]);

    manager_log($state, 'market_human_transfer_accept', ['offerId' => $offerId, 'buyer' => $buyer, 'seller' => $seller, 'player' => $boughtPlayer['name'] ?? '', 'cash' => $cash]);
    return [
      'status' => 'accepted',
      'managerSeason' => public_manager_season(current_manager_season($state, $user), $state, $user),
      'managerFinance' => public_finance($state['users'][$user]),
      'managerSquad' => public_manager_context_squad($state, $user, current_manager_season($state, $user)),
      'managerMarket' => public_manager_market($state, current_manager_season($state, $user)),
      'managerTransferOffers' => public_manager_transfer_offers($state, $user),
      'acceptedOffer' => public_player($boughtPlayer),
      'cashPrice' => $cash,
      'offerValue' => $tradedPlayer ? (int)($tradedPlayer['value'] ?? player_value($tradedPlayer)) : 0,
      'skillChange' => $buyer === $user ? manager_browse_skill_change($buyerBefore, $state['users'][$buyer]['squad'] ?? []) : null,
    ];
  }

  $sellerPlayerId = (string)($offer['playerId'] ?? $offer['player']['id'] ?? '');
  $sellerFound = manager_find_user_squad_player($state['users'][$seller], $sellerPlayerId);
  if (!$sellerFound) reply(['ok' => false, 'error' => 'bad_player'], 404);
  $offerPlayerId = (string)($offer['offerPlayerId'] ?? '');
  $buyerOfferFound = $offerPlayerId !== '' ? manager_find_user_squad_player($state['users'][$buyer], $offerPlayerId) : null;
  if ($offerPlayerId !== '' && !$buyerOfferFound) reply(['ok' => false, 'error' => 'bad_offer_player'], 404);
  if (($sellerFound['group'] ?? '') === 'starters' && empty($state['users'][$seller]['squad']['bench']) && $offerPlayerId === '') reply(['ok' => false, 'error' => 'no_replacement'], 409);
  manager_ensure_bench_room_for_receive($state['users'][$buyer]['squad'] ?? [], $buyerOfferFound);
  if ($offerPlayerId !== '') manager_ensure_bench_room_for_receive($state['users'][$seller]['squad'] ?? [], $sellerFound);

  $buyerBefore = $state['users'][$buyer]['squad'] ?? ['starters' => [], 'bench' => []];
  $sellerTeam = user_team($state['users'][$seller]);
  $buyerTeam = user_team($state['users'][$buyer]);
  $divisionName = (string)($offer['divisionName'] ?? '');

  $sellerPlayer = $sellerFound['player'];
  $boughtPlayer = manager_add_purchased_player_to_user($state['users'][$buyer], $sellerPlayer, (string)($sellerTeam['name'] ?? strtoupper($seller)), $seller, $divisionName, $cash, $now, $buyerOfferFound);

  $tradedPlayer = null;
  if ($offerPlayerId !== '') {
    $tradedPlayer = manager_take_transfer_offer_player($state['users'][$buyer], $offerPlayerId, (string)($sellerTeam['name'] ?? strtoupper($seller)), $seller, $divisionName, $now);
    if ($tradedPlayer) {
      manager_add_purchased_player_to_user($state['users'][$seller], $tradedPlayer, (string)($buyerTeam['name'] ?? strtoupper($buyer)), $buyer, (string)($state['users'][$buyer]['managerFinance']['divisionName'] ?? ''), 0, $now, $sellerFound);
    }
  }
  manager_take_transfer_offer_player($state['users'][$seller], $sellerPlayerId, (string)($buyerTeam['name'] ?? strtoupper($buyer)), $buyer, (string)($state['users'][$buyer]['managerFinance']['divisionName'] ?? ''), $now);

  $state['users'][$buyer]['managerFinance']['balance'] -= $cash;
  $state['users'][$buyer]['managerFinance']['transferSpent'] += $cash;
  $state['users'][$seller]['managerFinance']['balance'] += $cash;
  $state['users'][$seller]['managerFinance']['transferIncome'] += $cash;

  $offer['status'] = 'accepted';
  $offer['decidedAt'] = $now;
  $offer['decidedBy'] = $user;
  $offerForNotice = $offer;
  manager_add_transfer_notice($state, $user === $buyer ? $seller : $buyer, $user === $buyer ? 'counter_accepted' : 'accepted', $offerForNotice, $now);
  foreach (($state['managerTransferOffers'] ?? []) as $id => &$other) {
    if ($id === $offerId || !is_array($other) || ($other['status'] ?? 'pending') !== 'pending') continue;
    if ((string)($other['seller'] ?? '') === $seller && (string)($other['playerId'] ?? '') === $sellerPlayerId) {
      manager_add_transfer_notice($state, (string)($other['buyer'] ?? ''), 'rejected', $other, $now);
      $other['status'] = 'superseded';
      $other['decidedAt'] = $now;
    }
  }
  unset($other, $offer);
  unset($state['managerTransferOffers'][$offerId]);

  manager_log($state, 'human_transfer_accept', ['offerId' => $offerId, 'buyer' => $buyer, 'seller' => $seller, 'player' => $boughtPlayer['name'] ?? '', 'cash' => $cash]);
  return [
    'status' => 'accepted',
    'managerSeason' => public_manager_season(current_manager_season($state, $user), $state, $user),
    'managerFinance' => public_finance($state['users'][$user]),
    'managerSquad' => public_manager_context_squad($state, $user, isset($season) && is_array($season) ? $season : current_manager_season($state, $user)),
    'managerTransferOffers' => public_manager_transfer_offers($state, $user),
    'acceptedOffer' => public_player($boughtPlayer),
    'cashPrice' => $cash,
    'offerValue' => $tradedPlayer ? (int)($tradedPlayer['value'] ?? player_value($tradedPlayer)) : 0,
    'skillChange' => $buyer === $user ? manager_browse_skill_change($buyerBefore, $state['users'][$buyer]['squad'] ?? []) : null,
  ];
}

function next_transfer_player_id(array $squad): string {
  $used = [];
  foreach (array_merge($squad['starters'] ?? [], $squad['bench'] ?? []) as $player) {
    $used[(string)($player['id'] ?? '')] = true;
  }
  do {
    $id = 'T' . bin2hex(random_bytes(4));
  } while (isset($used[$id]));
  return $id;
}

function remove_manager_browse_player_from_current_season(array &$state, string $teamUser, string $playerId, string $user = ''): void {
  $seasonId = (string)(current_manager_season_id($state, $user) ?? '');
  if ($seasonId === '' || empty($state['managerSeasons'][$seasonId]['participants'])) return;
  foreach ($state['managerSeasons'][$seasonId]['participants'] as &$participant) {
    if ((string)($participant['user'] ?? '') !== $teamUser) continue;
    foreach (['starters', 'bench'] as $group) {
      if (empty($participant['squad'][$group]) || !is_array($participant['squad'][$group])) continue;
      foreach ($participant['squad'][$group] as $index => $player) {
        if ((string)($player['id'] ?? '') !== $playerId) continue;
        array_splice($participant['squad'][$group], $index, 1);
        if ($group === 'starters' && !empty($participant['squad']['bench']) && is_array($participant['squad']['bench'])) {
          $replacement = array_shift($participant['squad']['bench']);
          if ($replacement) {
            foreach (['homeX', 'homeY', 'x', 'y'] as $key) {
              if (isset($player[$key])) $replacement[$key] = $player[$key];
            }
            array_splice($participant['squad']['starters'], $index, 0, [$replacement]);
          }
        }
        ensure_squad_stats($participant['squad']);
        $participant['rating'] = squad_skill_rating($participant['squad']);
        $participant['strength'] = squad_skill_points($participant['squad']);
        unset($participant);
        return;
      }
    }
  }
  unset($participant);
}

function manager_browse_skill_change(array $beforeSquad, array $afterSquad): array {
  $rawBefore = squad_skill_points($beforeSquad);
  $rawAfter = squad_skill_points($afterSquad);
  $before = manager_display_skill($rawBefore);
  $after = manager_display_skill($rawAfter);
  return [
    'before' => $before,
    'after' => $after,
    'delta' => $after - $before,
    'rawBefore' => $rawBefore,
    'rawAfter' => $rawAfter,
    'rawDelta' => $rawAfter - $rawBefore,
  ];
}

function complete_manager_browse_purchase(array &$state, string $user, string $teamUser, array $team, string $divisionName, array $player, int $price, int $now, array $offerPlayerIds = []): array {
  ensure_user_squad($state['users'][$user]);
  ensure_manager_finance($state['users'][$user]);
  if ((int)$state['users'][$user]['managerFinance']['balance'] < $price) reply(['ok' => false, 'error' => 'no_money'], 409);

  $originalId = (string)($player['id'] ?? '');
  $soldKey = manager_browse_transfer_key($teamUser, $originalId);
  if (!empty($state['managerBrowseSold'][$soldKey])) reply(['ok' => false, 'error' => 'already_sold'], 409);
  $offerPlayerIds = array_values(array_unique(array_filter(array_map(fn($id) => trim((string)$id), $offerPlayerIds), fn($id) => $id !== '')));
  if (count($offerPlayerIds) > 3) reply(['ok' => false, 'error' => 'too_many_offer_players', 'maxPlayers' => 3], 409);
  $offerPlayers = manager_transfer_offer_players($state['users'][$user], $offerPlayerIds);
  manager_ensure_browse_trade_room($state['users'][$user]['squad'] ?? [], $offerPlayers);

  $beforeSquad = $state['users'][$user]['squad'] ?? ['starters' => [], 'bench' => []];
  $state['users'][$user]['managerFinance']['balance'] -= $price;
  $state['users'][$user]['managerFinance']['transferSpent'] += $price;
  $offeredPlayers = [];
  $offerValue = 0;
  foreach ($offerPlayerIds as $offerPlayerId) {
    $offeredPlayer = manager_take_transfer_offer_player($state['users'][$user], $offerPlayerId, (string)($team['name'] ?? 'TRANSFER BROWSE'), $teamUser, $divisionName, $now);
    if (!$offeredPlayer) continue;
    $playerValue = (int)($offeredPlayer['value'] ?? player_value($offeredPlayer));
    $offerValue += $playerValue;
    $offeredPlayers[] = $offeredPlayer;
  }
  remove_manager_browse_player_from_current_season($state, $teamUser, $originalId, $user);
  foreach ($offeredPlayers as $offeredPlayer) manager_add_trade_player_to_target($state, $user, $teamUser, $offeredPlayer, $now);

  ensure_player_id_unique_for_squad($player, $state['users'][$user]['squad'] ?? []);
  $player['sourceTeam'] = (string)($team['name'] ?? ($player['sourceTeam'] ?? 'TRANSFER BROWSE'));
  $player['sourceTeamId'] = $teamUser;
  $player['sourceDivision'] = $divisionName;
  $player['purchasedFrom'] = $player['sourceTeam'];
  $player['purchasedFromId'] = $teamUser;
  $player['purchasedFromDivision'] = $divisionName;
  $player['purchasedAt'] = $now;
  $player['purchasePrice'] = $price;
  unset($player['price']);
  $state['users'][$user]['squad']['bench'][] = $player;
  ensure_squad_stats($state['users'][$user]['squad']);

  if (!isset($state['managerBrowseSold']) || !is_array($state['managerBrowseSold'])) $state['managerBrowseSold'] = [];
	  $state['managerBrowseSold'][$soldKey] = [
    'teamUser' => $teamUser,
    'playerId' => $originalId,
    'buyer' => $user,
    'price' => $price,
    'offerValue' => $offerValue,
    'offerPlayer' => $offeredPlayers ? [
      'id' => (string)($offeredPlayers[0]['id'] ?? ''),
      'name' => (string)($offeredPlayers[0]['name'] ?? ''),
      'value' => (int)($offeredPlayers[0]['value'] ?? player_value($offeredPlayers[0])),
    ] : null,
    'offerPlayers' => array_map(fn($offeredPlayer) => [
      'id' => (string)($offeredPlayer['id'] ?? ''),
      'name' => (string)($offeredPlayer['name'] ?? ''),
      'value' => (int)($offeredPlayer['value'] ?? player_value($offeredPlayer)),
    ], $offeredPlayers),
    'at' => $now,
  ];
  $boughtPlayer = public_player($player);
  $boughtPlayer['value'] = player_value($boughtPlayer);
  return [
    'managerSeason' => public_manager_season(current_manager_season($state, $user), $state, $user),
    'managerFinance' => public_finance($state['users'][$user]),
    'managerSquad' => public_manager_context_squad($state, $user, isset($season) && is_array($season) ? $season : current_manager_season($state, $user)),
    'boughtPlayer' => $boughtPlayer,
    'cashPrice' => $price,
    'offerValue' => $offerValue,
    'offerPlayerCount' => count($offeredPlayers),
    'offerPlayers' => array_map(fn($offeredPlayer) => public_player($offeredPlayer), $offeredPlayers),
    'offerTotal' => $price + $offerValue,
    'skillChange' => manager_browse_skill_change($beforeSquad, $state['users'][$user]['squad'] ?? []),
  ];
}

function manager_browse_counter_price(int $basePrice, int $buyerLevel, int $targetLevel, int $rating): int {
  $gap = max(0, $buyerLevel - $targetLevel);
  $multiplier = mt_rand(118, 172) / 100 + $gap * 0.16 + max(0, $rating - 75) * 0.015;
  $multiplier = clamp_float($multiplier, 1.12, 3.25);
  return max($basePrice + 25000, (int)(round(($basePrice * $multiplier) / 25000) * 25000));
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

function add_manager_finance_for_result(array &$state, string $name, int $gf, int $ga, string $phase = 'league', bool $homeGame = true, array $season = []): void {
  if (!isset($state['users'][$name])) return;
  ensure_user_squad($state['users'][$name]);
  ensure_manager_finance($state['users'][$name]);
  $level = clamp_int((int)($state['users'][$name]['managerDivisionLevel'] ?? 5), 1, 5);
  $finance = $state['users'][$name]['managerFinance'] ?? [];
  $rank = $season ? manager_user_rank_in_season($season, $name) : 12;
  $teams = count($season['table'] ?? []) ?: 24;
  $breakdown = manager_match_income_breakdown($level, $gf, $ga, $phase, (int)($finance['arenaCapacity'] ?? manager_base_arena_capacity($level)), $rank, $teams, $homeGame);
  $income = (int)($breakdown['gateIncome'] ?? 0) + (int)($breakdown['merchIncome'] ?? 0) + (int)($breakdown['bonusIncome'] ?? 0) + (int)($breakdown['cupIncome'] ?? 0);
  $wages = squad_wages($state['users'][$name]['squad'] ?? []);
  foreach ($breakdown as $key => $amount) {
    if ($key === 'attendance') continue;
    $state['users'][$name]['managerFinance'][$key] = (int)($state['users'][$name]['managerFinance'][$key] ?? 0) + (int)$amount;
  }
  if ($homeGame) {
    $attendance = (int)($breakdown['attendance'] ?? 0);
    $state['users'][$name]['managerFinance']['arenaLastAttendance'] = $attendance;
    $state['users'][$name]['managerFinance']['arenaTotalAttendance'] = (int)($state['users'][$name]['managerFinance']['arenaTotalAttendance'] ?? 0) + $attendance;
    $state['users'][$name]['managerFinance']['arenaHomeMatches'] = (int)($state['users'][$name]['managerFinance']['arenaHomeMatches'] ?? 0) + 1;
  }
  $state['users'][$name]['managerFinance']['income'] += $income;
  $state['users'][$name]['managerFinance']['wages'] += $wages;
  $state['users'][$name]['managerFinance']['balance'] += $income - $wages;
}

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

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

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

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

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

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

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

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

function new_manager_watch_match(array &$state, string $user, string $seasonId, int $roundIndex, int $fixtureIndex, bool $register = true): array {
  $season =& $state['managerSeasons'][$seasonId];
  $fixture =& $season['rounds'][$roundIndex]['fixtures'][$fixtureIndex];
  $homeId = (string)$fixture['home'];
  $awayId = (string)$fixture['away'];
  $homeData = manager_side_data($state, $season, $homeId);
  $awayData = manager_side_data($state, $season, $awayId);
  manager_lock_fixture_form_for_side($season, $fixture, $homeId, $homeData, $roundIndex, $fixtureIndex);
  manager_lock_fixture_form_for_side($season, $fixture, $awayId, $awayData, $roundIndex, $fixtureIndex);
  $homeData = manager_apply_fixture_form_to_side_data($season, $fixture, $homeId, $homeData, $roundIndex, $fixtureIndex);
  $awayData = manager_apply_fixture_form_to_side_data($season, $fixture, $awayId, $awayData, $roundIndex, $fixtureIndex);
  $homeData = manager_apply_league_form_to_side_data($season, $homeId, $homeData);
  $awayData = manager_apply_league_form_to_side_data($season, $awayId, $awayData);
  $homeData['squad'] = manager_annotate_squad_injuries($homeData['squad'] ?? ['starters' => [], 'bench' => []], $season, $homeId);
  $awayData['squad'] = manager_annotate_squad_injuries($awayData['squad'] ?? ['starters' => [], 'bench' => []], $season, $awayId);
  $homeBench = array_slice(array_values(array_filter($homeData['squad']['bench'] ?? [], fn($player) => empty($player['injured']))), 0, MANAGER_MAX_BENCH_PLAYERS);
  $awayBench = array_slice(array_values(array_filter($awayData['squad']['bench'] ?? [], fn($player) => empty($player['injured']))), 0, MANAGER_MAX_BENCH_PLAYERS);
  $fixtureHumans = array_flip(manager_fixture_human_users($season, $fixture));
  $homePlayer = isset($fixtureHumans[$homeId]) ? $homeId : ($homeId === $user && $user !== '' ? $user : '__manager_cpu_home__');
  $awayPlayer = isset($fixtureHumans[$awayId]) ? $awayId : ($awayId === $user && $user !== '' ? $user : '__manager_cpu_away__');
  $match = [
    'id' => 'm' . bin2hex(random_bytes(5)),
    'status' => 'ready',
    'created' => time(),
    'updated' => time(),
    'lastTick' => microtime(true),
    'elapsed' => 0,
    'players' => ['home' => $homePlayer, 'away' => $awayPlayer],
    'teams' => ['home' => $homeData['team'], 'away' => $awayData['team']],
    'units' => ['home' => side_squad($homeData['squad']['starters'] ?? [], 'home'), 'away' => side_squad($awayData['squad']['starters'] ?? [], 'away')],
    'bench' => ['home' => side_squad($homeBench, 'home', 11), 'away' => side_squad($awayBench, 'away', 11)],
    'score' => [0, 0],
    'ball' => stopped_kickoff_ball(),
    'tactics' => ['home' => 'balanced', 'away' => 'balanced'],
    'engine' => default_engine((int)$homeData['rating'], (int)$awayData['rating']),
    'events' => [],
    'managerFixture' => ['seasonId' => $seasonId, 'roundIndex' => $roundIndex, 'fixtureIndex' => $fixtureIndex, 'home' => $homeId, 'away' => $awayId],
  ];
  reset_units($match);
  if (!$register) return $match;
  $fixture['status'] = 'watching';
  $fixture['matchId'] = $match['id'];
  $state['matches'][$match['id']] = $match;
  unset($fixture);
  return $state['matches'][$match['id']];
}

function tactic_power(string $mode): float {
  return match ($mode) {
    'defend' => 0.38,
    'attack' => 0.72,
    default => 0.55,
  };
}

function kickoff_ball(int $direction): array {
  $direction = $direction < 0 ? -1 : 1;
  return [
    'x' => 0.5,
    'y' => 0.5,
    'z' => 0.0,
    'vx' => mt_rand(-45, 45) / 1000,
    'vy' => $direction * (0.13 + mt_rand(0, 45) / 1000),
    'vz' => 0.0,
  ];
}

function stopped_ball_at(float $x, float $y): array {
  return [
    'x' => clamp_float($x, 0.02, 0.98),
    'y' => clamp_float($y, 0.01, 0.99),
    'z' => 0.0,
    'vx' => 0.0,
    'vy' => 0.0,
    'vz' => 0.0,
  ];
}

function stopped_kickoff_ball(): array {
  return stopped_ball_at(0.5, 0.5);
}

function ball_short_line_crossed(array $ball): bool {
  $y = (float)($ball['y'] ?? 0.5);
  return $y < -BALL_SHORT_LINE_OVER_MARGIN || $y > 1.0 + BALL_SHORT_LINE_OVER_MARGIN;
}

function ball_short_line_visible_out(array $ball): bool {
  $y = (float)($ball['y'] ?? 0.5);
  return $y < -BALL_SHORT_LINE_VISIBLE_MARGIN || $y > 1.0 + BALL_SHORT_LINE_VISIBLE_MARGIN;
}

function ball_short_line_stalled_out(array $ball): bool {
  if (!ball_short_line_crossed($ball)) return false;
  $speed = sqrt((float)($ball['vx'] ?? 0.0) ** 2 + (float)($ball['vy'] ?? 0.0) ** 2);
  return $speed < 0.018;
}

function distance2(array $a, array $b): float {
  $dx = (float)$a['x'] - (float)$b['x'];
  $dy = (float)$a['y'] - (float)$b['y'];
  return $dx * $dx + $dy * $dy;
}

function player_touch_control(array $player, bool $highBall = false): float {
  $control = $highBall
    ? player_stat($player, 'heading') * 0.55 + player_stat($player, 'positioning') * 0.25 + player_stat($player, 'strength') * 0.20
    : player_stat($player, 'passcontrol') * 0.48 + player_stat($player, 'shotcontrol') * 0.20 + player_stat($player, 'dribbling') * 0.18 + player_stat($player, 'positioning') * 0.14;
  if (($player['role'] ?? '') === 'GK') $control = max($control, player_stat($player, 'keeping'));
  return $control;
}

function player_touch_radius(array $player, bool $highBall = false): float {
  $isKeeper = ($player['role'] ?? '') === 'GK';
  $base = $isKeeper ? 0.021 : 0.017;
  $max = $isKeeper ? 0.046 : 0.038;
  return clamp_float($base + player_touch_control($player, $highBall) / 100 * 0.020, $base, $max);
}

function ball_touches_player_circle(array $ball, array $player, float $extra = 0.0): bool {
  $radius = player_touch_radius($player, (float)($ball['z'] ?? 0.0) > 0.08) + max(0.0, $extra);
  return distance2($player, $ball) <= $radius * $radius;
}

function player_ball_release_point(array $player, string $side, float $scale = 0.92): array {
  $radius = player_touch_radius($player, false);
  return [
    'x' => clamp_float((float)$player['x'], 0.02, 0.98),
    'y' => clamp_float((float)$player['y'] + attack_dir($side) * $radius * $scale, -0.02, 1.02),
  ];
}

function nearest_index(array $players, array $ball): ?int {
  $best = null;
  $bestDist = PHP_FLOAT_MAX;
  foreach ($players as $index => $player) {
    $dist = distance2($player, $ball);
    if ($dist < $bestDist) {
      $bestDist = $dist;
      $best = $index;
    }
  }
  return $best;
}

function role_zone(string $side, string $role): array {
  $zones = [
    'GK' => [0.88, 0.98],
    'RB' => [0.56, 0.92],
    'LB' => [0.56, 0.92],
    'CB' => [0.58, 0.94],
    'RM' => [0.31, 0.74],
    'LM' => [0.31, 0.74],
    'CM' => [0.30, 0.76],
    'ST' => [0.08, 0.56],
  ];
  [$min, $max] = $zones[$role] ?? [0.24, 0.78];
  if ($side === 'away') return [1.0 - $max, 1.0 - $min];
  return [$min, $max];
}

function role_zone_contains(string $side, string $role, float $y, float $margin = 0.0): bool {
  [$min, $max] = role_zone($side, $role);
  return $y >= $min - $margin && $y <= $max + $margin;
}

function clamp_role_y(string $side, string $role, float $y, float $margin = 0.0): float {
  [$min, $max] = role_zone($side, $role);
  return clamp_float($y, max(0.03, $min - $margin), min(0.97, $max + $margin));
}

function side_half_min(string $side): float {
  return $side === 'home' ? 0.535 : 0.03;
}

function side_half_max(string $side): float {
  return $side === 'home' ? 0.97 : 0.465;
}

function clamp_side_y(string $side, float $y): float {
  return clamp_float($y, side_half_min($side), side_half_max($side));
}

function keep_player_on_side(array &$player, string $side): void {
  $player['y'] = clamp_side_y($side, (float)$player['y']);
}

function nearest_role_index(array $players, array $ball, string $side): ?int {
  $best = null;
  $bestDist = PHP_FLOAT_MAX;
  $ballY = (float)$ball['y'];
  foreach ($players as $index => $player) {
    $role = (string)($player['role'] ?? 'CM');
    $margin = match ($role) {
      'GK' => 0.07,
      'CB', 'RB', 'LB' => 0.10 + player_stat($player, 'speed') / 900,
      'ST' => 0.12 + player_stat($player, 'speed') / 850,
      default => 0.14 + player_stat($player, 'speed') / 800,
    };
    if (!role_zone_contains($side, $role, $ballY, $margin)) continue;
    $dist = distance2($player, $ball);
    if ($dist < $bestDist) {
      $bestDist = $dist;
      $best = $index;
    }
  }
  return $best ?? nearest_index($players, $ball);
}

function move_towards(array &$player, float $targetX, float $targetY, float $speed, float $step): void {
  $dx = $targetX - (float)$player['x'];
  $dy = $targetY - (float)$player['y'];
  $dist = sqrt($dx * $dx + $dy * $dy);
  if ($dist <= 0.0001) {
    $player['vx'] = (float)($player['vx'] ?? 0.0) * 0.70;
    $player['vy'] = (float)($player['vy'] ?? 0.0) * 0.70;
    return;
  }
  if ($dist <= $speed * $step * 1.12) {
    $player['x'] = clamp_float($targetX, 0.03, 0.97);
    $player['y'] = clamp_float($targetY, 0.03, 0.97);
    $player['vx'] = 0.0;
    $player['vy'] = 0.0;
    return;
  }
  $desiredVx = $dx / $dist * $speed;
  $desiredVy = $dy / $dist * $speed;
  $blend = clamp_float(8.0 * $step, 0.12, 0.82);
  $vx = (float)($player['vx'] ?? 0.0);
  $vy = (float)($player['vy'] ?? 0.0);
  $vx += ($desiredVx - $vx) * $blend;
  $vy += ($desiredVy - $vy) * $blend;
  $currentSpeed = sqrt($vx * $vx + $vy * $vy);
  if ($currentSpeed > $speed) {
    $vx *= $speed / $currentSpeed;
    $vy *= $speed / $currentSpeed;
  }
  $move = min($dist, sqrt($vx * $vx + $vy * $vy) * $step);
  $moveDir = sqrt($vx * $vx + $vy * $vy);
  if ($moveDir <= 0.00001) {
    $vx = $dx / $dist * min($speed, $dist / max($step, 0.001));
    $vy = $dy / $dist * min($speed, $dist / max($step, 0.001));
    $moveDir = sqrt($vx * $vx + $vy * $vy);
    $move = min($dist, $moveDir * $step);
  }
  $player['x'] = clamp_float((float)$player['x'] + $vx / $moveDir * $move, 0.03, 0.97);
  $player['y'] = clamp_float((float)$player['y'] + $vy / $moveDir * $move, 0.03, 0.97);
  $damp = pow(0.18, $step);
  $player['vx'] = $vx * $damp;
  $player['vy'] = $vy * $damp;
}

function rand_float(): float {
  return mt_rand() / mt_getrandmax();
}

function player_index_by_id(array $players, ?string $id): ?int {
  if ($id === null) return null;
  foreach ($players as $index => $player) {
    if (($player['id'] ?? null) === $id) return $index;
  }
  return null;
}

function owner_ref(array $match): ?array {
  $owner = $match['engine']['owner'] ?? null;
  if (!is_array($owner)) return null;
  $side = $owner['side'] ?? '';
  if (!in_array($side, ['home', 'away'], true)) return null;
  $index = player_index_by_id($match['units'][$side] ?? [], $owner['id'] ?? null);
  return $index === null ? null : ['side' => $side, 'index' => $index];
}

function set_owner(array &$match, string $side, int $index): void {
  $player = $match['units'][$side][$index];
  $match['engine']['owner'] = ['side' => $side, 'id' => $player['id']];
  $match['engine']['passTarget'] = null;
  $match['engine']['shot'] = null;
  $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + 0.08;
}

function clear_owner(array &$match): void {
  $match['engine']['owner'] = null;
}

function attack_dir(string $side): int {
  return $side === 'home' ? -1 : 1;
}

function opponent_side(string $side): string {
  return $side === 'home' ? 'away' : 'home';
}

function side_team_name(array $match, string $side): string {
  return (string)($match['teams'][$side]['name'] ?? strtoupper($side));
}

function player_name_by_id(array $match, string $side, ?string $id): ?string {
  if ($id === null) return null;
  foreach ($match['units'][$side] ?? [] as $player) {
    if (($player['id'] ?? null) === $id) return (string)($player['name'] ?? $id);
  }
  return null;
}

function goal_scorer_name(array $match, string $scoringSide): ?string {
  $shot = $match['engine']['shot'] ?? null;
  if (is_array($shot) && ($shot['side'] ?? null) === $scoringSide) {
    $name = player_name_by_id($match, $scoringSide, is_string($shot['id'] ?? null) ? $shot['id'] : null);
    if ($name) return $name;
  }
  $lastTouch = (string)($match['engine']['lastTouch'] ?? '');
  if (str_contains($lastTouch, ':')) {
    [$side, $id] = explode(':', $lastTouch, 2);
    if ($side === $scoringSide) {
      $name = player_name_by_id($match, $scoringSide, $id);
      if ($name) return $name;
    }
  }
  return null;
}

function goal_assist_name(array $match, string $scoringSide, ?string $scorer): ?string {
  $shot = $match['engine']['shot'] ?? null;
  if (is_array($shot) && !empty($shot['penalty'])) return null;
  $pass = $match['engine']['lastPass'] ?? null;
  if (is_array($pass) && ($pass['side'] ?? '') === $scoringSide) {
    $at = (float)($pass['at'] ?? -99.0);
    if (((float)($match['elapsed'] ?? 0.0) - $at) <= 12.0) {
      $name = is_string($pass['name'] ?? null) ? (string)$pass['name'] : player_name_by_id($match, $scoringSide, is_string($pass['id'] ?? null) ? $pass['id'] : null);
      if ($name !== '' && ($scorer === null || $name !== $scorer)) return $name;
    }
  }
  $fallback = pick_assist_player($match['units'][$scoringSide] ?? [], ['name' => (string)($scorer ?? '')]);
  $name = (string)($fallback['name'] ?? '');
  if ($name === '' || ($scorer !== null && $name === $scorer)) return null;
  return $name;
}

function place_ball_on_player(array &$ball, array $player, string $side): void {
  $isKeeper = ($player['role'] ?? '') === 'GK';
  $radius = player_touch_radius($player, false);
  $offset = attack_dir($side) * ($isKeeper ? $radius * 0.34 : $radius * 0.72);
  $ball['x'] = clamp_float((float)$player['x'], 0.02, 0.98);
  $ball['y'] = clamp_float((float)$player['y'] + $offset, 0.01, 0.99);
  $ball['z'] = 0.0;
  $ball['vx'] = $isKeeper ? 0.0 : 0.008 * (0.5 - (float)$player['x']);
  $ball['vy'] = $isKeeper ? 0.0 : 0.024 * attack_dir($side);
  $ball['vz'] = 0.0;
}

function kickoff_taker_index(array $match, string $side): int {
  foreach ($match['units'][$side] ?? [] as $index => $player) {
    if (($player['role'] ?? '') === 'ST') return (int)$index;
  }
  return min(9, max(0, count($match['units'][$side] ?? []) - 1));
}

function kickoff_receiver_index(array $match, string $side, int $takerIndex): ?int {
  $taker = $match['units'][$side][$takerIndex] ?? null;
  if ($taker) {
    $best = null;
    $bestDist = PHP_FLOAT_MAX;
    foreach ($match['units'][$side] ?? [] as $index => $player) {
      if ($index === $takerIndex || ($player['role'] ?? '') === 'GK') continue;
      $dist = distance2($player, $taker);
      if (in_array($player['role'] ?? '', ['ST', 'CM'], true)) $dist *= 0.92;
      if ($dist < $bestDist) {
        $bestDist = $dist;
        $best = (int)$index;
      }
    }
    if ($best !== null) return $best;
  }
  foreach ($match['units'][$side] ?? [] as $index => $player) {
    if ($index !== $takerIndex && ($player['role'] ?? '') === 'ST') return (int)$index;
  }
  foreach ($match['units'][$side] ?? [] as $index => $player) {
    if ($index !== $takerIndex && ($player['role'] ?? '') === 'CM') return (int)$index;
  }
  foreach ($match['units'][$side] ?? [] as $index => $player) {
    if ($index !== $takerIndex && ($player['role'] ?? '') !== 'GK') return (int)$index;
  }
  return null;
}

function prepare_kickoff_units(array &$match, string $kickingSide): void {
  reset_units($match);
  $dir = attack_dir($kickingSide);
  $taker = kickoff_taker_index($match, $kickingSide);
  $receiver = kickoff_receiver_index($match, $kickingSide, $taker);
  $match['engine']['kickoffSide'] = $kickingSide;
  $match['engine']['kickoffTaker'] = $match['units'][$kickingSide][$taker]['id'] ?? null;
  $match['engine']['kickoffReceiver'] = $receiver !== null ? ($match['units'][$kickingSide][$receiver]['id'] ?? null) : null;
  if (isset($match['units'][$kickingSide][$taker])) {
    $match['units'][$kickingSide][$taker]['x'] = 0.485;
    $match['units'][$kickingSide][$taker]['y'] = 0.5 - $dir * 0.080;
    $match['units'][$kickingSide][$taker]['vx'] = 0.0;
    $match['units'][$kickingSide][$taker]['vy'] = 0.0;
  }
  if ($receiver !== null && isset($match['units'][$kickingSide][$receiver])) {
    $match['units'][$kickingSide][$receiver]['x'] = 0.565;
    $match['units'][$kickingSide][$receiver]['y'] = 0.5 - $dir * 0.070;
    $match['units'][$kickingSide][$receiver]['vx'] = 0.0;
    $match['units'][$kickingSide][$receiver]['vy'] = 0.0;
  }
}

function begin_kickoff(array &$match, string $kickingSide, float $pauseGameMinutes): void {
  ensure_match_engine($match);
  prepare_kickoff_units($match, $kickingSide);
  $match['ball'] = stopped_kickoff_ball();
  $match['engine']['kickoffDirection'] = attack_dir($kickingSide);
  $match['engine']['kickoffUntil'] = (float)$match['elapsed'] + max(0.0, $pauseGameMinutes);
  $match['engine']['kickoffWhistled'] = false;
  $match['engine']['kickoffWhistleUntil'] = 0.0;
  $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + max(0.0, $pauseGameMinutes) + 0.85;
}

function arrange_kickoff_players(array &$match, string $side, float $step): void {
  $takerId = is_string($match['engine']['kickoffTaker'] ?? null) ? $match['engine']['kickoffTaker'] : null;
  $receiverId = is_string($match['engine']['kickoffReceiver'] ?? null) ? $match['engine']['kickoffReceiver'] : null;
  $takerIndex = player_index_by_id($match['units'][$side] ?? [], $takerId) ?? kickoff_taker_index($match, $side);
  $receiverIndex = player_index_by_id($match['units'][$side] ?? [], $receiverId);
  $dir = attack_dir($side);
  if (isset($match['units'][$side][$takerIndex])) {
    $speed = 0.030 + player_stat($match['units'][$side][$takerIndex], 'speed') / 1500;
    move_towards($match['units'][$side][$takerIndex], 0.5, 0.5 - $dir * 0.012, $speed, $step);
  }
  if ($receiverIndex !== null && isset($match['units'][$side][$receiverIndex])) {
    $speed = 0.020 + player_stat($match['units'][$side][$receiverIndex], 'speed') / 2200;
    move_towards($match['units'][$side][$receiverIndex], 0.565, 0.5 - $dir * 0.070, $speed, $step);
  }
}

function kickoff_ball_for_side(array &$match, string $side): array {
  $direction = attack_dir($side);
  $takerId = is_string($match['engine']['kickoffTaker'] ?? null) ? $match['engine']['kickoffTaker'] : null;
  $takerIndex = player_index_by_id($match['units'][$side] ?? [], $takerId) ?? kickoff_taker_index($match, $side);
  $receiverId = is_string($match['engine']['kickoffReceiver'] ?? null) ? $match['engine']['kickoffReceiver'] : null;
  $receiverIndex = player_index_by_id($match['units'][$side] ?? [], $receiverId);
  if ($receiverIndex === null || $receiverIndex === $takerIndex) {
    $receiverIndex = kickoff_receiver_index($match, $side, $takerIndex);
  }
  if (isset($match['units'][$side][$takerIndex])) {
    $match['units'][$side][$takerIndex]['x'] = 0.5;
    $match['units'][$side][$takerIndex]['y'] = 0.5 - $direction * 0.012;
    $match['units'][$side][$takerIndex]['vx'] = 0.0;
    $match['units'][$side][$takerIndex]['vy'] = 0.0;
    $player = $match['units'][$side][$takerIndex];
    $match['engine']['lastTouch'] = $side . ':' . ($player['id'] ?? $takerIndex);
    $match['engine']['lastTouchAt'] = (float)$match['elapsed'];
  }
  if ($receiverIndex !== null && isset($match['units'][$side][$receiverIndex])) {
    $match['units'][$side][$receiverIndex]['x'] = 0.565;
    $match['units'][$side][$receiverIndex]['y'] = 0.5 - $direction * 0.070;
    $match['units'][$side][$receiverIndex]['vx'] = 0.0;
    $match['units'][$side][$receiverIndex]['vy'] = 0.0;
  }
  $match['engine']['kickoffSide'] = null;
  $match['engine']['kickoffTaker'] = null;
  $match['engine']['kickoffReceiver'] = null;
  $match['engine']['kickoffWhistled'] = false;
  $match['engine']['kickoffWhistleUntil'] = 0.0;
  $target = $receiverIndex !== null && isset($match['units'][$side][$receiverIndex])
    ? $match['units'][$side][$receiverIndex]
    : ['x' => 0.565, 'y' => 0.5 - $direction * 0.070, 'id' => null];
  $dx = (float)$target['x'] - 0.5;
  $dy = (float)$target['y'] - (0.5 - $direction * 0.004);
  $dist = max(0.001, sqrt($dx * $dx + $dy * $dy));
  $speed = clamp_float($dist / 0.42, 0.16, 0.28);
  $match['engine']['passTarget'] = [
    'side' => $side,
    'id' => $target['id'] ?? null,
    'quality' => 0.92,
    'type' => 'kickoff',
  ];
  return [
    'x' => 0.5,
    'y' => 0.5 - $direction * 0.004,
    'z' => 0.0,
    'vx' => $dx / $dist * $speed,
    'vy' => $dy / $dist * $speed,
    'vz' => 0.0,
  ];
}

function last_touch_side(array $match): ?string {
  $lastTouch = (string)($match['engine']['lastTouch'] ?? '');
  if (str_contains($lastTouch, ':')) {
    [$side] = explode(':', $lastTouch, 2);
    if (in_array($side, ['home', 'away'], true)) return $side;
  }
  $shot = $match['engine']['shot'] ?? null;
  if (is_array($shot) && in_array($shot['side'] ?? '', ['home', 'away'], true)) return (string)$shot['side'];
  return null;
}

function restart_kicker_index(array $match, string $side, string $type, array $spot): ?int {
  if ($type === 'goal_kick') {
    foreach ($match['units'][$side] ?? [] as $index => $player) {
      if (($player['role'] ?? '') === 'GK') return (int)$index;
    }
  }
  $best = null;
  $bestDist = PHP_FLOAT_MAX;
  foreach ($match['units'][$side] ?? [] as $index => $player) {
    if ($type === 'corner' && ($player['role'] ?? '') === 'GK') continue;
    $dist = distance2($player, $spot);
    if ($type === 'corner' && in_array($player['role'] ?? '', ['LM', 'RM', 'ST'], true)) $dist *= 0.75;
    if ($dist < $bestDist) {
      $bestDist = $dist;
      $best = (int)$index;
    }
  }
  return $best;
}

function restart_receiver_index(array $match, string $side, string $type, int $kickerIndex): ?int {
  $roles = $type === 'goal_kick' ? ['CB', 'RB', 'LB', 'CM', 'RM', 'LM'] : ['ST', 'CM', 'RM', 'LM', 'CB'];
  foreach ($roles as $role) {
    foreach ($match['units'][$side] ?? [] as $index => $player) {
      if ($index !== $kickerIndex && ($player['role'] ?? '') === $role) return (int)$index;
    }
  }
  foreach ($match['units'][$side] ?? [] as $index => $player) {
    if ($index !== $kickerIndex && ($player['role'] ?? '') !== 'GK') return (int)$index;
  }
  return null;
}

function prepare_restart_units(array &$match, string $side, string $type, array $spot): void {
  reset_units($match);
  $kickIndex = restart_kicker_index($match, $side, $type, $spot);
  if ($kickIndex === null || empty($match['units'][$side][$kickIndex])) return;
  $match['engine']['restartKicker'] = $match['units'][$side][$kickIndex]['id'] ?? null;
  $match['units'][$side][$kickIndex]['x'] = clamp_float((float)$spot['x'], 0.02, 0.98);
  $match['units'][$side][$kickIndex]['y'] = clamp_float((float)$spot['y'], 0.02, 0.98);
  $match['units'][$side][$kickIndex]['vx'] = 0.0;
  $match['units'][$side][$kickIndex]['vy'] = 0.0;

  $receiverIndex = restart_receiver_index($match, $side, $type, $kickIndex);
  if ($receiverIndex !== null && isset($match['units'][$side][$receiverIndex])) {
    $dir = attack_dir($side);
    $match['units'][$side][$receiverIndex]['x'] = $type === 'corner' ? 0.50 : 0.42;
    $match['units'][$side][$receiverIndex]['y'] = $type === 'corner'
      ? clamp_float($dir < 0 ? 0.13 : 0.87, 0.04, 0.96)
      : clamp_float((float)$spot['y'] + $dir * 0.20, 0.08, 0.92);
    $match['units'][$side][$receiverIndex]['vx'] = 0.0;
    $match['units'][$side][$receiverIndex]['vy'] = 0.0;
  }
}

function begin_restart(array &$match, string $type, string $side, array $spot, ?array $outBall = null, ?array $lineBall = null): void {
  ensure_match_engine($match);
  clear_owner($match);
  $outBall = $outBall ?: $spot;
  $out = [
    'x' => clamp_float((float)($outBall['x'] ?? $spot['x']), -0.30, 1.30),
    'y' => clamp_float((float)($outBall['y'] ?? $spot['y']), -0.35, 1.35),
  ];
  $line = $lineBall
    ? [
      'x' => clamp_float((float)($lineBall['x'] ?? $out['x']), -0.08, 1.08),
      'y' => clamp_float((float)($lineBall['y'] ?? ($out['y'] < 0.5 ? -0.028 : 1.028)), -0.16, 1.16),
    ]
    : [
      'x' => $out['x'],
      'y' => $out['y'] < 0.5 ? -0.028 : 1.028,
    ];
  $match['engine']['passTarget'] = null;
  $match['engine']['shot'] = null;
  $match['engine']['restartType'] = $type;
  $match['engine']['restartSide'] = $side;
  $match['engine']['restartSpot'] = ['x' => (float)$spot['x'], 'y' => (float)$spot['y']];
  $match['engine']['restartStartedAt'] = (float)$match['elapsed'];
  $match['engine']['restartLineBall'] = $line;
  $match['engine']['restartOutBall'] = $out;
  $match['engine']['restartRollUntil'] = (float)$match['elapsed'] + 0.38;
  $match['engine']['restartDeliveryUntil'] = (float)$match['elapsed'] + 1.12;
  $match['engine']['restartUntil'] = (float)$match['elapsed'] + 1.42;
  $match['engine']['restartSignalUntil'] = 0.0;
  $match['engine']['restartWhistled'] = false;
  prepare_restart_units($match, $side, $type, $match['engine']['restartSpot']);
  $match['ball'] = ['x' => $line['x'], 'y' => $line['y'], 'z' => 0.0, 'vx' => 0.0, 'vy' => 0.0, 'vz' => 0.0];
  $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + 2.05;
  $label = $type === 'corner' ? ' CORNER' : ' GOAL KICK';
  $match['events'][] = ['t' => round((float)$match['elapsed']), 'text' => side_team_name($match, $side) . $label];
}

function take_restart(array &$match, string $side, string $type, array $spot): void {
  $kickIndex = player_index_by_id($match['units'][$side] ?? [], is_string($match['engine']['restartKicker'] ?? null) ? $match['engine']['restartKicker'] : null);
  $kickIndex = $kickIndex ?? restart_kicker_index($match, $side, $type, $spot);
  if ($kickIndex === null || empty($match['units'][$side][$kickIndex])) return;
  $receiverIndex = restart_receiver_index($match, $side, $type, $kickIndex);
  $dir = attack_dir($side);
  $target = $receiverIndex !== null && isset($match['units'][$side][$receiverIndex])
    ? $match['units'][$side][$receiverIndex]
    : ['id' => null, 'x' => 0.5, 'y' => clamp_float((float)$spot['y'] + $dir * ($type === 'corner' ? 0.22 : 0.28), 0.06, 0.94)];
  $dx = (float)$target['x'] - (float)$spot['x'];
  $dy = (float)$target['y'] - (float)$spot['y'];
  $dist = max(0.001, sqrt($dx * $dx + $dy * $dy));
  $power = $type === 'corner' ? 0.36 : 0.30;
  $speed = clamp_float($dist / 0.68, 0.18, $power);
  $kicker = $match['units'][$side][$kickIndex];
  $match['engine']['lastTouch'] = $side . ':' . ($kicker['id'] ?? $kickIndex);
  $match['engine']['lastTouchAt'] = (float)$match['elapsed'];
  $match['engine']['passTarget'] = [
    'side' => $side,
    'id' => $target['id'] ?? null,
    'quality' => $type === 'corner' ? 0.78 : 0.86,
    'type' => $type === 'corner' ? 'long' : 'ground',
  ];
  $match['ball'] = [
    'x' => (float)$spot['x'],
    'y' => (float)$spot['y'],
    'z' => $type === 'corner' ? 0.035 : 0.0,
    'vx' => $dx / $dist * $speed,
    'vy' => $dy / $dist * $speed,
    'vz' => $type === 'corner' ? 0.24 : 0.0,
  ];
  $match['engine']['restartType'] = null;
  $match['engine']['restartSide'] = null;
  $match['engine']['restartSpot'] = null;
  $match['engine']['restartKicker'] = null;
  $match['engine']['restartStartedAt'] = 0.0;
  $match['engine']['restartRollUntil'] = 0.0;
  $match['engine']['restartDeliveryUntil'] = 0.0;
  $match['engine']['restartUntil'] = 0.0;
  $match['engine']['restartSignalUntil'] = 0.0;
  $match['engine']['restartWhistled'] = false;
  $match['engine']['restartLineBall'] = null;
  $match['engine']['restartOutBall'] = null;
  $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + ($type === 'corner' ? 0.34 : 0.22);
}

function award_foul(array &$match, string $fouledSide, string $foulerSide, array &$ball, bool $yellow, ?array $foulerPlayer = null, ?array $fouledPlayer = null): void {
  ensure_match_engine($match);
  $spot = [
    'x' => clamp_float((float)($ball['x'] ?? 0.5), 0.08, 0.92),
    'y' => clamp_float((float)($ball['y'] ?? 0.5), 0.08, 0.92),
  ];
  $match['engine']['fouls'][$foulerSide]++;
  if ($foulerPlayer) telemetry_add($match, $foulerSide, $foulerPlayer, ['fouls' => 1]);
  $red = false;
  $secondYellow = false;
  $directRed = false;
  if ($foulerPlayer) {
    telemetry_register_player($match, $foulerSide, $foulerPlayer);
    $key = telemetry_key($foulerPlayer);
    $currentYellows = (int)($match['engine']['playerStats'][$foulerSide][$key]['yellows'] ?? 0);
    $directRedChance = clamp_float(0.002 + player_stat($foulerPlayer, 'aggression') / 2900 + (100 - player_stat($foulerPlayer, 'discipline')) / 3600, 0.002, 0.055);
    $directRed = rand_float() < $directRedChance;
    if ($directRed) {
      $yellow = false;
      $red = true;
    } elseif ($yellow && $currentYellows >= 1) {
      $secondYellow = true;
      $red = true;
    }
  }
  if ($yellow) {
    $match['engine']['yellows'][$foulerSide]++;
    if ($foulerPlayer) telemetry_add($match, $foulerSide, $foulerPlayer, ['yellows' => 1]);
    $match['events'][] = ['t' => round((float)$match['elapsed']), 'text' => side_team_name($match, $foulerSide) . ($secondYellow ? ' SECOND YELLOW CARD' : ' YELLOW CARD')];
  }
  if ($red) {
    $match['engine']['reds'][$foulerSide]++;
    if ($foulerPlayer) {
      telemetry_add($match, $foulerSide, $foulerPlayer, [
        'reds' => 1,
        'secondYellowReds' => $secondYellow ? 1 : 0,
        'directReds' => $directRed ? 1 : 0,
      ]);
      sync_match_player_cards($match, $foulerSide, $foulerPlayer);
      send_off_match_player($match, $foulerSide, $foulerPlayer);
    }
    $match['events'][] = ['t' => round((float)$match['elapsed']), 'text' => side_team_name($match, $foulerSide) . ($secondYellow ? ' RED CARD (2Y)' : ' RED CARD')];
  } elseif ($foulerPlayer && $yellow) {
    sync_match_player_cards($match, $foulerSide, $foulerPlayer);
  }
  if ($foulerPlayer) telemetry_maybe_complain($match, $foulerSide, $foulerPlayer, $yellow ? 0.24 : 0.12);
  if ($fouledPlayer) telemetry_maybe_complain($match, $fouledSide, $fouledPlayer, 0.20);
  if (is_penalty_foul_spot($spot, $fouledSide)) {
    begin_penalty($match, $fouledSide);
    $ball = $match['ball'];
    return;
  }
  $match['events'][] = ['t' => round((float)$match['elapsed']), 'text' => side_team_name($match, $fouledSide) . ' FREE KICK'];
  clear_owner($match);
  $match['engine']['passTarget'] = null;
  $match['engine']['shot'] = null;
  $match['engine']['freeKickSide'] = $fouledSide;
  $match['engine']['freeKickSpot'] = $spot;
  $kickIndex = nearest_role_index($match['units'][$fouledSide] ?? [], $spot, $fouledSide);
  $match['engine']['freeKickKicker'] = $kickIndex !== null ? ($match['units'][$fouledSide][$kickIndex]['id'] ?? null) : null;
  $match['engine']['freeKickUntil'] = (float)$match['elapsed'] + 0.95;
  $match['engine']['freeKickSignalUntil'] = (float)$match['elapsed'] + 1.45;
  $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + 1.65;
  $ball = stopped_ball_at($spot['x'], $spot['y']);
  $match['ball'] = $ball;
}

function free_kick_kicker_index(array $match, string $side, array $spot): ?int {
  $kickerId = $match['engine']['freeKickKicker'] ?? null;
  $index = player_index_by_id($match['units'][$side] ?? [], is_string($kickerId) ? $kickerId : null);
  return $index ?? nearest_role_index($match['units'][$side] ?? [], $spot, $side);
}

function move_player_away_from_spot(array &$player, array $spot, float $minDistance, float $speed, float $step): void {
  $dx = (float)$player['x'] - (float)$spot['x'];
  $dy = (float)$player['y'] - (float)$spot['y'];
  $dist = sqrt($dx * $dx + $dy * $dy);
  if ($dist >= $minDistance) return;
  if ($dist < 0.001) {
    $dx = ((float)($player['homeX'] ?? 0.5) < 0.5) ? -1.0 : 1.0;
    $dy = ((float)($player['homeY'] ?? 0.5) < 0.5) ? -0.35 : 0.35;
    $dist = sqrt($dx * $dx + $dy * $dy);
  }
  $targetX = clamp_float((float)$spot['x'] + $dx / $dist * $minDistance, 0.04, 0.96);
  $targetY = clamp_float((float)$spot['y'] + $dy / $dist * $minDistance, 0.04, 0.96);
  move_towards($player, $targetX, $targetY, $speed, $step);
}

function arrange_free_kick_players(array &$match, string $freeKickSide, array $spot, float $step): void {
  $opponent = opponent_side($freeKickSide);
  if (!empty($match['units'][$opponent]) && is_array($match['units'][$opponent])) {
    foreach ($match['units'][$opponent] as &$player) {
      $speed = 0.065 + player_stat($player, 'speed') / 900;
      move_player_away_from_spot($player, $spot, 0.175, $speed, $step);
    }
    unset($player);
  }

  $kickIndex = free_kick_kicker_index($match, $freeKickSide, $spot);
  if (!empty($match['units'][$freeKickSide]) && is_array($match['units'][$freeKickSide])) {
    foreach ($match['units'][$freeKickSide] as $index => &$player) {
      if ($index === $kickIndex) {
        $speed = 0.030 + player_stat($player, 'speed') / 1600;
        move_towards($player, (float)$spot['x'], (float)$spot['y'], $speed, $step);
        continue;
      }
      move_player_away_from_spot($player, $spot, 0.070, 0.035 + player_stat($player, 'speed') / 1800, $step);
      $role = (string)($player['role'] ?? 'CM');
      if ($role === 'GK') continue;
      $dir = attack_dir($freeKickSide);
      $supportX = clamp_float((float)$player['homeX'] + ((float)$spot['x'] - 0.5) * 0.10, 0.06, 0.94);
      $supportY = clamp_role_y($freeKickSide, $role, (float)$spot['y'] + $dir * (0.12 + ($index % 3) * 0.035), 0.16);
      $speed = 0.025 + player_stat($player, 'speed') / 2000;
      move_towards($player, $supportX, $supportY, $speed, $step);
    }
    unset($player);
  }
}

function take_free_kick(array &$match, string $side, int $kickIndex): void {
  $player = $match['units'][$side][$kickIndex];
  $shotProbability = shot_probability($match, $side, $player);
  $receiver = choose_receiver($match, $side, $player);
  $goalY = $side === 'home' ? 0.0 : 1.0;
  $goalDistance = abs((float)$player['y'] - $goalY);
  $central = 1.0 - clamp_float(abs((float)$player['x'] - 0.5) / 0.42, 0.0, 1.0);
  $shootNow = $goalDistance < 0.22 || ($goalDistance < 0.34 && $central > 0.34 && $shotProbability > 0.20);
  $match['engine']['freeKickSide'] = null;
  $match['engine']['freeKickSpot'] = null;
  $match['engine']['freeKickKicker'] = null;
  $match['engine']['freeKickUntil'] = 0.0;
  $match['engine']['freeKickSignalUntil'] = 0.0;
  $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + 0.18;
  if ($shootNow) {
    release_shot($match, $side, $kickIndex);
    return;
  }
  if ($receiver) {
    release_pass($match, $side, $kickIndex, $receiver);
    return;
  }
  set_owner($match, $side, $kickIndex);
}

function penalty_spot_for_side(string $side): array {
  $spotY = $side === 'home' ? (11.0 / 105.0) : (1.0 - 11.0 / 105.0);
  return ['x' => 0.5, 'y' => $spotY];
}

function is_penalty_foul_spot(array $spot, string $attackingSide): bool {
  $x = (float)($spot['x'] ?? 0.5);
  $y = (float)($spot['y'] ?? 0.5);
  $halfWidth = ((7.32 + 16.5 * 2.0) / 68.0) / 2.0;
  $minX = 0.5 - $halfWidth;
  $maxX = 0.5 + $halfWidth;
  $depth = 16.5 / 105.0;
  if ($x < $minX || $x > $maxX) return false;
  return $attackingSide === 'home' ? $y <= $depth : $y >= (1.0 - $depth);
}

function penalty_kicker_index(array $match, string $side): ?int {
  $best = null;
  $bestScore = -1;
  foreach ($match['units'][$side] ?? [] as $index => $player) {
    if (($player['role'] ?? '') === 'GK') continue;
    $score = player_stat($player, 'shooting') * 1.15 + player_stat($player, 'shotcontrol') * 0.85 + player_stat($player, 'composure') * 0.70 + player_stat($player, 'crit') * 0.15;
    if (in_array($player['role'] ?? '', ['ST', 'CM'], true)) $score += 8;
    if ($score > $bestScore) {
      $bestScore = $score;
      $best = (int)$index;
    }
  }
  return $best;
}

function prepare_penalty_units(array &$match, string $side): void {
  reset_units($match);
  $spot = penalty_spot_for_side($side);
  $kickerIndex = penalty_kicker_index($match, $side);
  if ($kickerIndex !== null && isset($match['units'][$side][$kickerIndex])) {
    $dir = attack_dir($side);
    $match['engine']['penaltyKicker'] = $match['units'][$side][$kickerIndex]['id'] ?? null;
    $match['units'][$side][$kickerIndex]['x'] = 0.5;
    $match['units'][$side][$kickerIndex]['y'] = clamp_float((float)$spot['y'] - $dir * 0.045, 0.03, 0.97);
    $match['units'][$side][$kickerIndex]['vx'] = 0.0;
    $match['units'][$side][$kickerIndex]['vy'] = 0.0;
  }
  $keeperSide = opponent_side($side);
  foreach ($match['units'][$keeperSide] ?? [] as &$player) {
    if (($player['role'] ?? '') !== 'GK') continue;
    $player['x'] = 0.5;
    $player['y'] = $side === 'home' ? 0.012 : 0.988;
    $player['vx'] = 0.0;
    $player['vy'] = 0.0;
    break;
  }
  unset($player);
}

function begin_penalty(array &$match, string $side): void {
  ensure_match_engine($match);
  clear_owner($match);
  $spot = penalty_spot_for_side($side);
  $match['engine']['passTarget'] = null;
  $match['engine']['shot'] = null;
  $match['engine']['freeKickSide'] = null;
  $match['engine']['freeKickSpot'] = null;
  $match['engine']['freeKickKicker'] = null;
  $match['engine']['freeKickUntil'] = 0.0;
  $match['engine']['freeKickSignalUntil'] = 0.0;
  $match['engine']['penaltySide'] = $side;
  $match['engine']['penaltyUntil'] = (float)$match['elapsed'] + 0.95;
  $match['engine']['penaltySignalUntil'] = 0.0;
  $match['engine']['penaltyWhistled'] = false;
  prepare_penalty_units($match, $side);
  $match['ball'] = stopped_ball_at($spot['x'], $spot['y']);
  $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + 1.75;
  $match['events'][] = ['t' => round((float)$match['elapsed']), 'text' => side_team_name($match, $side) . ' PENALTY'];
}

function take_penalty(array &$match, string $side): void {
  $spot = penalty_spot_for_side($side);
  $kickIndex = player_index_by_id($match['units'][$side] ?? [], is_string($match['engine']['penaltyKicker'] ?? null) ? $match['engine']['penaltyKicker'] : null);
  $kickIndex = $kickIndex ?? penalty_kicker_index($match, $side);
  if ($kickIndex === null || empty($match['units'][$side][$kickIndex])) return;
  $player = $match['units'][$side][$kickIndex];
  $shooting = player_stat($player, 'shooting');
  $shotControl = player_stat($player, 'shotcontrol');
  $composure = player_stat($player, 'composure');
  $accuracy = ($shooting * 0.45 + $shotControl * 0.30 + $composure * 0.25) / 100;
  $targetX = clamp_float(0.5 + (rand_float() * 2 - 1) * (0.22 + (1.0 - $accuracy) * 0.10), goal_min_x() + 0.01, goal_max_x() - 0.01);
  if (rand_float() > $accuracy + 0.18) $targetX += rand_float() < 0.5 ? -0.20 : 0.20;
  $goalY = $side === 'home' ? -0.04 : 1.04;
  $dx = $targetX - (float)$spot['x'];
  $dy = $goalY - (float)$spot['y'];
  $dist = max(0.001, sqrt($dx * $dx + $dy * $dy));
  $speed = clamp_float(0.36 + $shooting / 520 + $shotControl / 900, 0.40, 0.64);
  clear_owner($match);
  $match['engine']['shots'][$side]++;
  telemetry_add($match, $side, $player, ['shots' => 1, 'touches' => 1]);
  $match['engine']['lastShotSeq'] = (int)($match['engine']['lastShotSeq'] ?? 0) + 1;
  $match['engine']['lastShotAt'] = (float)$match['elapsed'];
  $match['engine']['lastShotBy'] = [
    'side' => $side,
    'id' => $player['id'] ?? null,
    'name' => $player['name'] ?? null,
  ];
  $match['engine']['shot'] = ['side' => $side, 'id' => $player['id'] ?? null, 'power' => $speed, 'crit' => false, 'penalty' => true];
  $match['engine']['penaltySide'] = null;
  $match['engine']['penaltyKicker'] = null;
  $match['engine']['penaltyUntil'] = 0.0;
  $match['engine']['penaltySignalUntil'] = 0.0;
  $match['engine']['penaltyWhistled'] = false;
  $match['engine']['lastTouch'] = $side . ':' . ($player['id'] ?? $kickIndex);
  $match['engine']['lastTouchAt'] = (float)$match['elapsed'];
  $match['engine']['lastPass'] = null;
  $match['ball'] = [
    'x' => (float)$spot['x'],
    'y' => (float)$spot['y'],
    'z' => rand_float() < 0.24 ? 0.035 : 0.0,
    'vx' => $dx / $dist * $speed,
    'vy' => $dy / $dist * $speed,
    'vz' => rand_float() < 0.24 ? 0.11 : 0.0,
  ];
  $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + 0.44;
}

function make_substitution(array &$match, string $side): array {
  ensure_match_engine($match);
  if (!in_array($side, ['home', 'away'], true)) return ['ok' => false, 'error' => 'bad_side'];
  $used = (int)($match['engine']['subsUsed'][$side] ?? 0);
  if ($used >= MANAGER_MAX_MATCH_SUBS) return ['ok' => false, 'error' => 'subs_used'];
  if (empty($match['units'][$side]) || empty($match['bench'][$side])) return ['ok' => false, 'error' => 'no_bench'];

  $owner = owner_ref($match);
  $ownerIndex = $owner && $owner['side'] === $side ? $owner['index'] : null;
  $outIndex = null;
  $outScore = PHP_FLOAT_MAX;
  foreach ($match['units'][$side] as $index => $player) {
    if (($player['role'] ?? '') === 'GK') continue;
    if ($ownerIndex !== null && $index === $ownerIndex) continue;
    $score = (int)($player['rating'] ?? 50) + mt_rand(0, 9);
    if ($score < $outScore) {
      $outScore = $score;
      $outIndex = $index;
    }
  }
  if ($outIndex === null) {
    foreach ($match['units'][$side] as $index => $player) {
      if (($player['role'] ?? '') === 'GK') continue;
      $outIndex = $index;
      break;
    }
  }
  if ($outIndex === null) $outIndex = 0;

  $outgoing = $match['units'][$side][$outIndex];
  $outRole = (string)($outgoing['role'] ?? 'CM');
  $benchIndex = null;
  $benchScore = -1;
  foreach ($match['bench'][$side] as $index => $player) {
    if (($player['role'] ?? '') === $outRole) {
      $benchIndex = $index;
      break;
    }
    if (($player['role'] ?? '') !== 'GK' && (int)($player['rating'] ?? 0) > $benchScore) {
      $benchScore = (int)($player['rating'] ?? 0);
      $benchIndex = $index;
    }
  }
  if ($benchIndex === null) $benchIndex = 0;

  $incoming = array_splice($match['bench'][$side], $benchIndex, 1)[0];
  $incoming['role'] = $outRole;
  $incoming['homeX'] = $outgoing['homeX'];
  $incoming['homeY'] = $outgoing['homeY'];
  $incoming['x'] = $outgoing['x'];
  $incoming['y'] = $outgoing['y'];
  $match['units'][$side][$outIndex] = $incoming;
  $match['engine']['subsUsed'][$side] = $used + 1;
  if ($ownerIndex !== null && $ownerIndex === $outIndex) set_owner($match, $side, $outIndex);
  $match['events'][] = ['t' => round((float)$match['elapsed']), 'text' => side_team_name($match, $side) . ' SUB ' . ($used + 1) . '/' . MANAGER_MAX_MATCH_SUBS];
  return ['ok' => true, 'in' => $incoming['id'] ?? null, 'out' => $outgoing['id'] ?? null];
}

function nearest_opponent_index(array $match, string $side, float $x, float $y): ?int {
  $opponent = opponent_side($side);
  return nearest_index($match['units'][$opponent] ?? [], ['x' => $x, 'y' => $y]);
}

function pressure_on(array $match, string $side, float $x, float $y): float {
  $opponent = opponent_side($side);
  $best = PHP_FLOAT_MAX;
  foreach ($match['units'][$opponent] ?? [] as $player) {
    if (($player['role'] ?? '') === 'GK') continue;
    $dist = sqrt(distance2($player, ['x' => $x, 'y' => $y]));
    $best = min($best, $dist);
  }
  if ($best === PHP_FLOAT_MAX) return 0.0;
  return clamp_float(1.0 - $best / 0.18, 0.0, 1.0);
}

function pass_line_clearance(array $match, string $side, array $passer, array $receiver): float {
  $opponent = opponent_side($side);
  $ax = (float)$passer['x'];
  $ay = (float)$passer['y'];
  $bx = (float)$receiver['x'];
  $by = (float)$receiver['y'];
  $dx = $bx - $ax;
  $dy = $by - $ay;
  $len2 = max(0.0001, $dx * $dx + $dy * $dy);
  $clearance = 1.0;
  foreach ($match['units'][$opponent] ?? [] as $defender) {
    $px = (float)$defender['x'];
    $py = (float)$defender['y'];
    $t = clamp_float((($px - $ax) * $dx + ($py - $ay) * $dy) / $len2, 0.0, 1.0);
    $cx = $ax + $dx * $t;
    $cy = $ay + $dy * $t;
    $dist = sqrt(($px - $cx) * ($px - $cx) + ($py - $cy) * ($py - $cy));
    if ($dist < 0.035) $clearance *= 0.25;
    elseif ($dist < 0.075) $clearance *= 0.62;
  }
  return clamp_float($clearance, 0.0, 1.0);
}

function receiver_score(array $match, string $side, array $passer, array $receiver): float {
  if (($receiver['id'] ?? '') === ($passer['id'] ?? '')) return 0.0;
  $dx = (float)$receiver['x'] - (float)$passer['x'];
  $dy = (float)$receiver['y'] - (float)$passer['y'];
  $dist = sqrt($dx * $dx + $dy * $dy);
  if ($dist < 0.08 || $dist > 0.72) return 0.0;
  $forward = attack_dir($side) * $dy;
  $forwardScore = clamp_float(($forward + 0.12) / 0.50, 0.0, 1.0);
  $distanceScore = 1.0 - clamp_float(abs($dist - 0.26) / 0.48, 0.0, 1.0);
  $space = 1.0 - pressure_on($match, $side, (float)$receiver['x'], (float)$receiver['y']);
  $line = pass_line_clearance($match, $side, $passer, $receiver);
  $role = in_array($receiver['role'] ?? '', ['ST', 'RM', 'LM', 'CM'], true) ? 1.0 : 0.75;
  $pass = player_stat($passer, 'passing') / 100;
  $vision = player_stat($passer, 'vision') / 100;
  $crossing = player_stat($passer, 'crossing') / 100;
  $control = player_stat($receiver, 'passcontrol') / 100;
  $positioning = player_stat($receiver, 'positioning') / 100;
  $wideLane = clamp_float(abs((float)$receiver['x'] - 0.5) / 0.44, 0.0, 1.0);
  $wideRole = in_array($receiver['role'] ?? '', ['RM', 'LM', 'RB', 'LB'], true) ? 1.0 : 0.0;
  $passerCentral = 1.0 - clamp_float(abs((float)$passer['x'] - 0.5) / 0.42, 0.0, 1.0);
  $wideSwitch = clamp_float($wideLane * 0.60 + $wideRole * 0.40, 0.0, 1.0) * (0.35 + $passerCentral * 0.45 + $crossing * 0.20);
  return clamp_float(0.16 * $forwardScore + 0.13 * $distanceScore + 0.20 * $space + 0.18 * $line + 0.06 * $role + 0.05 * $pass + 0.06 * $vision + 0.04 * $control + 0.04 * $positioning + 0.08 * $wideSwitch, 0.0, 1.0);
}

function choose_receiver(array $match, string $side, array $passer): ?array {
  $best = null;
  $bestScore = 0.0;
  $wide = null;
  $wideScore = 0.0;
  $long = null;
  $longScore = 0.0;
  $passerPressure = pressure_on($match, $side, (float)$passer['x'], (float)$passer['y']);
  $passerCentral = 1.0 - clamp_float(abs((float)$passer['x'] - 0.5) / 0.42, 0.0, 1.0);
  $passing = player_stat($passer, 'passing');
  $vision = player_stat($passer, 'vision');
  $crossing = player_stat($passer, 'crossing');
  foreach ($match['units'][$side] ?? [] as $index => $receiver) {
    $score = receiver_score($match, $side, $passer, $receiver);
    if ($score > $bestScore) {
      $bestScore = $score;
      $best = ['index' => $index, 'score' => $score];
    }
    $dx = (float)$receiver['x'] - (float)$passer['x'];
    $dy = (float)$receiver['y'] - (float)$passer['y'];
    $dist = sqrt($dx * $dx + $dy * $dy);
    $forward = attack_dir($side) * $dy;
    $wideLane = abs((float)$receiver['x'] - 0.5);
    $isWideOption = $wideLane > 0.26 || in_array($receiver['role'] ?? '', ['RM', 'LM', 'RB', 'LB'], true);
    if ($isWideOption && abs($dx) > 0.10 && $forward > -0.06 && $score > 0.30) {
      $candidateScore = $score + $wideLane * 0.22 + $passerCentral * 0.08 + $crossing / 550;
      if ($candidateScore > $wideScore) {
        $wideScore = $candidateScore;
        $wide = ['index' => $index, 'score' => clamp_float($score + 0.05, 0.0, 1.0), 'type' => 'wide'];
      }
    }
    $isForwardTarget = ($receiver['role'] ?? '') !== 'GK';
    if ($isForwardTarget && $dist > 0.22 && $forward > 0.05 && $score > 0.12) {
      $roleBoost = in_array($receiver['role'] ?? '', ['ST', 'RM', 'LM'], true) ? 0.12 : 0.0;
      $candidateScore = $score + $forward * 0.42 + $dist * 0.20 + ($passing + $vision) / 680 + $roleBoost;
      if ($candidateScore > $longScore) {
        $longScore = $candidateScore;
        $long = ['index' => $index, 'score' => clamp_float($score + 0.04, 0.0, 1.0), 'type' => 'long'];
      }
    }
  }
  if ($long !== null) {
    $longChance = clamp_float(0.14 + ($passing + $vision) / 920 + $passerPressure * 0.16, 0.14, 0.44);
    if (in_array($passer['role'] ?? '', ['GK', 'CB', 'RB', 'LB', 'CM', 'RM', 'LM'], true) && rand_float() < $longChance) return $long;
  }
  if ($wide !== null) {
    $wideChance = clamp_float(0.10 + $passerCentral * 0.13 + $passerPressure * 0.10 + $crossing / 700, 0.10, 0.38);
    if (rand_float() < $wideChance) return $wide;
  }
  return $best !== null && $bestScore > 0.37 ? $best : null;
}

function shot_probability(array $match, string $side, array $player): float {
  $dir = attack_dir($side);
  $goalY = $side === 'home' ? 0.0 : 1.0;
  $goalDistance = abs((float)$player['y'] - $goalY);
  if ($goalDistance > 0.62) return 0.0;
  $central = 1.0 - clamp_float(abs((float)$player['x'] - 0.5) / 0.42, 0.0, 1.0);
  $distanceScore = 1.0 - clamp_float($goalDistance / 0.62, 0.0, 1.0);
  $skill = (player_stat($player, 'shooting') * 0.46 + player_stat($player, 'shotcontrol') * 0.28 + player_stat($player, 'composure') * 0.16 + player_stat($player, 'positioning') * 0.10) / 100;
  $pressure = pressure_on($match, $side, (float)$player['x'], (float)$player['y']);
  $rolePenalty = in_array($player['role'] ?? '', ['CB', 'RB', 'LB'], true) && $goalDistance > 0.22 ? 0.12 : 0.0;
  $roleBoost = match ((string)($player['role'] ?? '')) {
    'ST' => 0.26,
    'RM', 'LM' => 0.10,
    'CM' => 0.05,
    default => 0.0,
  };
  $pressurePenalty = $pressure * clamp_float(0.22 - player_stat($player, 'composure') / 800, 0.08, 0.22);
  return clamp_float(-0.04 + 0.42 * $distanceScore + 0.20 * $central + 0.48 * $skill + $roleBoost - $pressurePenalty - $rolePenalty, 0.0, 0.96);
}

function release_pass(array &$match, string $side, int $ownerIndex, array $target): void {
  $passer = $match['units'][$side][$ownerIndex];
  $receiver = $match['units'][$side][$target['index']];
  $passing = player_stat($passer, 'passing');
  $passControl = player_stat($passer, 'passcontrol');
  $vision = player_stat($passer, 'vision');
  $composure = player_stat($passer, 'composure');
  $crossing = player_stat($passer, 'crossing');
  $dx = (float)$receiver['x'] - (float)$passer['x'];
  $dy = (float)$receiver['y'] - (float)$passer['y'];
  $dist = max(0.001, sqrt($dx * $dx + $dy * $dy));
  $type = (string)($target['type'] ?? 'ground');
  $isLong = $type === 'long';
  $weightedPass = $passing * 0.50 + $passControl * 0.22 + $vision * 0.18 + $composure * 0.10 + ($type === 'wide' || $isLong ? $crossing * 0.10 : 0);
  $error = (1.0 - clamp_float($weightedPass / ($type === 'wide' || $isLong ? 110 : 100), 0.0, 1.0)) * 0.16;
  $angleX = $dx / $dist + (rand_float() * 2 - 1) * $error;
  $angleY = $dy / $dist + (rand_float() * 2 - 1) * $error;
  $len = max(0.001, sqrt($angleX * $angleX + $angleY * $angleY));
  $targetTime = $isLong
    ? clamp_float(0.82 + $dist * 0.52 - ($passing + $vision) / 1700, 0.78, 1.22)
    : clamp_float(0.36 + $dist * 1.08 - ($passing + $passControl + $vision * 0.45) / 900, 0.34, 1.12);
  $speed = $isLong ? clamp_float($dist / $targetTime, 0.24, 0.52) : clamp_float($dist / $targetTime, 0.14, 0.46);
  clear_owner($match);
  $match['engine']['passTarget'] = [
    'side' => $side,
    'id' => $receiver['id'],
    'quality' => clamp_float(($target['score'] + $passing / 100 + $passControl / 100 + $vision / 100 + $composure / 100) / 5, 0.0, 1.0),
    'type' => $isLong ? 'long' : ((string)($target['type'] ?? '') === 'wide' ? 'wide' : 'ground'),
  ];
  $match['engine']['lastPass'] = [
    'side' => $side,
    'id' => $passer['id'] ?? null,
    'name' => $passer['name'] ?? null,
    'to' => $receiver['id'] ?? null,
    'at' => (float)$match['elapsed'],
  ];
  telemetry_add($match, $side, $passer, [
    'passes' => 1,
    'longPasses' => $isLong ? 1 : 0,
    'widePasses' => ((string)($target['type'] ?? '') === 'wide') ? 1 : 0,
  ]);
  $release = player_ball_release_point($passer, $side, 0.92);
  $match['ball'] = [
    'x' => $release['x'],
    'y' => $release['y'],
    'z' => $isLong ? 0.045 : 0.0,
    'vx' => $angleX / $len * $speed,
    'vy' => $angleY / $len * $speed,
    'vz' => $isLong ? 0.25 + ($passing + $crossing) / 200 * 0.09 + min(0.05, $dist * 0.06) : 0.0,
  ];
  $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + ($isLong ? 0.32 : 0.18);
}

function release_shot(array &$match, string $side, int $ownerIndex): void {
  $player = $match['units'][$side][$ownerIndex];
  $shooting = player_stat($player, 'shooting');
  $shotControl = player_stat($player, 'shotcontrol');
  $composure = player_stat($player, 'composure');
  $strength = player_stat($player, 'strength');
  $crit = player_stat($player, 'crit');
  $isCrit = rand_float() < ($crit / 100) * 0.12;
  if ($isCrit) $match['engine']['crits'][$side]++;
  $goalX = 0.5;
  $goalY = $side === 'home' ? -0.04 : 1.04;
  $accuracy = ($shooting * 0.45 + $shotControl * 0.34 + $composure * 0.21) / 100;
  $error = (1 - $accuracy) * ($isCrit ? 0.12 : 0.28);
  $targetX = clamp_float($goalX + (rand_float() * 2 - 1) * $error, 0.22, 0.78);
  if (!$isCrit && rand_float() > $accuracy + 0.10) {
    $targetX += rand_float() < 0.5 ? -0.18 : 0.18;
  }
  $dx = $targetX - (float)$player['x'];
  $dy = $goalY - (float)$player['y'];
  $dist = max(0.001, sqrt($dx * $dx + $dy * $dy));
  $speed = clamp_float(0.25 + $dist * 0.24 + $shooting / 100 * 0.10 + $strength / 100 * 0.05 + ($isCrit ? 0.08 : 0.0), 0.30, 0.62);
  $lifted = rand_float() < clamp_float(0.10 + $shotControl / 600 + $shooting / 900, 0.10, 0.34);
  clear_owner($match);
  $match['engine']['shots'][$side]++;
  telemetry_add($match, $side, $player, ['shots' => 1, 'crits' => $isCrit ? 1 : 0, 'touches' => 1]);
  $match['engine']['lastShotSeq'] = (int)($match['engine']['lastShotSeq'] ?? 0) + 1;
  $match['engine']['lastShotAt'] = (float)$match['elapsed'];
  $match['engine']['lastShotBy'] = [
    'side' => $side,
    'id' => $player['id'] ?? null,
    'name' => $player['name'] ?? null,
  ];
  $match['engine']['shot'] = ['side' => $side, 'id' => $player['id'], 'power' => $speed, 'crit' => $isCrit];
  $release = player_ball_release_point($player, $side, 0.96);
  $match['ball'] = [
    'x' => $release['x'],
    'y' => $release['y'],
    'z' => $lifted ? 0.05 : 0.0,
    'vx' => $dx / $dist * $speed,
    'vy' => $dy / $dist * $speed,
    'vz' => $lifted ? 0.11 + $shooting / 100 * 0.07 : 0.0,
  ];
  $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + 0.48;
  if ($isCrit) {
    $match['events'][] = ['t' => round((float)$match['elapsed']), 'text' => ($match['teams'][$side]['name'] ?? strtoupper($side)) . ' CRIT SHOT'];
  }
}

function apply_keeper_save(array &$match, array &$ball): bool {
  $nearTop = (float)$ball['y'] < 0.035 && (float)($ball['vy'] ?? 0) < 0;
  $nearBottom = (float)$ball['y'] > 0.965 && (float)($ball['vy'] ?? 0) > 0;
  if (!$nearTop && !$nearBottom) return false;
  if ((float)$ball['x'] < goal_min_x() - 0.035 || (float)$ball['x'] > goal_max_x() + 0.035) return false;
  $defendingSide = $nearTop ? 'away' : 'home';
  if (empty($match['units'][$defendingSide][0])) return false;
  $keeper = $match['units'][$defendingSide][0];
  $dist = sqrt(distance2($keeper, $ball));
  $reach = 0.052 + player_stat($keeper, 'keeping') / 1100 + player_stat($keeper, 'speed') / 1800 + player_stat($keeper, 'positioning') / 2200;
  if ($dist > $reach) return false;

  $shot = $match['engine']['shot'] ?? null;
  $shotPower = is_array($shot) ? (float)($shot['power'] ?? 0.25) : 0.22;
  $critPenalty = is_array($shot) && !empty($shot['crit']) ? 0.16 : 0.0;
  $saveSkill = player_stat($keeper, 'keeping') * 0.58 + player_stat($keeper, 'speed') * 0.14 + player_stat($keeper, 'positioning') * 0.14 + player_stat($keeper, 'composure') * 0.08 + player_stat($keeper, 'shotcontrol') * 0.06;
  $saveChance = clamp_float(0.16 + $saveSkill / 120 - $shotPower * 1.08 - $critPenalty - $dist * 1.4, 0.05, 0.92);
  if (rand_float() > $saveChance) return false;

  $match['engine']['saves'][$defendingSide]++;
  telemetry_add($match, $defendingSide, $keeper, ['saves' => 1, 'touches' => 1]);
  $catchChance = clamp_float((player_stat($keeper, 'keeping') * 0.72 + player_stat($keeper, 'composure') * 0.18 + player_stat($keeper, 'strength') * 0.10) / 125 - $shotPower * 0.75, 0.05, 0.72);
  if (rand_float() < $catchChance) {
    set_owner($match, $defendingSide, 0);
    place_ball_on_player($ball, $keeper, $defendingSide);
    $match['events'][] = ['t' => round((float)$match['elapsed']), 'text' => ($match['teams'][$defendingSide]['name'] ?? strtoupper($defendingSide)) . ' KEEPER HOLD'];
  } else {
    clear_owner($match);
    $awayFromGoal = $nearTop ? 1 : -1;
    $ball['vx'] = ((float)$ball['x'] - 0.5) * 0.18 + (rand_float() * 2 - 1) * 0.08;
    $ball['vy'] = $awayFromGoal * (0.13 + (player_stat($keeper, 'keeping') + player_stat($keeper, 'strength') * 0.35) / 1050);
    $ball['vz'] = 0.06 + rand_float() * 0.10;
    $match['engine']['shot'] = null;
    $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + 0.30;
    $match['events'][] = ['t' => round((float)$match['elapsed']), 'text' => ($match['teams'][$defendingSide]['name'] ?? strtoupper($defendingSide)) . ' KEEPER SAVE'];
  }
  return true;
}

function move_players(array &$match, array $ball, float $step): void {
  $owner = owner_ref($match);
  $clock = (float)($match['elapsed'] ?? 0);
  $nearest = [
    'home' => nearest_role_index($match['units']['home'] ?? [], $ball, 'home'),
    'away' => nearest_role_index($match['units']['away'] ?? [], $ball, 'away'),
  ];
  foreach (['home', 'away'] as $side) {
    if (empty($match['units'][$side])) continue;
    $mode = $match['tactics'][$side] ?? 'balanced';
    $lineShift = match ($mode) {
      'attack' => $side === 'home' ? -0.055 : 0.055,
      'defend' => $side === 'home' ? 0.055 : -0.055,
      default => 0.0,
    };
    foreach ($match['units'][$side] as $index => &$player) {
      $role = (string)($player['role'] ?? 'CM');
      $oldX = (float)($player['x'] ?? ($player['homeX'] ?? 0.5));
      $oldY = (float)($player['y'] ?? ($player['homeY'] ?? 0.5));
      $speedStat = player_stat($player, 'speed');
      $stamina = player_stat($player, 'stamina');
      $workrate = player_stat($player, 'workrate');
      $positioning = player_stat($player, 'positioning');
      $gameIq = ($speedStat * 0.22 + player_stat($player, 'passcontrol') * 0.16 + player_stat($player, 'tackling') * 0.17 + $positioning * 0.25 + $workrate * 0.20);
      $speed = 0.044 + ($speedStat / 100) * 0.096 + ($stamina / 100) * 0.018;
      $isOwner = $owner && $owner['side'] === $side && $owner['index'] === $index;
      $isChaser = $nearest[$side] === $index;
      $distToBall = sqrt(distance2($player, $ball));
      $zoneMargin = match ($role) {
        'GK' => 0.04,
        'CB', 'RB', 'LB' => 0.055 + ($speedStat + $positioning) / 3000,
        'ST' => 0.075 + ($speedStat + $positioning) / 2800,
        default => 0.085 + ($speedStat + $positioning) / 2600,
      };
      $canJoinPlay = role_zone_contains($side, $role, (float)$ball['y'], $zoneMargin + (player_stat($player, 'tackling') + $workrate) / 3300);
      $nearRadius = 0.10 + $gameIq / 800;
      $isNearPlay = $canJoinPlay && $distToBall < $nearRadius;
      $urgent = $isOwner || $isChaser;
      if (($player['role'] ?? '') === 'GK') {
        $isThreat = $side === 'home' ? (float)$ball['y'] > 0.50 : (float)$ball['y'] < 0.50;
        $targetX = $isThreat ? clamp_float((float)$ball['x'], goal_min_x(), goal_max_x()) : 0.5;
        $targetY = (float)$player['homeY'];
        $speed *= $isThreat ? 0.70 : 0.22;
      } elseif ($isOwner) {
        $dir = attack_dir($side);
        if (in_array($role, ['RM', 'LM', 'RB', 'LB'], true)) {
          $wideBias = ((float)$player['homeX'] < 0.5) ? -0.020 : 0.020;
          $targetX = clamp_float((float)$player['x'] + ((float)$player['homeX'] - (float)$player['x']) * 0.18 + $wideBias, 0.05, 0.95);
        } else {
          $targetX = clamp_float((float)$player['x'] + (0.5 - (float)$player['x']) * 0.10, 0.06, 0.94);
        }
        $targetY = clamp_role_y($side, $role, (float)$player['y'] + $dir * (0.11 + (player_stat($player, 'shotcontrol') + player_stat($player, 'dribbling')) / 200 * 0.05), $zoneMargin + 0.14);
        $speed *= 0.88 + (player_stat($player, 'shotcontrol') + player_stat($player, 'dribbling')) / 560;
      } elseif ($owner && $owner['side'] !== $side) {
        $ownerPlayer = $match['units'][$owner['side']][$owner['index']];
        $support = AiDefensiveLane($player, $ownerPlayer, $side);
        $targetX = $support['x'];
        $targetY = $support['y'];
        if ($isChaser) {
          $targetX = (float)$ownerPlayer['x'];
          $targetY = clamp_role_y($side, $role, (float)$ownerPlayer['y'], $zoneMargin + 0.12);
          $speed *= 1.28;
          $urgent = true;
        } elseif ($isNearPlay) {
          $targetX = clamp_float($support['x'], 0.06, 0.94);
          $targetY = clamp_role_y($side, $role, $support['y'], $zoneMargin);
          $speed *= 0.26 + $gameIq / 420;
        } else {
          $targetX = clamp_float((float)$player['homeX'] + ((float)$ball['x'] - 0.5) * 0.04, 0.06, 0.94);
          $targetY = clamp_role_y($side, $role, (float)$player['homeY'] + $lineShift * 0.35, $zoneMargin * 0.6);
          $speed *= 0.10 + $gameIq / 900;
        }
      } elseif ($isChaser) {
        $lead = 0.20 + player_stat($player, 'speed') / 450;
        $targetX = clamp_float((float)$ball['x'] + (float)($ball['vx'] ?? 0) * $lead, 0.03, 0.97);
        $targetY = clamp_role_y($side, $role, (float)$ball['y'] + (float)($ball['vy'] ?? 0) * $lead, $zoneMargin + 0.10);
        $speed *= 1.35;
        $urgent = true;
      } elseif ($isNearPlay) {
        $targetX = clamp_float((float)$player['homeX'] + ((float)$ball['x'] - 0.5) * 0.10, 0.06, 0.94);
        $targetY = clamp_role_y($side, $role, (float)$player['homeY'] + ((float)$ball['y'] - (float)$player['homeY']) * 0.14 + $lineShift * 0.55, $zoneMargin);
        $speed *= 0.18 + $gameIq / 500;
      } else {
        $targetX = clamp_float((float)$player['homeX'] + ((float)$ball['x'] - 0.5) * 0.035, 0.06, 0.94);
        $targetY = clamp_role_y($side, $role, (float)$player['homeY'] + $lineShift * 0.30, $zoneMargin * 0.5);
        $speed *= 0.08 + $gameIq / 1050;
      }

      if (!$urgent && $role !== 'GK') {
        $phaseBase = $clock * 0.75 + $index * 2.4 + ($side === 'away' ? 1.1 : 0.0);
        $targetX = clamp_float($targetX + sin($phaseBase) * 0.006, 0.05, 0.95);
        $targetY = clamp_role_y($side, $role, $targetY + sin($clock * 0.62 + $index * 1.7) * 0.004, $zoneMargin * 0.55);
      }

      if (!$urgent) {
        $dx = $targetX - (float)$player['x'];
        $dy = $targetY - (float)$player['y'];
        $targetDist = sqrt($dx * $dx + $dy * $dy);
        if ($targetDist < ($isNearPlay ? 0.018 : 0.034)) {
          telemetry_record_movement($match, $side, $player, $oldX, $oldY, $step, $isNearPlay, $urgent);
          continue;
        }
        $reactionWindow = 0.20 + $gameIq / 420;
        $phase = fmod($clock + $index * 0.17 + ($side === 'away' ? 0.09 : 0.0), 0.90);
        if (!$isNearPlay && $phase > $reactionWindow) {
          telemetry_record_movement($match, $side, $player, $oldX, $oldY, $step, $isNearPlay, $urgent);
          continue;
        }
      }
      move_towards($player, $targetX, $targetY, $speed, $step);
      telemetry_record_movement($match, $side, $player, $oldX, $oldY, $step, $isNearPlay, $urgent);
    }
    unset($player);
  }
}

function AiDefensiveLane(array $player, array $threat, string $defendingSide): array {
  $ownGoalY = $defendingSide === 'home' ? 1.0 : 0.0;
  $coverX = (float)$threat['x'] + (0.5 - (float)$threat['x']) * 0.45;
  $coverY = (float)$threat['y'] + ($ownGoalY - (float)$threat['y']) * 0.22;
  $lane = ((float)$player['homeX'] - 0.5) * 0.12;
  return [
    'x' => clamp_float($coverX + $lane, 0.06, 0.94),
    'y' => clamp_float($coverY, 0.06, 0.94),
  ];
}

function apply_player_touch(array &$match, array &$ball, float $step): void {
  $owner = owner_ref($match);
  if ($owner) {
    $side = $owner['side'];
    $index = $owner['index'];
    $player = &$match['units'][$side][$index];
    $match['engine']['possession'][$side] += $step;
    telemetry_add($match, $side, $player, ['possessionSeconds' => $step]);
    $match['engine']['lastTouch'] = $side . ':' . ($player['id'] ?? $index);
    place_ball_on_player($ball, $player, $side);
    $canUseBall = ball_touches_player_circle($ball, $player, 0.003);

    $opponent = opponent_side($side);
    $defIndex = nearest_index($match['units'][$opponent] ?? [], $ball);
    if ($defIndex !== null) {
      $defender = $match['units'][$opponent][$defIndex];
      if (ball_touches_player_circle($ball, $defender, 0.006) && (float)$match['elapsed'] >= (float)($match['engine']['ballFreeUntil'] ?? 0)) {
        $defAggression = player_stat($defender, 'aggression');
        $defDiscipline = player_stat($defender, 'discipline');
        $tackle = player_stat($defender, 'tackling') * 0.46 + player_stat($defender, 'speed') * 0.16 + player_stat($defender, 'strength') * 0.16 + player_stat($defender, 'positioning') * 0.12 + $defAggression * 0.10 + mt_rand(0, 20);
        $control = player_stat($player, 'passcontrol') * 0.25 + player_stat($player, 'shotcontrol') * 0.25 + player_stat($player, 'dribbling') * 0.22 + player_stat($player, 'composure') * 0.16 + player_stat($player, 'strength') * 0.07 + player_stat($player, 'speed') * 0.05 + mt_rand(0, 20);
        if ($tackle > $control) {
          $match['engine']['tackles'][$opponent]++;
          telemetry_add($match, $opponent, $defender, ['tackles' => 1, 'touches' => 1]);
          telemetry_add($match, $side, $player, ['lostBalls' => 1]);
          telemetry_maybe_complain($match, $side, $player, 0.16);
          set_owner($match, $opponent, $defIndex);
          place_ball_on_player($ball, $match['units'][$opponent][$defIndex], $opponent);
          $match['events'][] = ['t' => round((float)$match['elapsed']), 'text' => ($match['teams'][$opponent]['name'] ?? strtoupper($opponent)) . ' TACKLE'];
          unset($player);
          return;
        } else {
          $foulChance = clamp_float(0.08 + ($control - $tackle) / 125 + (100 - player_stat($defender, 'tackling')) / 430 + $defAggression / 420 - $defDiscipline / 360, 0.03, 0.62);
          if (rand_float() < $foulChance) {
            $yellowChance = clamp_float(0.03 + ($control - $tackle) / 200 + $defAggression / 360 + (100 - $defDiscipline) / 420, 0.015, 0.32);
            award_foul($match, $side, $opponent, $ball, rand_float() < $yellowChance, $defender, $player);
            unset($player);
            return;
          }
          $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + 0.24;
        }
      }
    }

    if ($canUseBall && (float)$match['elapsed'] >= (float)($match['engine']['nextDecisionAt'] ?? 0)) {
      $shotProbability = shot_probability($match, $side, $player);
      $receiver = choose_receiver($match, $side, $player);
      $passProbability = $receiver ? clamp_float(0.03 + $receiver['score'] * 0.84 + (player_stat($player, 'passing') + player_stat($player, 'vision')) / 420 + player_stat($player, 'composure') / 520 + pressure_on($match, $side, (float)$player['x'], (float)$player['y']) * 0.22, 0.0, 0.96) : 0.0;
      $match['engine']['nextDecisionAt'] = (float)$match['elapsed'] + 0.18 + rand_float() * 0.24;
      $isStriker = ($player['role'] ?? '') === 'ST';
      if ($isStriker && $shotProbability > 0.12 && rand_float() < clamp_float($shotProbability + 0.24, 0.0, 0.94)) {
        release_shot($match, $side, $index);
        $ball = $match['ball'];
      } elseif ($receiver && rand_float() < $passProbability && ($shotProbability < 0.68 || rand_float() < 0.40)) {
        release_pass($match, $side, $index, $receiver);
        $ball = $match['ball'];
      } elseif ($shotProbability > 0 && rand_float() < $shotProbability) {
        release_shot($match, $side, $index);
        $ball = $match['ball'];
      } elseif ($receiver && rand_float() < $passProbability * 0.45) {
        release_pass($match, $side, $index, $receiver);
        $ball = $match['ball'];
      }
    }
    unset($player);
    return;
  }

  if ((float)$match['elapsed'] < (float)($match['engine']['ballFreeUntil'] ?? 0)) return;
  $isHigh = (float)($ball['z'] ?? 0) > 0.08;
  $incomingSpeed = sqrt((float)($ball['vx'] ?? 0) ** 2 + (float)($ball['vy'] ?? 0) ** 2);
  $interceptExtra = clamp_float($incomingSpeed * 0.018, 0.0, 0.010);
  $target = $match['engine']['passTarget'] ?? null;
  $bestSide = null;
  $bestIndex = null;
  $bestScore = PHP_FLOAT_MAX;
  foreach (['home', 'away'] as $side) {
    foreach ($match['units'][$side] ?? [] as $candidate => $candidatePlayer) {
      $touchRadius = player_touch_radius($candidatePlayer, $isHigh) + $interceptExtra;
      $dist = distance2($candidatePlayer, $ball);
      if ($dist > $touchRadius * $touchRadius) continue;
      $targeted = is_array($target) && ($target['side'] ?? '') === $side && ($target['id'] ?? '') === ($candidatePlayer['id'] ?? null);
      $score = $dist / max(0.00001, $touchRadius * $touchRadius)
        - player_touch_control($candidatePlayer, $isHigh) / 700
        - ($targeted ? 0.18 : 0.0);
      if ($score < $bestScore) {
        $bestScore = $score;
        $bestSide = $side;
        $bestIndex = (int)$candidate;
      }
    }
  }
  if ($bestSide === null || $bestIndex === null) return;
  $player = $match['units'][$bestSide][$bestIndex];
  $controlStat = player_touch_control($player, $isHigh);

  $match['engine']['possession'][$bestSide] += $step;
  telemetry_add($match, $bestSide, $player, ['possessionSeconds' => $step, 'touches' => 1]);
  $touchKey = $bestSide . ':' . ($player['id'] ?? $bestIndex);
  if (($match['engine']['lastTouch'] ?? null) !== $touchKey || ((float)$match['elapsed'] - (float)($match['engine']['lastTouchAt'] ?? -10)) > 0.6) {
    $match['engine']['touches'][$bestSide]++;
    $match['engine']['lastTouch'] = $touchKey;
    $match['engine']['lastTouchAt'] = (float)$match['elapsed'];
  }

  $targeted = is_array($target) && ($target['side'] ?? '') === $bestSide && ($target['id'] ?? '') === ($player['id'] ?? null);
  $quality = is_array($target) ? (float)($target['quality'] ?? 0.5) : 0.45;
  $catchChance = clamp_float(0.18 + $controlStat / 130 + ($targeted ? $quality * 0.34 : 0.0) - $incomingSpeed * 0.70, 0.05, 0.96);
  if (($player['role'] ?? '') === 'GK') $catchChance = clamp_float($catchChance + player_stat($player, 'keeping') / 220, 0.10, 0.98);
  if (rand_float() < $catchChance) {
    set_owner($match, $bestSide, $bestIndex);
    place_ball_on_player($ball, $player, $bestSide);
    $match['ball'] = $ball;
    $match['engine']['passTarget'] = null;
    return;
  }

  $attackDir = attack_dir($bestSide);
  $skill = $isHigh ? player_stat($player, 'heading') : (player_stat($player, 'passcontrol') * 0.7 + player_stat($player, 'composure') * 0.3);
  $looseTouch = player_ball_release_point($player, $bestSide, 0.82);
  $ball['x'] = $looseTouch['x'];
  $ball['y'] = $looseTouch['y'];
  $ball['vx'] = (rand_float() * 2 - 1) * (0.05 + (100 - $skill) / 900);
  $ball['vy'] = $attackDir * (0.09 + $skill / 900);
  $ball['vz'] = $isHigh ? 0.05 : 0.0;
  $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + 0.26;
}

function tick_match_fixed_dt(array &$match, float $dt, bool $updateWallClock = false): void {
  if (($match['status'] ?? '') !== 'active') return;
  ensure_match_engine($match);
  $dt = max(0.0, min(2.0, $dt));
  if ($dt <= 0) return;
  if ($updateWallClock) {
    $match['lastTick'] = microtime(true);
    $match['updated'] = time();
  }
  $steps = max(1, (int)ceil($dt / 0.05));
  $step = $dt / $steps;
  $clockStep = $step * (MATCH_GAME_MINUTES / MATCH_REAL_SECONDS);
  $goalMin = goal_min_x();
  $goalMax = goal_max_x();
  for ($i = 0; $i < $steps; $i++) {
    $ball = $match['ball'];
    $kickoffUntil = (float)($match['engine']['kickoffUntil'] ?? 0.0);
    if ($kickoffUntil > (float)$match['elapsed']) {
      $match['ball'] = stopped_kickoff_ball();
      $match['elapsed'] += $clockStep;
      continue;
    }
    if (!empty($match['engine']['kickoffDirection'])) {
      $kickoffSide = in_array($match['engine']['kickoffSide'] ?? '', ['home', 'away'], true)
        ? (string)$match['engine']['kickoffSide']
        : (((int)$match['engine']['kickoffDirection']) > 0 ? 'away' : 'home');
      if (empty($match['engine']['kickoffWhistled'])) {
        $match['engine']['kickoffWhistled'] = true;
        $match['engine']['kickoffWhistleUntil'] = (float)$match['elapsed'] + 0.42;
        $match['engine']['kickoffWhistleSeq'] = (int)($match['engine']['kickoffWhistleSeq'] ?? 0) + 1;
        $match['ball'] = stopped_kickoff_ball();
        $match['elapsed'] += $clockStep;
        continue;
      }
      if ((float)($match['engine']['kickoffWhistleUntil'] ?? 0.0) > (float)$match['elapsed']) {
        arrange_kickoff_players($match, $kickoffSide, $step);
        $match['ball'] = stopped_kickoff_ball();
        $match['elapsed'] += $clockStep;
        continue;
      }
      $ball = kickoff_ball_for_side($match, $kickoffSide);
      $match['ball'] = $ball;
      $match['engine']['kickoffDirection'] = null;
      $match['engine']['kickoffUntil'] = 0.0;
      $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + 0.18;
    }
    if (!empty($match['engine']['restartType'])) {
      $restartType = (string)$match['engine']['restartType'];
      $restartSide = in_array($match['engine']['restartSide'] ?? '', ['home', 'away'], true) ? (string)$match['engine']['restartSide'] : 'home';
      $restartSpot = is_array($match['engine']['restartSpot'] ?? null) ? $match['engine']['restartSpot'] : ['x' => 0.5, 'y' => 0.5];
      $restartBall = stopped_ball_at((float)($restartSpot['x'] ?? 0.5), (float)($restartSpot['y'] ?? 0.5));
      $restartStarted = (float)($match['engine']['restartStartedAt'] ?? $match['elapsed']);
      $restartLine = is_array($match['engine']['restartLineBall'] ?? null) ? $match['engine']['restartLineBall'] : $restartBall;
      $restartOut = is_array($match['engine']['restartOutBall'] ?? null) ? $match['engine']['restartOutBall'] : $restartBall;
      $outBall = [
        'x' => (float)($restartOut['x'] ?? $restartBall['x']),
        'y' => (float)($restartOut['y'] ?? $restartBall['y']),
        'z' => 0.0,
        'vx' => 0.0,
        'vy' => 0.0,
        'vz' => 0.0,
      ];
      if ((float)($match['engine']['restartRollUntil'] ?? 0.0) > (float)$match['elapsed']) {
        $rollSpan = max(0.001, (float)($match['engine']['restartRollUntil'] ?? $match['elapsed']) - $restartStarted);
        $rollT = clamp_float(((float)$match['elapsed'] - $restartStarted) / $rollSpan, 0.0, 1.0);
        $rollT = $rollT * $rollT * (3.0 - 2.0 * $rollT);
        $match['ball'] = [
          'x' => (float)($restartLine['x'] ?? $outBall['x']) + ($outBall['x'] - (float)($restartLine['x'] ?? $outBall['x'])) * $rollT,
          'y' => (float)($restartLine['y'] ?? $outBall['y']) + ($outBall['y'] - (float)($restartLine['y'] ?? $outBall['y'])) * $rollT,
          'z' => 0.0,
          'vx' => 0.0,
          'vy' => 0.0,
          'vz' => 0.0,
        ];
        $match['elapsed'] += $clockStep;
        continue;
      }
      if ((float)($match['engine']['restartDeliveryUntil'] ?? 0.0) > (float)$match['elapsed']) {
        $match['ball'] = $outBall;
        $match['elapsed'] += $clockStep;
        continue;
      }
      if ((float)($match['engine']['restartUntil'] ?? 0.0) > (float)$match['elapsed']) {
        $match['ball'] = $restartBall;
        $match['elapsed'] += $clockStep;
        continue;
      }
      if (empty($match['engine']['restartWhistled'])) {
        $match['engine']['restartWhistled'] = true;
        $match['engine']['restartSignalUntil'] = (float)$match['elapsed'] + 0.42;
        $match['engine']['restartSeq'] = (int)($match['engine']['restartSeq'] ?? 0) + 1;
        $match['ball'] = $restartBall;
        $match['elapsed'] += $clockStep;
        continue;
      }
      if ((float)($match['engine']['restartSignalUntil'] ?? 0.0) > (float)$match['elapsed']) {
        $match['ball'] = $restartBall;
        $match['elapsed'] += $clockStep;
        continue;
      }
      take_restart($match, $restartSide, $restartType, $restartBall);
      $ball = $match['ball'];
    }
    $penaltySide = in_array($match['engine']['penaltySide'] ?? '', ['home', 'away'], true) ? (string)$match['engine']['penaltySide'] : '';
    if ($penaltySide !== '') {
      $spot = penalty_spot_for_side($penaltySide);
      $penaltyBall = stopped_ball_at($spot['x'], $spot['y']);
      if ((float)($match['engine']['penaltyUntil'] ?? 0.0) > (float)$match['elapsed']) {
        prepare_penalty_units($match, $penaltySide);
        $match['ball'] = $penaltyBall;
        $match['elapsed'] += $clockStep;
        continue;
      }
      if (empty($match['engine']['penaltyWhistled'])) {
        $match['engine']['penaltyWhistled'] = true;
        $match['engine']['penaltySignalUntil'] = (float)$match['elapsed'] + 0.42;
        $match['engine']['restartSeq'] = (int)($match['engine']['restartSeq'] ?? 0) + 1;
        $match['ball'] = $penaltyBall;
        $match['elapsed'] += $clockStep;
        continue;
      }
      if ((float)($match['engine']['penaltySignalUntil'] ?? 0.0) > (float)$match['elapsed']) {
        $match['ball'] = $penaltyBall;
        $match['elapsed'] += $clockStep;
        continue;
      }
      take_penalty($match, $penaltySide);
      $ball = $match['ball'];
    }
    $freeKickUntil = (float)($match['engine']['freeKickUntil'] ?? 0.0);
    if ($freeKickUntil > (float)$match['elapsed']) {
      $spot = is_array($match['engine']['freeKickSpot'] ?? null) ? $match['engine']['freeKickSpot'] : ['x' => 0.5, 'y' => 0.5];
      $fkSide = in_array($match['engine']['freeKickSide'] ?? '', ['home', 'away'], true) ? $match['engine']['freeKickSide'] : 'home';
      arrange_free_kick_players($match, $fkSide, $spot, $step);
      $match['ball'] = stopped_ball_at((float)($spot['x'] ?? 0.5), (float)($spot['y'] ?? 0.5));
      $match['elapsed'] += $clockStep;
      continue;
    }
    $freeKickSignalUntil = (float)($match['engine']['freeKickSignalUntil'] ?? 0.0);
    if ($freeKickSignalUntil > (float)$match['elapsed']) {
      $spot = is_array($match['engine']['freeKickSpot'] ?? null) ? $match['engine']['freeKickSpot'] : ['x' => 0.5, 'y' => 0.5];
      $fkSide = in_array($match['engine']['freeKickSide'] ?? '', ['home', 'away'], true) ? $match['engine']['freeKickSide'] : 'home';
      arrange_free_kick_players($match, $fkSide, $spot, $step);
      $match['ball'] = stopped_ball_at((float)($spot['x'] ?? 0.5), (float)($spot['y'] ?? 0.5));
      $match['elapsed'] += $clockStep;
      continue;
    }
    if (!empty($match['engine']['freeKickSide'])) {
      $fkSide = in_array($match['engine']['freeKickSide'], ['home', 'away'], true) ? $match['engine']['freeKickSide'] : 'home';
      $spot = is_array($match['engine']['freeKickSpot'] ?? null) ? $match['engine']['freeKickSpot'] : ['x' => 0.5, 'y' => 0.5];
      $ball = stopped_ball_at((float)($spot['x'] ?? 0.5), (float)($spot['y'] ?? 0.5));
      $kickIndex = free_kick_kicker_index($match, $fkSide, $ball);
      if ($kickIndex !== null && isset($match['units'][$fkSide][$kickIndex])) {
        $dist = sqrt(distance2($match['units'][$fkSide][$kickIndex], $ball));
        if ($dist > 0.035 && (float)$match['elapsed'] < max($freeKickUntil, $freeKickSignalUntil) + 0.90) {
          arrange_free_kick_players($match, $fkSide, $ball, $step);
          $match['ball'] = $ball;
          $match['elapsed'] += $clockStep;
          continue;
        }
        $match['units'][$fkSide][$kickIndex]['x'] = $ball['x'];
        $match['units'][$fkSide][$kickIndex]['y'] = clamp_float((float)$ball['y'], 0.03, 0.97);
        take_free_kick($match, $fkSide, $kickIndex);
        $ball = $match['ball'];
      } else {
        $match['engine']['freeKickSide'] = null;
        $match['engine']['freeKickSpot'] = null;
        $match['engine']['freeKickKicker'] = null;
        $match['engine']['freeKickUntil'] = 0.0;
        $match['engine']['freeKickSignalUntil'] = 0.0;
      }
      $match['ball'] = $ball;
    }
    $shortLineRunoff = ball_short_line_crossed($ball) && !ball_short_line_visible_out($ball);
    if ($shortLineRunoff) clear_owner($match);
    if (!$shortLineRunoff) {
      move_players($match, $ball, $step);
      apply_player_touch($match, $ball, $step);
      if (owner_ref($match)) {
        $match['ball'] = $ball;
        $match['elapsed'] += $clockStep;
        continue;
      }
    }
    $hasShot = !empty($match['engine']['shot']);
    $airPass = is_array($match['engine']['passTarget'] ?? null)
      && (($match['engine']['passTarget']['type'] ?? '') === 'long')
      && ((float)($ball['z'] ?? 0.0) > 0.0 || (float)($ball['vz'] ?? 0.0) > 0.0);
    $airborne = $hasShot || $airPass;
    if (!$airborne) {
      $ball['z'] = 0.0;
      $ball['vz'] = 0.0;
    }
    $ball['vx'] = (float)($ball['vx'] ?? 0.0);
    $ball['vy'] = (float)($ball['vy'] ?? 0.0);
    $ball['vz'] = (float)($ball['vz'] ?? 0.0);
    $speed = sqrt($ball['vx'] * $ball['vx'] + $ball['vy'] * $ball['vy']);
    if ($speed > BALL_MAX_SPEED) {
      $ball['vx'] *= BALL_MAX_SPEED / $speed;
      $ball['vy'] *= BALL_MAX_SPEED / $speed;
      $speed = BALL_MAX_SPEED;
    }
    $ball['x'] += $ball['vx'] * $step;
    $ball['y'] += $ball['vy'] * $step;

    if ($airborne) {
      $ball['z'] = max(0.0, (float)($ball['z'] ?? 0.0) + $ball['vz'] * $step);
      if ($ball['z'] > 0.0 || $ball['vz'] > 0.0) {
        $ball['vz'] -= BALL_GRAVITY * $step;
        $drag = exp(-BALL_AIR_DRAG * $step);
        $ball['vx'] *= $drag;
        $ball['vy'] *= $drag;
      }
      if ($ball['z'] <= 0.0 && $ball['vz'] < 0.0) {
        $ball['z'] = 0.0;
        if ($airPass) {
          $ball['vz'] = 0.0;
          $ball['vx'] *= 0.82;
          $ball['vy'] *= 0.82;
        } else {
          $ball['vz'] *= -BALL_BOUNCE;
          $ball['vx'] *= 0.92;
          $ball['vy'] *= 0.92;
          if (abs($ball['vz']) < 0.022) $ball['vz'] = 0.0;
        }
      }
    }

    if (!$airborne || (float)($ball['z'] ?? 0.0) <= 0.0) {
      $ball['z'] = 0.0;
      if (!$hasShot || $ball['vz'] <= 0.0) $ball['vz'] = 0.0;
      $speed = sqrt($ball['vx'] * $ball['vx'] + $ball['vy'] * $ball['vy']);
      if ($speed > 0.0) {
        $newSpeed = max(0.0, $speed - ($hasShot ? BALL_SHOT_ROLL_DECEL : BALL_ROLL_DECEL) * $step);
        if ($newSpeed < BALL_STOP_SPEED) {
          $ball['vx'] = 0.0;
          $ball['vy'] = 0.0;
        } else {
          $ball['vx'] *= $newSpeed / $speed;
          $ball['vy'] *= $newSpeed / $speed;
        }
      }
      if ($hasShot && sqrt($ball['vx'] * $ball['vx'] + $ball['vy'] * $ball['vy']) < 0.035) {
        $match['engine']['shot'] = null;
      }
    }
    if ($ball['x'] < 0.02 || $ball['x'] > 0.98) {
      $ball['x'] = max(0.02, min(0.98, $ball['x']));
      $ball['vx'] = 0.0;
      $ball['vy'] *= 0.82;
    }
    if (!ball_short_line_crossed($ball) && apply_keeper_save($match, $ball)) {
      $match['ball'] = $ball;
      $match['elapsed'] += $clockStep;
      continue;
    }
    if (ball_short_line_crossed($ball) && (ball_short_line_visible_out($ball) || ball_short_line_stalled_out($ball))) {
      $topLine = $ball['y'] < 0;
      $inGoal = $ball['x'] >= $goalMin && $ball['x'] <= $goalMax;
      $scoringSide = $topLine ? 'home' : 'away';
      $kickoffSide = $topLine ? 'away' : 'home';
      $scorer = null;
      if ($inGoal && $topLine) {
        $match['score'][0]++;
        $scorer = goal_scorer_name($match, 'home');
      } elseif ($inGoal) {
        $match['score'][1]++;
        $scorer = goal_scorer_name($match, 'away');
      }
      if ($inGoal) {
        $scorer = $scorer ?? goal_scorer_name($match, $scoringSide);
        $shot = $match['engine']['shot'] ?? null;
        $isPenaltyGoal = is_array($shot) && !empty($shot['penalty']);
        $assist = $isPenaltyGoal ? null : goal_assist_name($match, $scoringSide, $scorer);
        $goalText = side_team_name($match, $scoringSide) . ' GOAL' . ($scorer ? ' - ' . $scorer : '') . ($assist ? ' | ASSIST: ' . $assist : '');
        $match['events'][] = ['t' => round((float)$match['elapsed']), 'text' => $goalText];
        $match['engine']['goalSeq'] = (int)($match['engine']['goalSeq'] ?? 0) + 1;
        $match['engine']['lastGoal'] = [
          'seq' => $match['engine']['goalSeq'],
          'at' => (float)$match['elapsed'],
          'side' => $scoringSide,
          'team' => side_team_name($match, $scoringSide),
          'scorer' => $scorer,
          'assist' => $assist,
          'penalty' => $isPenaltyGoal,
        ];
        $match['engine']['goalScorers'][] = $match['engine']['lastGoal'];
      }
      clear_owner($match);
      $match['engine']['passTarget'] = null;
      $match['engine']['shot'] = null;
      if ($inGoal) {
        begin_kickoff($match, $kickoffSide, KICKOFF_PAUSE_GAME_MINUTES);
        $ball = $match['ball'];
      } else {
        $attackingSide = $topLine ? 'home' : 'away';
        $defendingSide = opponent_side($attackingSide);
        $lastSide = last_touch_side($match) ?? $attackingSide;
        if ($lastSide === $attackingSide) {
          $spot = ['x' => 0.5, 'y' => $topLine ? 0.085 : 0.915];
          $outBall = ['x' => clamp_float((float)$ball['x'], 0.02, 0.98), 'y' => $topLine ? -0.26 : 1.26];
          begin_restart($match, 'goal_kick', $defendingSide, $spot, $outBall, $ball);
        } else {
          $spot = [
            'x' => (float)$ball['x'] < 0.5 ? 0.018 : 0.982,
            'y' => $topLine ? 0.018 : 0.982,
          ];
          $outBall = ['x' => clamp_float((float)$ball['x'], 0.02, 0.98), 'y' => $topLine ? -0.26 : 1.26];
          begin_restart($match, 'corner', $attackingSide, $spot, $outBall, $ball);
        }
        $ball = $match['ball'];
      }
    }
    $match['ball'] = $ball;
    $match['elapsed'] += $clockStep;
  }
  if ($match['elapsed'] >= MATCH_GAME_MINUTES) {
    $match['elapsed'] = MATCH_GAME_MINUTES;
    $match['status'] = 'finished';
  }
  if (count($match['events']) > 8) $match['events'] = array_slice($match['events'], -8);
}

function tick_match(array &$match): void {
  if (($match['status'] ?? '') !== 'active') return;
  $now = microtime(true);
  $dt = max(0.0, min(2.0, $now - (float)($match['lastTick'] ?? $now)));
  tick_match_fixed_dt($match, $dt, true);
}

function public_referee(array $match): array {
  $engine = $match['engine'] ?? default_engine();
  $ball = $match['ball'] ?? ['x' => 0.5, 'y' => 0.5];
  $freeKickSpot = is_array($engine['freeKickSpot'] ?? null) ? $engine['freeKickSpot'] : null;
  $restartSpot = is_array($engine['restartSpot'] ?? null) ? $engine['restartSpot'] : null;
  $penaltySpot = in_array($engine['penaltySide'] ?? '', ['home', 'away'], true) ? penalty_spot_for_side((string)$engine['penaltySide']) : null;
  $spot = $freeKickSpot ?: ($penaltySpot ?: $restartSpot);
  $x = 0.975;
  $kickoffWhistle = (float)($engine['kickoffWhistleUntil'] ?? 0.0) > (float)($match['elapsed'] ?? 0.0);
  $restartWhistle = (float)($engine['restartSignalUntil'] ?? 0.0) > (float)($match['elapsed'] ?? 0.0);
  $penaltyWhistle = (float)($engine['penaltySignalUntil'] ?? 0.0) > (float)($match['elapsed'] ?? 0.0);
  $y = $spot ? (float)($spot['y'] ?? 0.5) : ($kickoffWhistle ? 0.5 : (float)($ball['y'] ?? 0.5) + 0.03);
  $card = null;
  $whistle = $kickoffWhistle || $restartWhistle || $penaltyWhistle;
  $elapsed = (float)($match['elapsed'] ?? 0);
  foreach (array_reverse($match['events'] ?? []) as $event) {
    $age = $elapsed - (float)($event['t'] ?? $elapsed);
    $text = (string)($event['text'] ?? '');
    if ($age > 1.6) break;
    if (str_contains($text, 'YELLOW CARD')) $card = 'yellow';
    if (str_contains($text, 'RED CARD')) $card = 'red';
    if (str_contains($text, 'FREE KICK') || str_contains($text, 'PENALTY') || str_contains($text, 'GOAL')) $whistle = true;
  }
  return [
    'x' => clamp_float($x, 0.02, 0.98),
    'y' => clamp_float($y, 0.08, 0.92),
    'card' => $card,
    'whistle' => $whistle,
  ];
}

function public_match(array $match, string $user): array {
  ensure_match_engine($match);
  $side = (($match['players']['home'] ?? null) === $user) ? 'home' : 'away';
  $engine = $match['engine'] ?? default_engine();
  return [
    'id' => $match['id'],
    'status' => $match['status'],
    'side' => $side,
    'elapsed' => round((float)($match['elapsed'] ?? 0), 2),
    'score' => $match['score'],
    'players' => $match['players'],
    'teams' => $match['teams'],
    'units' => $match['units'] ?? ['home' => [], 'away' => []],
    'bench' => $match['bench'] ?? ['home' => [], 'away' => []],
    'ratings' => [
      'home' => average_rating($match['units']['home'] ?? []),
      'away' => average_rating($match['units']['away'] ?? []),
    ],
    'managerFixture' => !empty($match['managerFixture']) ? [
      'seasonId' => (string)($match['managerFixture']['seasonId'] ?? ''),
      'roundIndex' => (int)($match['managerFixture']['roundIndex'] ?? 0),
      'fixtureIndex' => (int)($match['managerFixture']['fixtureIndex'] ?? 0),
    ] : null,
    'engine' => [
      'touches' => $engine['touches'] ?? ['home' => 0, 'away' => 0],
      'possession' => $engine['possession'] ?? ['home' => 0, 'away' => 0],
      'shots' => $engine['shots'] ?? ['home' => 0, 'away' => 0],
      'tackles' => $engine['tackles'] ?? ['home' => 0, 'away' => 0],
      'fouls' => $engine['fouls'] ?? ['home' => 0, 'away' => 0],
      'yellows' => $engine['yellows'] ?? ['home' => 0, 'away' => 0],
      'reds' => $engine['reds'] ?? ['home' => 0, 'away' => 0],
      'subsUsed' => $engine['subsUsed'] ?? ['home' => 0, 'away' => 0],
      'saves' => $engine['saves'] ?? ['home' => 0, 'away' => 0],
      'crits' => $engine['crits'] ?? ['home' => 0, 'away' => 0],
      'owner' => $engine['owner'] ?? null,
      'lastTouch' => $engine['lastTouch'] ?? null,
      'lastTouchAt' => $engine['lastTouchAt'] ?? null,
      'lastShotSeq' => $engine['lastShotSeq'] ?? 0,
      'lastShotAt' => $engine['lastShotAt'] ?? null,
      'lastShotBy' => $engine['lastShotBy'] ?? null,
      'lastGoal' => $engine['lastGoal'] ?? null,
      'goalScorers' => $engine['goalScorers'] ?? [],
      'passTarget' => $engine['passTarget'] ?? null,
      'shot' => $engine['shot'] ?? null,
      'kickoff' => [
        'side' => $engine['kickoffSide'] ?? null,
        'taker' => $engine['kickoffTaker'] ?? null,
        'receiver' => $engine['kickoffReceiver'] ?? null,
        'waiting' => !empty($engine['kickoffDirection']) && (float)($engine['kickoffUntil'] ?? 0.0) > (float)($match['elapsed'] ?? 0.0),
        'signal' => (float)($engine['kickoffWhistleUntil'] ?? 0.0) > (float)($match['elapsed'] ?? 0.0),
        'seq' => (int)($engine['kickoffWhistleSeq'] ?? 0),
      ],
      'restart' => [
        'type' => $engine['restartType'] ?? null,
        'side' => $engine['restartSide'] ?? null,
        'spot' => $engine['restartSpot'] ?? null,
        'line' => $engine['restartLineBall'] ?? null,
        'out' => $engine['restartOutBall'] ?? null,
        'kicker' => $engine['restartKicker'] ?? null,
        'startedAt' => isset($engine['restartStartedAt']) ? round((float)$engine['restartStartedAt'], 2) : null,
        'rollUntil' => isset($engine['restartRollUntil']) ? round((float)$engine['restartRollUntil'], 2) : null,
        'deliveryUntil' => isset($engine['restartDeliveryUntil']) ? round((float)$engine['restartDeliveryUntil'], 2) : null,
        'waiting' => !empty($engine['restartType']) && (float)($engine['restartUntil'] ?? 0.0) > (float)($match['elapsed'] ?? 0.0),
        'signal' => (float)($engine['restartSignalUntil'] ?? 0.0) > (float)($match['elapsed'] ?? 0.0),
        'seq' => (int)($engine['restartSeq'] ?? 0),
      ],
      'freeKick' => [
        'side' => $engine['freeKickSide'] ?? null,
        'spot' => $engine['freeKickSpot'] ?? null,
        'kicker' => $engine['freeKickKicker'] ?? null,
        'signal' => (float)($match['elapsed'] ?? 0.0) >= (float)($engine['freeKickUntil'] ?? 0.0)
          && (float)($engine['freeKickSignalUntil'] ?? 0.0) > (float)($match['elapsed'] ?? 0.0),
      ],
      'penalty' => [
        'side' => $engine['penaltySide'] ?? null,
        'kicker' => $engine['penaltyKicker'] ?? null,
        'spot' => in_array($engine['penaltySide'] ?? '', ['home', 'away'], true) ? penalty_spot_for_side((string)$engine['penaltySide']) : null,
        'signal' => (float)($engine['penaltySignalUntil'] ?? 0.0) > (float)($match['elapsed'] ?? 0.0),
      ],
    ],
    'referee' => public_referee($match),
    'tactics' => $match['tactics'],
    'ball' => $match['ball'],
    'events' => $match['events'] ?? [],
  ];
}

$data = body();
$action = $_GET['action'] ?? $data['action'] ?? 'state';
validate_request_method_and_csrf((string)$action, $data);

try {
  if ($action === 'logout') {
    session_destroy();
    reply(['ok' => true]);
  }

  if ($action === 'version') {
    reply(['ok' => true, 'appVersion' => app_version(), 'time' => time()]);
  }

  $result = with_state(function (&$state) use ($action, $data) {
    $now = time();

    if ($action === 'register') {
      $name = clean_name((string)($data['username'] ?? $data['name'] ?? ''));
      $pass = (string)($data['password'] ?? '');
      if (!preg_match('/^[a-z0-9_-]{3,24}$/', $name)) reply(['ok' => false, 'error' => 'bad_username'], 400);
      if (strlen($pass) < 3) reply(['ok' => false, 'error' => 'short_password'], 400);
      if (isset($state['users'][$name])) reply(['ok' => false, 'error' => 'user_exists'], 409);
      $state['users'][$name] = [
        'password' => password_hash($pass, PASSWORD_DEFAULT),
        'created' => $now,
        'lastSeen' => $now,
      ];
      session_regenerate_id(true);
      unset($_SESSION['csrfToken']);
      $_SESSION['user'] = $name;
    }

    if ($action === 'login') {
      $name = clean_name((string)($data['username'] ?? ''));
      $pass = (string)($data['password'] ?? '');
      if (!isset($state['users'][$name]) || !password_verify($pass, $state['users'][$name]['password'] ?? '')) {
        reply(['ok' => false, 'error' => 'bad_login'], 401);
      }
      session_regenerate_id(true);
      unset($_SESSION['csrfToken']);
      $_SESSION['user'] = $name;
      $state['users'][$name]['lastSeen'] = $now;
    }

    if ($action === 'changePassword') {
      $user = require_user($state);
      $old = (string)($data['oldPassword'] ?? '');
      $new = (string)($data['newPassword'] ?? '');
      if (!password_verify($old, $state['users'][$user]['password'] ?? '')) reply(['ok' => false, 'error' => 'bad_password'], 401);
      if (strlen($new) < 3) reply(['ok' => false, 'error' => 'short_password'], 400);
      $state['users'][$user]['password'] = password_hash($new, PASSWORD_DEFAULT);
    }

    if ($action === 'resetGameData' || $action === 'resetManagerLeague') {
      $user = require_user($state);
      if ($user !== 'jdc') reply(['ok' => false, 'error' => 'forbidden'], 403);
      reset_active_manager_game($state);
      return [
        'user' => public_user($user, $state['users'][$user]),
        'teams' => public_teams($state, $user),
        'match' => null,
        'managerInvites' => [],
      ];
    }

    if ($action === 'resetEverything') {
      $user = require_user($state);
      if ($user !== 'jdc') reply(['ok' => false, 'error' => 'forbidden'], 403);
      $users = [];
      foreach (($state['users'] ?? []) as $name => $account) {
        if (!is_array($account) || empty($account['password'])) continue;
        $users[$name] = [
          'password' => $account['password'],
          'created' => (int)($account['created'] ?? $now),
          'lastSeen' => $name === $user ? $now : (int)($account['lastSeen'] ?? $now),
        ];
      }
      $state = default_state();
      $state['users'] = $users;
      $state['managerInvites'] = [];
      $state['managerMarket'] = [];
      $state['managerBrowseOffers'] = [];
      $state['managerTransferOffers'] = [];
      $state['managerTransferNotices'] = [];
      $state['managerBrowseSold'] = [];
      $deletedManagerSaves = manager_delete_all_saves();
      return [
        'user' => public_user($user, $state['users'][$user]),
        'teams' => public_teams($state, $user),
        'match' => null,
        'managerInvites' => [],
        'managerSaves' => [],
        'managerHasAnySave' => false,
        'deletedManagerSaves' => $deletedManagerSaves,
      ];
    }

    $user = $_SESSION['user'] ?? null;
    if (is_string($user) && isset($state['users'][$user])) {
      if ($now - (int)($state['users'][$user]['lastSeen'] ?? 0) >= 30) $state['users'][$user]['lastSeen'] = $now;
    }
    sync_manager_matches($state);
    manager_repair_relegated_carry($state);
    manager_repair_unstarted_cups($state);

    if ($action === 'managerSaveList') {
      $user = require_user($state);
      ensure_current_manager_save($state, $user);
      $managerSaves = public_manager_saves($user);
      return ['managerSaves' => $managerSaves, 'managerHasAnySave' => count($managerSaves) > 0];
    }

    if ($action === 'deleteManagerSave') {
      $user = require_user($state);
      $saveId = (string)($data['saveId'] ?? '');
      $save = read_manager_save($saveId);
      if (!manager_save_can_delete($save, $user)) reply(['ok' => false, 'error' => 'forbidden'], 403);
      $seasonId = (string)($save['meta']['seasonId'] ?? '');
      @unlink(manager_save_path($saveId));
      @unlink(manager_save_sidecar_path($saveId));
      manager_remove_active_season_if_unsaved($state, $seasonId);
      $managerSaves = public_manager_saves($user);
      $season = current_manager_season($state, $user);
      return [
        'managerSaves' => $managerSaves,
        'managerHasAnySave' => count($managerSaves) > 0,
        'managerSeason' => public_manager_season($season, $state, $user),
        'managerHasSeason' => $season !== null,
        'managerCanContinue' => manager_user_can_continue($state, $user),
      ];
    }

    if ($action === 'loadManagerSave') {
      $user = require_user($state);
      $save = read_manager_save((string)($data['saveId'] ?? ''));
      if (!manager_save_allowed($save, $user)) reply(['ok' => false, 'error' => 'forbidden'], 403);
      load_manager_save_into_state($state, $save);
      $state['users'][$user]['lastSeen'] = $now;
      $meta = manager_save_metadata($save, (string)($data['saveId'] ?? ''));
      manager_log($state, 'load_manager_save', ['user' => $user, 'save' => $meta['file'], 'users' => $meta['users']]);
      $season = current_manager_season($state, $user);
      manager_generate_cpu_market_offers($state, $season, $now, $user);
      return [
        'user' => public_user($user, $state['users'][$user]),
        'teams' => public_teams($state, $user),
        'managerSeason' => public_manager_season($season, $state, $user),
        'managerInvites' => public_manager_invites($state, $user),
        'managerCanContinue' => manager_user_can_continue($state, $user),
        'managerHasAnySave' => manager_user_has_any_save($user),
        'managerFinance' => isset($state['users'][$user]) ? public_finance($state['users'][$user]) : null,
        'managerSquad' => isset($state['users'][$user]) ? public_manager_context_squad($state, $user, isset($season) && is_array($season) ? $season : current_manager_season($state, $user)) : null,
        'managerMarket' => public_manager_market($state, $season),
        'managerTransferOffers' => public_manager_transfer_offers($state, $user),
        'loadedSave' => $meta,
      ];
    }

    if ($action === 'trainingData') {
      require_user($state);
      $trainingDivisions = [];
      $trainingTeams = [];
      foreach (training_divisions() as $division) {
        $trainingDivision = $division;
        $trainingDivision['teams'] = [];
        foreach ($division['teams'] as $team) {
          $base = (int)($team['baseRating'] ?? team_base_rating((string)($team['sourceId'] ?? $team['id'])));
          $players = [];
          $trainingSquad = make_named_squad('home', $base, (string)($team['sourceId'] ?? $team['id']), (string)($team['name'] ?? ''));
          foreach ($trainingSquad['starters'] as $player) {
            $player = public_player($player);
            $players[] = [
              'id' => $team['id'] . ':' . ($player['id'] ?? ''),
              'teamId' => $team['id'],
              'teamName' => $team['name'],
              'divisionId' => $division['id'],
              'divisionName' => $division['name'],
              'shirt' => $team['shirt'],
              'trim' => $team['trim'],
              'name' => $player['name'] ?? ($player['id'] ?? 'Player'),
              'role' => $player['displayRole'] ?? player_display_role($player),
              'slotRole' => $player['slotRole'] ?? player_slot_role($player),
              'foot' => $player['foot'] ?? player_preferred_foot($player),
              'rating' => $player['rating'] ?? 50,
              'baseRating' => $player['baseRating'] ?? ($player['rating'] ?? 50),
              'effectiveRating' => $player['effectiveRating'] ?? ($player['rating'] ?? 50),
              'stats' => $player['stats'] ?? [],
              'speedKmh' => round(player_stat($player, 'speed') * 0.45, 1),
              'shotKmh' => round(player_stat($player, 'shooting') * 1.5, 1),
            ];
          }
          $trainingTeam = $team;
          $trainingTeam['players'] = $players;
          $trainingDivision['teams'][] = $trainingTeam;
          $trainingTeams[] = $trainingTeam;
        }
        $trainingDivisions[] = $trainingDivision;
      }
      return ['training' => ['divisions' => $trainingDivisions, 'teams' => $trainingTeams]];
    }

    if ($action === 'managerSetup') {
      $user = require_user($state);
      ensure_user_squad($state['users'][$user]);
      ensure_manager_finance($state['users'][$user]);
      ensure_manager_market($state);
      manager_award_finished_season_trophies($state);
      $season = null;
      $seasonId = current_manager_season_id($state, $user);
      if ($seasonId !== null && isset($state['managerSeasons'][$seasonId])) {
        $season =& $state['managerSeasons'][$seasonId];
        if (($season['status'] ?? '') === 'finished') manager_award_season_trophies($state, $season);
        manager_prepare_injuries_for_current_round($state, $season, $now);
        if (manager_participant($season, $user) !== null) {
          $sideData = manager_side_data($state, $season, $user);
          manager_lock_next_form_for_side($season, $user, $sideData);
        }
      }
      manager_generate_cpu_market_offers($state, $season, $now, $user);
      return [
        'managerUsers' => public_manager_users($state),
        'managerSeason' => public_manager_season($season, $state, $user),
        'managerInvites' => public_manager_invites($state, $user),
        'managerCanContinue' => manager_user_can_continue($state, $user),
        'managerHasAnySave' => manager_user_has_any_save($user),
        'managerFinance' => public_finance($state['users'][$user]),
        'managerSquad' => public_manager_context_squad($state, $user, isset($season) && is_array($season) ? $season : current_manager_season($state, $user)),
        'managerMarket' => public_manager_market($state, $season),
        'managerDivisions' => public_manager_divisions($state, $season),
        'managerTransferOffers' => public_manager_transfer_offers($state, $user),
      ];
    }

    if ($action === 'managerStartArenaExpansion') {
      $user = require_user($state);
      $season = current_manager_season($state, $user);
      if (!$season) reply(['ok' => false, 'error' => 'no_season'], 409);
      if (!in_array($user, $season['humanManagers'] ?? [], true)) reply(['ok' => false, 'error' => 'not_your_season'], 403);
      ensure_manager_finance($state['users'][$user]);
      $finance =& $state['users'][$user]['managerFinance'];
      if (is_array($finance['arenaBuild'] ?? null)) reply(['ok' => false, 'error' => 'arena_building'], 409);
      $option = manager_arena_expansion_option($finance);
      if (!$option) reply(['ok' => false, 'error' => 'arena_max'], 409);
      if ((int)($finance['balance'] ?? 0) < (int)$option['cost']) reply(['ok' => false, 'error' => 'no_money'], 409);
      $finance['balance'] -= (int)$option['cost'];
      $finance['arenaSpent'] = (int)($finance['arenaSpent'] ?? 0) + (int)$option['cost'];
      $finance['arenaBuild'] = [
        'level' => (int)$option['level'],
        'seats' => (int)$option['seats'],
        'cost' => (int)$option['cost'],
        'roundsTotal' => (int)$option['rounds'],
        'roundsLeft' => (int)$option['rounds'],
        'startedAt' => $now,
      ];
      manager_log($state, 'arena_expansion_start', ['seasonId' => (string)($season['id'] ?? ''), 'user' => $user, 'level' => (int)$option['level'], 'seats' => (int)$option['seats'], 'cost' => (int)$option['cost'], 'rounds' => (int)$option['rounds']]);
      unset($finance);
      return [
        'managerSeason' => public_manager_season(current_manager_season($state, $user), $state, $user),
        'managerFinance' => public_finance($state['users'][$user]),
      ];
    }

    if ($action === 'managerTeamDetails') {
      $user = require_user($state);
      manager_award_finished_season_trophies($state);
      $seasonId = current_manager_season_id($state, $user);
      if ($seasonId === null || empty($state['managerSeasons'][$seasonId])) reply(['ok' => false, 'error' => 'no_season'], 409);
      $season =& $state['managerSeasons'][$seasonId];
      if (($season['status'] ?? '') === 'finished') manager_award_season_trophies($state, $season);
      manager_prepare_injuries_for_current_round($state, $season, $now);
      if (!in_array($user, $season['humanManagers'] ?? [], true)) reply(['ok' => false, 'error' => 'not_your_season'], 403);
      $target = (string)($data['teamUser'] ?? '');
      if (manager_participant($season, $target) === null && str_starts_with($target, 'cpu:')) {
        return ['teamDetails' => public_manager_division_team_details($target, $state)];
      }
      if (manager_participant($season, $target) !== null) {
        $sideData = manager_side_data($state, $season, $target);
        manager_lock_next_form_for_side($season, $target, $sideData);
      }
      return ['teamDetails' => public_manager_team_details($state, $season, $target)];
    }

    if ($action === 'browseTeamDetails') {
      $user = require_user($state);
      $season = current_manager_season($state, $user);
      if (!$season) reply(['ok' => false, 'error' => 'no_season'], 409);
      if (!in_array($user, $season['humanManagers'] ?? [], true)) reply(['ok' => false, 'error' => 'not_your_season'], 403);
      $target = (string)($data['teamUser'] ?? '');
      return ['teamDetails' => manager_browse_team_details($state, $target, $user)];
    }

    if ($action === 'createManagerSeason') {
      $user = require_user($state);
      $selected = $data['users'] ?? [];
      if (!is_array($selected)) reply(['ok' => false, 'error' => 'bad_users'], 400);
      $selected = array_values(array_unique(array_map(fn($name) => clean_name((string)$name), $selected)));
      if (!$selected) $selected = [$user];
      if (!in_array($user, $selected, true)) $selected[] = $user;
      $selected = array_values(array_unique(array_merge([$user], array_filter($selected, fn($name) => $name !== $user))));
      $freshStart = !empty($data['freshStart']) && count($selected) === 1;
      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);
      }
      if ($duplicate = manager_duplicate_human_team($state, $selected)) {
        reply(['ok' => false, 'error' => 'duplicate_human_team', 'team' => $duplicate['team'], 'users' => $duplicate['users']], 409);
      }
      if ($freshStart) {
        reset_active_manager_game($state);
      }
      if (count($selected) > 1) {
        if (!isset($state['managerInvites']) || !is_array($state['managerInvites'])) $state['managerInvites'] = [];
        $divisionLevel = clamp_int((int)($state['users'][$user]['managerDivisionLevel'] ?? 5), 1, 5);
        $inviteId = 'i' . bin2hex(random_bytes(5));
        $state['managerInvites'][$inviteId] = [
          'id' => $inviteId,
          'status' => 'pending',
          'from' => $user,
          'users' => $selected,
          'accepted' => [$user => true],
          'divisionLevel' => $divisionLevel,
          'seasonYear' => manager_state_year($state),
          'created' => $now,
        ];
        return [
          'managerInvite' => $state['managerInvites'][$inviteId],
          'managerInvites' => public_manager_invites($state, $user),
        ];
      }
      $season = create_manager_season_array($state, $selected, $user, $now);
      $id = (string)$season['id'];
      $state['managerSeasons'][$id] = $season;
      set_manager_current($state, $id, $season['humanManagers'] ?? $selected);
      manager_autosave_season($state, $state['managerSeasons'][$id], null, $user);
      manager_log($state, 'create_season', ['seasonId' => $id, 'user' => $user, 'humans' => $season['humanManagers'] ?? $selected, 'division' => $season['divisionName'] ?? '']);
      return ['managerSeason' => public_manager_season($state['managerSeasons'][$id], $state, $user), 'managerCanContinue' => manager_user_can_continue($state, $user), 'managerHasAnySave' => manager_user_has_any_save($user)];
    }

    if ($action === 'acceptManagerInvite') {
      $user = require_user($state);
      $inviteId = (string)($data['inviteId'] ?? '');
      if ($inviteId === '' || empty($state['managerInvites'][$inviteId])) reply(['ok' => false, 'error' => 'bad_invite'], 404);
      $invite =& $state['managerInvites'][$inviteId];
      if (($invite['status'] ?? 'pending') !== 'pending') reply(['ok' => false, 'error' => 'invite_closed'], 409);
      if (!in_array($user, $invite['users'] ?? [], true)) reply(['ok' => false, 'error' => 'not_invited'], 403);
      $invite['accepted'][$user] = true;
      if (manager_invite_ready($invite)) {
        $creator = (string)($invite['from'] ?? $user);
        $season = create_manager_season_array($state, array_values($invite['users'] ?? []), $creator, $now, (int)($invite['seasonYear'] ?? manager_state_year($state)));
        $id = (string)$season['id'];
        $state['managerSeasons'][$id] = $season;
        set_manager_current($state, $id, $season['humanManagers'] ?? array_values($invite['users'] ?? []));
        manager_autosave_season($state, $state['managerSeasons'][$id], null, $creator);
        manager_log($state, 'invite_started', ['seasonId' => $id, 'inviteId' => $inviteId, 'user' => $user, 'humans' => $season['humanManagers'] ?? []]);
        $invite['status'] = 'started';
        $invite['seasonId'] = $id;
        unset($invite);
        return [
          'managerSeason' => public_manager_season($state['managerSeasons'][$id], $state, $user),
          'managerInvites' => public_manager_invites($state, $user),
          'managerCanContinue' => manager_user_can_continue($state, $user),
          'managerHasAnySave' => manager_user_has_any_save($user),
        ];
      }
      unset($invite);
      return ['managerInvites' => public_manager_invites($state, $user)];
    }

    if ($action === 'continueManagerSeason') {
      $user = require_user($state);
      $currentId = (string)(current_manager_season_id($state, $user) ?? '');
      if ($currentId === '' || empty($state['managerSeasons'][$currentId])) reply(['ok' => false, 'error' => 'no_season'], 409);
      $current =& $state['managerSeasons'][$currentId];
      if (!in_array($user, $current['humanManagers'] ?? [], true)) reply(['ok' => false, 'error' => 'not_your_season'], 403);
      if (($current['status'] ?? '') !== 'finished') {
        $response = [
          'managerSeason' => public_manager_season($current, $state, $user),
          'managerFinance' => public_finance($state['users'][$user]),
          'managerSquad' => public_manager_context_squad($state, $user, isset($season) && is_array($season) ? $season : current_manager_season($state, $user)),
          'managerMarket' => public_manager_market($state, $current),
          'managerTransferOffers' => public_manager_transfer_offers($state, $user),
          'managerCanContinue' => manager_user_can_continue($state, $user),
          'managerHasAnySave' => manager_user_has_any_save($user),
          'alreadyContinued' => true,
        ];
        unset($current);
        return $response;
      }
      $waitingUsers = manager_continue_waiting_users($state, $current, $user);
      if ($waitingUsers) {
        $response = [
          'ok' => false,
          'error' => 'season_year_wait',
          'waitingUsers' => $waitingUsers,
          'managerSeason' => public_manager_season($current, $state, $user),
          'managerFinance' => public_finance($state['users'][$user]),
          'managerSquad' => public_manager_context_squad($state, $user, isset($season) && is_array($season) ? $season : current_manager_season($state, $user)),
          'managerMarket' => public_manager_market($state, $current),
          'managerTransferOffers' => public_manager_transfer_offers($state, $user),
          'managerCanContinue' => manager_user_can_continue($state, $user),
          'managerHasAnySave' => manager_user_has_any_save($user),
        ];
        unset($current);
        reply($response, 409);
      }
      if (empty($current['progressionApplied'])) apply_manager_progression($state, $current);
      $targetLevel = manager_user_division_level($state, $user);
      $existingId = continued_manager_season_id($state, $currentId, $user, $targetLevel);
      if ($existingId !== null && isset($state['managerSeasons'][$existingId])) {
        $existing =& $state['managerSeasons'][$existingId];
        set_manager_current($state, $existingId, $existing['humanManagers'] ?? [$user]);
        if (!manager_user_can_continue($state, $user)) manager_autosave_season($state, $existing, null, $user);
        manager_log($state, 'continue_existing_season', ['fromSeasonId' => $currentId, 'seasonId' => $existingId, 'user' => $user, 'division' => $existing['divisionName'] ?? '']);
        $response = [
          'managerSeason' => public_manager_season($existing, $state, $user),
          'managerFinance' => public_finance($state['users'][$user]),
          'managerSquad' => public_manager_context_squad($state, $user, isset($season) && is_array($season) ? $season : current_manager_season($state, $user)),
          'managerMarket' => public_manager_market($state, $existing),
          'managerTransferOffers' => public_manager_transfer_offers($state, $user),
          'managerCanContinue' => manager_user_can_continue($state, $user),
          'managerHasAnySave' => manager_user_has_any_save($user),
          'alreadyContinued' => true,
        ];
        unset($existing, $current);
        return $response;
      }
      $group = manager_continue_group_users($state, $current, $user);
      $nextYear = manager_season_year($current, $state) + 1;
      $previousSeason = $current;
      unset($current);
      $season = create_manager_season_array($state, $group, $user, $now, $nextYear, $previousSeason);
      $id = (string)$season['id'];
      $season['continuedFrom'] = $currentId;
      $season['continuedGroupLevel'] = $targetLevel;
      $season['continuedGroupUsers'] = $group;
      $state['managerSeasons'][$id] = $season;
      set_manager_current($state, $id, $group);
      manager_autosave_season($state, $state['managerSeasons'][$id], null, $user);
      manager_log($state, 'continue_season', ['fromSeasonId' => $currentId, 'seasonId' => $id, 'user' => $user, 'group' => $group, 'division' => $season['divisionName'] ?? '', 'year' => $season['seasonYear'] ?? $nextYear]);
      ensure_manager_market($state);
      return [
        'managerSeason' => public_manager_season($state['managerSeasons'][$id], $state, $user),
        'managerFinance' => public_finance($state['users'][$user]),
        'managerSquad' => public_manager_context_squad($state, $user, isset($season) && is_array($season) ? $season : current_manager_season($state, $user)),
        'managerMarket' => public_manager_market($state, $state['managerSeasons'][$id]),
        'managerTransferOffers' => public_manager_transfer_offers($state, $user),
        'managerCanContinue' => manager_user_can_continue($state, $user),
        'managerHasAnySave' => manager_user_has_any_save($user),
      ];
    }

    if ($action === 'managerNextRound') {
      $user = require_user($state);
      $id = (string)(current_manager_season_id($state, $user) ?? '');
      if ($id === '' || empty($state['managerSeasons'][$id])) reply(['ok' => false, 'error' => 'no_season'], 409);
      $season =& $state['managerSeasons'][$id];
      if (!in_array($user, $season['humanManagers'] ?? [], true)) reply(['ok' => false, 'error' => 'not_your_season'], 403);
      if (($season['status'] ?? '') === 'finished') return ['managerSeason' => public_manager_season($season, $state, $user)];
      if (manager_has_watching($season)) reply(['ok' => false, 'error' => 'match_in_progress'], 409);
      $roundIndex = manager_pending_round_index($season);
      if ($roundIndex === null) {
        if (($season['status'] ?? '') === 'active' && empty($season['leagueResolved'])) start_manager_playoffs($season);
        elseif (($season['status'] ?? '') === 'playoff') advance_manager_playoffs($season);
        if (!empty($season['leagueResolved']) && !manager_has_unplayed_phase($season, 'cup')) $season['status'] = 'finished';
        if (($season['status'] ?? '') === 'finished') apply_manager_progression($state, $season);
        $roundIndex = manager_pending_round_index($season);
      }
      if ($roundIndex === null) {
        $season['updated'] = $now;
        return [
          'managerSeason' => public_manager_season($season, $state, $user),
          'managerFinance' => public_finance($state['users'][$user]),
          'managerMarket' => public_manager_market($state, $season),
          'managerTransferOffers' => public_manager_transfer_offers($state, $user),
        ];
      }
      $requestedOwnFixture = manager_requested_user_fixture($season, $user, $data);
      if ($requestedOwnFixture && (int)$requestedOwnFixture['roundIndex'] !== $roundIndex) {
        $roundIndex = manager_prepare_requested_user_fixture($state, $season, $user, $data, $now);
        $season['updated'] = $now;
        if ($roundIndex !== null && (int)$requestedOwnFixture['roundIndex'] !== $roundIndex) {
          $split = manager_round_ready_waiting($season, $roundIndex);
          ensure_manager_market($state);
          return [
            'managerSeason' => public_manager_season($season, $state, $user),
            'managerFinance' => public_finance($state['users'][$user]),
            'managerSquad' => public_manager_context_squad($state, $user, $season),
            'managerMarket' => public_manager_market($state, $season),
            'managerTransferOffers' => public_manager_transfer_offers($state, $user),
            'simVotePending' => true,
            'roundWaiting' => $split['waiting'],
          ];
        }
      }
      if (manager_next_round_request_is_stale($season, $roundIndex, $data)) {
        $season['updated'] = $now;
        manager_log($state, 'next_round_stale', [
          'user' => $user,
          'requestedRoundIndex' => $data['roundIndex'] ?? null,
          'requestedPhase' => $data['roundPhase'] ?? '',
        ] + manager_round_log_context($season, $roundIndex));
        return manager_next_round_stale_response($state, $season, $user);
      }
      manager_trigger_due_injuries($state, $season, $roundIndex, $now);
      $ownInjuryBlock = manager_injured_starters($state, $season, $user);
      if ($ownInjuryBlock) {
        $season['updated'] = $now;
        return manager_injury_block_payload($state, $season, $user, $roundIndex);
      }
      $ownFixture = manager_user_round_fixture($season, $user, $roundIndex);
      if (!$ownFixture || !in_array(($ownFixture['fixture']['status'] ?? ''), ['pending', 'watching'], true)) {
        $split = manager_round_ready_waiting($season, $roundIndex);
        $simulated = !$split['waiting'] && !manager_has_watching($season) ? simulate_manager_round_fixtures($state, $season, $roundIndex, false) : 0;
        $season['updated'] = $now;
        manager_log($state, 'next_round_wait', ['user' => $user, 'ready' => $split['ready'], 'waiting' => $split['waiting'], 'simulatedCpu' => $simulated] + manager_round_log_context($season, $roundIndex));
        if ($simulated > 0) manager_log($state, 'sim_round_cpu_finish', ['user' => $user, 'simulated' => $simulated, 'status' => $season['status'] ?? ''] + manager_round_log_context($season, $roundIndex));
        ensure_manager_market($state);
        return [
          'managerSeason' => public_manager_season($season, $state, $user),
          'managerFinance' => public_finance($state['users'][$user]),
          'managerSquad' => public_manager_context_squad($state, $user, $season),
          'managerMarket' => public_manager_market($state, $season),
          'managerTransferOffers' => public_manager_transfer_offers($state, $user),
          'simVotePending' => !empty($split['waiting']),
          'roundWaiting' => $split['waiting'],
        ];
      }
      $fixtureIndex = (int)$ownFixture['fixtureIndex'];
      $fixture = $season['rounds'][$roundIndex]['fixtures'][$fixtureIndex] ?? [];
      if (($fixture['status'] ?? '') === 'watching') reply(['ok' => false, 'error' => 'match_in_progress'], 409);
      $fixtureHumans = manager_fixture_human_users($season, $fixture);
      if (count($fixtureHumans) > 1) {
        $voteKey = manager_sim_fixture_key($season, $roundIndex, $fixtureIndex);
        $watchKey = manager_watch_fixture_key($season, $roundIndex, $fixtureIndex);
        if (isset($season['watchVotes']) && is_array($season['watchVotes']) && isset($season['watchVotes'][$watchKey][$user])) {
          unset($season['watchVotes'][$watchKey][$user]);
          if (empty($season['watchVotes'][$watchKey])) unset($season['watchVotes'][$watchKey]);
        }
        if (!isset($season['simVotes']) || !is_array($season['simVotes'])) $season['simVotes'] = [];
        if (!isset($season['simVotes'][$voteKey]) || !is_array($season['simVotes'][$voteKey])) $season['simVotes'][$voteKey] = [];
        $season['simVotes'][$voteKey][$user] = $now;
        $ready = array_values(array_filter($fixtureHumans, fn($human) => !empty($season['simVotes'][$voteKey][$human])));
        $waiting = array_values(array_filter($fixtureHumans, fn($human) => empty($season['simVotes'][$voteKey][$human])));
        if ($waiting) {
          $season['updated'] = $now;
          return [
            'managerSeason' => public_manager_season($season, $state, $user),
            'managerFinance' => public_finance($state['users'][$user]),
            'managerSquad' => public_manager_context_squad($state, $user, $season),
            'managerMarket' => public_manager_market($state, $season),
            'managerTransferOffers' => public_manager_transfer_offers($state, $user),
            'simVotePending' => true,
            'humanMatchReady' => $ready,
            'humanMatchWaiting' => $waiting,
          ];
        }
        unset($season['simVotes'][$voteKey]);
        unset($season['watchVotes'][$watchKey]);
        simulate_manager_fixture_headless($state, $season, $roundIndex, $fixtureIndex);
        manager_mark_fixture_result_reveal_delay($season, $roundIndex, $fixtureIndex, $user, $now);
        manager_require_round_continue($season, $roundIndex);
        $fixtureAfter = $season['rounds'][$roundIndex]['fixtures'][$fixtureIndex] ?? null;
        $split = manager_round_ready_waiting($season, $roundIndex);
        $simulatedCpu = simulate_manager_round_fixtures($state, $season, $roundIndex, false);
        $season['updated'] = $now;
        manager_log($state, 'sim_human_fixture_headless', ['user' => $user, 'cpuFixtures' => $simulatedCpu, 'status' => $season['status'] ?? '', 'ready' => $split['ready'], 'waiting' => $split['waiting']] + manager_round_log_context($season, $roundIndex, $fixtureIndex, is_array($fixtureAfter) ? $fixtureAfter : $fixture));
        manager_autosave_season($state, $season, is_array($fixtureAfter) ? $fixtureAfter : null, $user);
        ensure_manager_market($state);
        return [
          'managerSeason' => public_manager_season($season, $state, $user),
          'managerFinance' => public_finance($state['users'][$user]),
          'managerSquad' => public_manager_context_squad($state, $user, $season),
          'managerMarket' => public_manager_market($state, $season),
          'managerTransferOffers' => public_manager_transfer_offers($state, $user),
          'managerFixtureSimulated' => true,
          'roundWaiting' => array_values(array_filter($split['waiting'], fn($name) => $name !== $user)),
          'roundContinuePending' => true,
        ];
      }
      simulate_manager_fixture_headless($state, $season, $roundIndex, $fixtureIndex);
      manager_mark_fixture_result_reveal_delay($season, $roundIndex, $fixtureIndex, $user, $now);
      manager_require_round_continue($season, $roundIndex);
      $fixtureAfter = $season['rounds'][$roundIndex]['fixtures'][$fixtureIndex] ?? null;
      $split = manager_round_ready_waiting($season, $roundIndex);
      $simulatedCpu = simulate_manager_round_fixtures($state, $season, $roundIndex, false);
      $season['updated'] = $now;
      manager_log($state, 'sim_user_fixture', ['user' => $user, 'cpuFixtures' => $simulatedCpu, 'status' => $season['status'] ?? '', 'ready' => $split['ready'], 'waiting' => $split['waiting']] + manager_round_log_context($season, $roundIndex, $fixtureIndex, is_array($fixtureAfter) ? $fixtureAfter : $fixture));
      manager_autosave_season($state, $season, is_array($fixtureAfter) ? $fixtureAfter : null, $user);
      ensure_manager_market($state);
      return [
        'managerSeason' => public_manager_season($season, $state, $user),
        'managerFinance' => public_finance($state['users'][$user]),
        'managerSquad' => public_manager_context_squad($state, $user, $season),
        'managerMarket' => public_manager_market($state, $season),
        'managerTransferOffers' => public_manager_transfer_offers($state, $user),
        'managerFixtureSimulated' => true,
        'roundWaiting' => array_values(array_filter($split['waiting'], fn($name) => $name !== $user)),
        'roundContinuePending' => true,
      ];
    }

    if ($action === 'managerRoundContinue') {
      $user = require_user($state);
      $id = (string)(current_manager_season_id($state, $user) ?? '');
      if ($id === '' || empty($state['managerSeasons'][$id])) reply(['ok' => false, 'error' => 'no_season'], 409);
      $season =& $state['managerSeasons'][$id];
      if (!in_array($user, $season['humanManagers'] ?? [], true)) reply(['ok' => false, 'error' => 'not_your_season'], 403);
      if (($season['status'] ?? '') === 'finished') return ['managerSeason' => public_manager_season($season, $state, $user)];
      $roundIndex = manager_pending_round_index($season);
      if (manager_round_continue_request_is_stale($season, $roundIndex, $data)) {
        $season['updated'] = $now;
        manager_log($state, 'round_continue_stale', [
          'user' => $user,
          'requestedRoundIndex' => $data['roundIndex'] ?? null,
          'requestedPhase' => $data['roundPhase'] ?? '',
        ] + ($roundIndex !== null ? manager_round_log_context($season, $roundIndex) : ['seasonId' => $id]));
        return manager_next_round_stale_response($state, $season, $user);
      }
      if ($roundIndex === null) {
        return [
          'managerSeason' => public_manager_season($season, $state, $user),
          'managerFinance' => public_finance($state['users'][$user]),
          'managerSquad' => public_manager_context_squad($state, $user, $season),
          'managerMarket' => public_manager_market($state, $season),
          'managerTransferOffers' => public_manager_transfer_offers($state, $user),
        ];
      }
      $ownFixture = manager_user_round_fixture($season, $user, $roundIndex);
      if (!$ownFixture || ($ownFixture['fixture']['status'] ?? '') !== 'played') {
        $split = manager_round_ready_waiting($season, $roundIndex);
        return [
          'managerSeason' => public_manager_season($season, $state, $user),
          'managerFinance' => public_finance($state['users'][$user]),
          'managerSquad' => public_manager_context_squad($state, $user, $season),
          'managerMarket' => public_manager_market($state, $season),
          'managerTransferOffers' => public_manager_transfer_offers($state, $user),
          'simVotePending' => true,
          'roundWaiting' => $split['waiting'],
        ];
      }
      if (manager_fixture_result_hidden_for_viewer($ownFixture['fixture'], $user, $now)) {
        $split = manager_round_ready_waiting($season, $roundIndex);
        return [
          'managerSeason' => public_manager_season($season, $state, $user),
          'managerFinance' => public_finance($state['users'][$user]),
          'managerSquad' => public_manager_context_squad($state, $user, $season),
          'managerMarket' => public_manager_market($state, $season),
          'managerTransferOffers' => public_manager_transfer_offers($state, $user),
          'simVotePending' => true,
          'roundWaiting' => $split['waiting'],
        ];
      }
      manager_mark_round_continue($season, $user, $roundIndex, $now);
      $split = manager_round_ready_waiting($season, $roundIndex);
      $simulatedCpu = 0;
      if (!$split['waiting'] && !manager_has_watching($season)) {
        $simulatedCpu = simulate_manager_round_fixtures($state, $season, $roundIndex, false);
      }
      $season['updated'] = $now;
      manager_log($state, 'round_continue', ['user' => $user, 'ready' => $split['ready'], 'waiting' => $split['waiting'], 'advanced' => !$split['waiting'], 'simulatedCpu' => $simulatedCpu] + manager_round_log_context($season, $roundIndex));
      if ($simulatedCpu > 0) manager_log($state, 'continue_round_cpu_finish', ['user' => $user, 'simulated' => $simulatedCpu, 'status' => $season['status'] ?? ''] + manager_round_log_context($season, $roundIndex));
      ensure_manager_market($state);
      return [
        'managerSeason' => public_manager_season($season, $state, $user),
        'managerFinance' => public_finance($state['users'][$user]),
        'managerSquad' => public_manager_context_squad($state, $user, $season),
        'managerMarket' => public_manager_market($state, $season),
        'managerTransferOffers' => public_manager_transfer_offers($state, $user),
        'managerRoundContinued' => true,
        'managerRoundAdvanced' => !$split['waiting'],
        'simVotePending' => !empty($split['waiting']),
        'roundWaiting' => $split['waiting'],
      ];
    }

    if ($action === 'managerSimSeason') {
      $user = require_user($state);
      $id = (string)(current_manager_season_id($state, $user) ?? '');
      if ($id === '' || empty($state['managerSeasons'][$id])) reply(['ok' => false, 'error' => 'no_season'], 409);
      $season =& $state['managerSeasons'][$id];
      if (!in_array($user, $season['humanManagers'] ?? [], true)) reply(['ok' => false, 'error' => 'not_your_season'], 403);
      if (($season['status'] ?? '') === 'finished') return ['managerSeason' => public_manager_season($season, $state, $user)];
      if (manager_has_watching($season)) reply(['ok' => false, 'error' => 'match_in_progress'], 409);
      $humans = array_values($season['humanManagers'] ?? []);
      $adminOverride = $user === 'jdc' && !empty($data['adminOverride']);
      if (count($humans) > 1 && !$adminOverride) {
        $voteKey = manager_sim_season_key($season);
        $expectedKey = (string)($data['simSeasonKey'] ?? '');
        if ($expectedKey !== '' && $expectedKey !== $voteKey) {
          return [
            'managerSeason' => public_manager_season($season, $state, $user),
            'managerFinance' => public_finance($state['users'][$user]),
            'staleSeason' => true,
          ];
        }
        if (!isset($season['simSeasonVotes']) || !is_array($season['simSeasonVotes'])) $season['simSeasonVotes'] = [];
        $season['simSeasonVotes'] = array_intersect_key($season['simSeasonVotes'], [$voteKey => true]);
        if (!isset($season['simSeasonVotes'][$voteKey]) || !is_array($season['simSeasonVotes'][$voteKey])) $season['simSeasonVotes'][$voteKey] = [];
        $season['simSeasonVotes'][$voteKey][$user] = $now;
        $readyCount = 0;
        foreach ($humans as $human) {
          if (!empty($season['simSeasonVotes'][$voteKey][$human])) $readyCount++;
        }
        if ($readyCount < count($humans)) {
          $season['updated'] = $now;
          return [
            'managerSeason' => public_manager_season($season, $state, $user),
            'managerFinance' => public_finance($state['users'][$user]),
            'simSeasonVotePending' => true,
          ];
        }
        unset($season['simSeasonVotes'][$voteKey]);
      }

      unset($season['simVotes'], $season['watchVotes'], $season['simSeasonVotes'], $season['roundContinues'], $season['roundContinueRequired']);
      $guard = 0;
      while (($season['status'] ?? '') !== 'finished' && $guard++ < 500) {
        if (manager_has_watching($season)) reply(['ok' => false, 'error' => 'match_in_progress'], 409);
        $roundIndex = manager_pending_round_index($season);
        if ($roundIndex === null) {
          if (($season['status'] ?? '') === 'active' && empty($season['leagueResolved'])) start_manager_playoffs($season);
          elseif (($season['status'] ?? '') === 'playoff') advance_manager_playoffs($season);
          if (!empty($season['leagueResolved']) && !manager_has_unplayed_phase($season, 'cup')) $season['status'] = 'finished';
          if (($season['status'] ?? '') === 'finished') {
            apply_manager_progression($state, $season);
            break;
          }
          $roundIndex = manager_pending_round_index($season);
          if ($roundIndex === null) break;
        }
        manager_trigger_due_injuries($state, $season, $roundIndex, $now);
        if (public_manager_injury_blocks($state, $season, $roundIndex)) {
          $season['updated'] = $now;
          return manager_injury_block_payload($state, $season, $user, $roundIndex);
        }
        simulate_manager_round_fixtures($state, $season, $roundIndex, true);
      }
      if (($season['status'] ?? '') === 'finished') apply_manager_progression($state, $season);
      $season['updated'] = $now;
      manager_log($state, 'sim_season', ['seasonId' => $id, 'user' => $user, 'status' => $season['status'] ?? '', 'guard' => $guard]);
      manager_autosave_season($state, $season, null, $user);
      ensure_manager_market($state);
      return [
        'managerSeason' => public_manager_season($season, $state, $user),
        'managerFinance' => public_finance($state['users'][$user]),
        'managerSquad' => public_manager_context_squad($state, $user, isset($season) && is_array($season) ? $season : current_manager_season($state, $user)),
        'managerMarket' => public_manager_market($state, $season),
        'managerTransferOffers' => public_manager_transfer_offers($state, $user),
        'seasonSimulated' => ($season['status'] ?? '') === 'finished',
      ];
    }

    if ($action === 'managerWatchMatch') {
      $user = require_user($state);
      $id = (string)(current_manager_season_id($state, $user) ?? '');
      if ($id === '' || empty($state['managerSeasons'][$id])) reply(['ok' => false, 'error' => 'no_season'], 409);
      $season =& $state['managerSeasons'][$id];
      if (!in_array($user, $season['humanManagers'] ?? [], true)) reply(['ok' => false, 'error' => 'not_your_season'], 403);
      if (!in_array(($season['status'] ?? ''), ['active', 'playoff'], true)) reply(['ok' => false, 'error' => 'season_not_active'], 409);
      if (($existing = user_match_id($state, $user)) !== null) {
        if (!empty($state['matches'][$existing]['managerFixture'])) {
          tick_state_match($state, $existing);
          return ['match' => public_match($state['matches'][$existing], $user)];
        }
        reply(['ok' => false, 'error' => 'current_match'], 409);
      }
      $roundIndex = manager_pending_round_index($season);
      if ($roundIndex === null) reply(['ok' => false, 'error' => 'no_fixture'], 409);
      $next = manager_user_round_fixture($season, $user, $roundIndex);
      if (!$next || !in_array(($next['fixture']['status'] ?? ''), ['pending', 'watching'], true)) {
        $split = manager_round_ready_waiting($season, $roundIndex);
        return [
          'managerSeason' => public_manager_season($season, $state, $user),
          'managerFinance' => public_finance($state['users'][$user]),
          'managerSquad' => public_manager_context_squad($state, $user, $season),
          'simVotePending' => !empty($split['waiting']),
          'roundWaiting' => $split['waiting'],
        ];
      }
      $fixture = $next['fixture'];
      manager_trigger_due_injuries($state, $season, (int)$next['roundIndex'], $now);
      if (manager_injured_starters($state, $season, $user)) {
        $season['updated'] = $now;
        return manager_injury_block_payload($state, $season, $user, (int)$next['roundIndex']);
      }
      if (($fixture['status'] ?? '') === 'watching' && !empty($fixture['matchId']) && !empty($state['matches'][$fixture['matchId']])) {
        tick_state_match($state, (string)$fixture['matchId']);
        $m = $state['matches'][$fixture['matchId']];
        if (($m['players']['home'] ?? '') === $user || ($m['players']['away'] ?? '') === $user) {
          return ['match' => public_match($m, $user)];
        }
        return [
          'managerSeason' => public_manager_season($season, $state, $user),
          'managerFinance' => public_finance($state['users'][$user]),
          'managerSquad' => public_manager_context_squad($state, $user, $season),
          'simVotePending' => true,
          'humanMatchWaiting' => array_values(array_filter(manager_fixture_human_users($season, $fixture), fn($name) => $name !== $user)),
        ];
      }
      $fixtureHumans = manager_fixture_human_users($season, $fixture);
      if (count($fixtureHumans) > 1) {
        $voteKey = manager_watch_fixture_key($season, (int)$next['roundIndex'], (int)$next['fixtureIndex']);
        $simKey = manager_sim_fixture_key($season, (int)$next['roundIndex'], (int)$next['fixtureIndex']);
        if (isset($season['simVotes']) && is_array($season['simVotes']) && isset($season['simVotes'][$simKey][$user])) {
          unset($season['simVotes'][$simKey][$user]);
          if (empty($season['simVotes'][$simKey])) unset($season['simVotes'][$simKey]);
        }
        if (!isset($season['watchVotes']) || !is_array($season['watchVotes'])) $season['watchVotes'] = [];
        if (!isset($season['watchVotes'][$voteKey]) || !is_array($season['watchVotes'][$voteKey])) $season['watchVotes'][$voteKey] = [];
        $season['watchVotes'][$voteKey][$user] = $now;
        $ready = array_values(array_filter($fixtureHumans, fn($human) => !empty($season['watchVotes'][$voteKey][$human])));
        $waiting = array_values(array_filter($fixtureHumans, fn($human) => empty($season['watchVotes'][$voteKey][$human])));
        if ($waiting) {
          $season['updated'] = $now;
          return [
            'managerSeason' => public_manager_season($season, $state, $user),
            'managerFinance' => public_finance($state['users'][$user]),
            'managerSquad' => public_manager_context_squad($state, $user, $season),
            'simVotePending' => true,
            'humanMatchReady' => $ready,
            'humanMatchWaiting' => $waiting,
          ];
        }
        unset($season['watchVotes'][$voteKey], $season['simVotes'][$simKey]);
      }
      $match = new_manager_watch_match($state, $user, $id, (int)$next['roundIndex'], (int)$next['fixtureIndex']);
      return ['match' => public_match($match, $user)];
    }

    if ($action === 'abortManagerSeason') {
      $user = require_user($state);
      $id = (string)(current_manager_season_id($state, $user) ?? '');
      if ($id !== '' && isset($state['managerSeasons'][$id])) {
        if (!in_array($user, $state['managerSeasons'][$id]['humanManagers'] ?? [], true)) reply(['ok' => false, 'error' => 'not_your_season'], 403);
        $state['managerSeasons'][$id]['status'] = 'aborted';
        $state['managerSeasons'][$id]['abortedBy'] = $user;
        $state['managerSeasons'][$id]['updated'] = $now;
        manager_log($state, 'abort_season', ['seasonId' => $id, 'user' => $user]);
      }
      unset($state['managerCurrentByUser'][$user]);
      if (($state['managerCurrentId'] ?? null) === $id) $state['managerCurrentId'] = null;
      return [
        'managerSeason' => null,
        'managerFinance' => public_finance($state['users'][$user]),
      ];
    }

    if ($action === 'browseTransferBid') {
      $user = require_user($state);
      $season = current_manager_season($state, $user);
      if (!$season) reply(['ok' => false, 'error' => 'no_season'], 409);
      if (!in_array($user, $season['humanManagers'] ?? [], true)) reply(['ok' => false, 'error' => 'not_your_season'], 403);
      ensure_user_squad($state['users'][$user]);
      ensure_manager_finance($state['users'][$user]);

      $teamUser = (string)($data['teamUser'] ?? '');
      $playerId = (string)($data['playerId'] ?? '');
      if ($teamUser === '' || $playerId === '') reply(['ok' => false, 'error' => 'bad_player'], 400);

      $details = manager_browse_team_details($state, $teamUser, $user);
      $found = manager_browse_find_player($details, $playerId);
      if (!$found) reply(['ok' => false, 'error' => 'bad_player'], 404);

      $player = $found['player'];
      if (!is_cpu_manager($teamUser)) return manager_create_human_transfer_offer($state, $season, $user, $teamUser, $details, $player, $data, $now);
      $team = $details['team'] ?? ['name' => strtoupper($teamUser), 'shirt' => '#fff', 'trim' => '#111'];
	      $divisionName = (string)($details['divisionName'] ?? manager_division_name(manager_browse_team_level($teamUser)));
	      $basePrice = (int)($player['price'] ?? $player['value'] ?? player_value($player));
	      $cashOffer = manager_transfer_cash_offer($data, $basePrice);
	      $offerPlayerIds = manager_transfer_offer_player_ids($data, 3);
	      $offerPlayers = manager_transfer_offer_players($state['users'][$user], $offerPlayerIds);
	      manager_ensure_browse_trade_room($state['users'][$user]['squad'] ?? [], $offerPlayers);
	      $offerValue = manager_transfer_offer_players_value($offerPlayers);
	      $offerTotal = $cashOffer + $offerValue;
	      if ((int)$state['users'][$user]['managerFinance']['balance'] < $cashOffer) reply(['ok' => false, 'error' => 'no_money'], 409);

	      $buyerLevel = clamp_int((int)($state['users'][$user]['managerDivisionLevel'] ?? $season['divisionLevel'] ?? 5), 1, 5);
	      $targetParticipant = manager_participant($season, $teamUser);
	      $targetLevel = $targetParticipant ? clamp_int((int)($targetParticipant['divisionLevel'] ?? 5), 1, 5) : manager_browse_team_level($teamUser);
	      $rating = (int)($player['rating'] ?? average_stats($player['stats'] ?? []));
	      $gap = max(0, $buyerLevel - $targetLevel);
	      $offerRatio = $basePrice > 0 ? $offerTotal / $basePrice : 1.0;
	      $rejectChance = clamp_int((int)round(14 + $gap * 16 + max(0, $rating - 72) * 1.1 + max(0, 1.0 - $offerRatio) * 90 - max(0, $offerRatio - 1.0) * 30 - max(0, $targetLevel - $buyerLevel) * 5), 5, 88);
	      $counterChance = clamp_int((int)round(22 + $gap * 10 + max(0, $rating - 68) * 0.9 + max(0, 1.1 - $offerRatio) * 45 - max(0, $offerRatio - 1.0) * 28), 10, 86);
	      $roll = mt_rand(1, 100);

      if ($roll <= $rejectChance) {
        return [
	          'status' => 'rejected',
	          'message' => ($team['name'] ?? 'The club') . ' rejected the bid.',
	          'player' => $player,
	          'team' => $team,
	          'basePrice' => $basePrice,
	          'cashPrice' => $cashOffer,
	          'offerValue' => $offerValue,
	          'offerPlayerCount' => count($offerPlayers),
	          'offerTotal' => $offerTotal,
	        ];
	      }

	      if ($roll <= $rejectChance + $counterChance) {
	        $counterTotal = max($offerTotal + 25000, manager_browse_counter_price($basePrice, $buyerLevel, $targetLevel, $rating));
	        $counterPrice = max(0, (int)(round(($counterTotal - $offerValue) / 25000) * 25000));
	        $offerId = 'o' . bin2hex(random_bytes(5));
	        if (!isset($state['managerBrowseOffers']) || !is_array($state['managerBrowseOffers'])) $state['managerBrowseOffers'] = [];
	        if (!isset($state['managerBrowseOffers'][$user]) || !is_array($state['managerBrowseOffers'][$user])) $state['managerBrowseOffers'][$user] = [];
        $state['managerBrowseOffers'][$user][$offerId] = [
          'id' => $offerId,
          'teamUser' => $teamUser,
          'team' => $team,
          'divisionName' => $divisionName,
	          'player' => $player,
	          'basePrice' => $basePrice,
	          'cashOffer' => $cashOffer,
	          'offerPlayerId' => (string)($offerPlayerIds[0] ?? ''),
	          'offerPlayerIds' => $offerPlayerIds,
	          'offerPlayers' => array_map(fn($offerPlayer) => $offerPlayer['player'], $offerPlayers),
	          'offerValue' => $offerValue,
	          'offerTotal' => $offerTotal,
	          'price' => $counterPrice,
	          'counterTotal' => $counterPrice + $offerValue,
	          'created' => $now,
	          'expires' => $now + 600,
	        ];
        return [
          'status' => 'counter',
	          'offerId' => $offerId,
	          'price' => $counterPrice,
	          'basePrice' => $basePrice,
	          'cashPrice' => $counterPrice,
	          'offerValue' => $offerValue,
	          'offerPlayerCount' => count($offerPlayers),
	          'offerTotal' => $offerTotal,
	          'counterTotal' => $counterPrice + $offerValue,
	          'message' => ($team['name'] ?? 'The club') . ' wants more money.',
	          'player' => $player,
	          'team' => $team,
	        ];
	      }

	      $result = complete_manager_browse_purchase($state, $user, $teamUser, $team, $divisionName, $player, $cashOffer, $now, $offerPlayerIds);
	      $result['status'] = 'bought';
	      $result['price'] = $cashOffer;
	      $result['cashPrice'] = $cashOffer;
	      $result['basePrice'] = $basePrice;
	      $result['offerValue'] = $offerValue;
	      $result['offerPlayerCount'] = count($offerPlayers);
	      $result['offerTotal'] = $cashOffer + $offerValue;
	      return $result;
	    }

    if ($action === 'acceptBrowseTransfer') {
      $user = require_user($state);
      $season = current_manager_season($state, $user);
      if (!$season) reply(['ok' => false, 'error' => 'no_season'], 409);
      if (!in_array($user, $season['humanManagers'] ?? [], true)) reply(['ok' => false, 'error' => 'not_your_season'], 403);
      $offerId = (string)($data['offerId'] ?? '');
      if ($offerId === '' || empty($state['managerBrowseOffers'][$user][$offerId])) reply(['ok' => false, 'error' => 'bad_offer'], 404);
      $offer = $state['managerBrowseOffers'][$user][$offerId];
      if ((int)($offer['expires'] ?? 0) < $now) {
        unset($state['managerBrowseOffers'][$user][$offerId]);
        reply(['ok' => false, 'error' => 'offer_expired'], 409);
      }
      $result = complete_manager_browse_purchase(
        $state,
        $user,
        (string)($offer['teamUser'] ?? ''),
	        is_array($offer['team'] ?? null) ? $offer['team'] : ['name' => 'TRANSFER BROWSE', 'shirt' => '#fff', 'trim' => '#111'],
	        (string)($offer['divisionName'] ?? ''),
	        is_array($offer['player'] ?? null) ? $offer['player'] : [],
	        (int)($offer['price'] ?? 0),
	        $now,
	        is_array($offer['offerPlayerIds'] ?? null) ? $offer['offerPlayerIds'] : array_filter([(string)($offer['offerPlayerId'] ?? '')])
	      );
	      unset($state['managerBrowseOffers'][$user][$offerId]);
	      $result['status'] = 'bought';
	      $result['price'] = (int)($offer['price'] ?? 0);
	      $result['cashPrice'] = (int)($offer['price'] ?? 0);
	      $result['basePrice'] = (int)($offer['basePrice'] ?? 0);
	      $result['offerValue'] = (int)($offer['offerValue'] ?? 0);
	      $result['offerPlayerCount'] = count(is_array($offer['offerPlayerIds'] ?? null) ? $offer['offerPlayerIds'] : array_filter([(string)($offer['offerPlayerId'] ?? '')]));
	      $result['offerTotal'] = (int)($offer['price'] ?? 0) + (int)($offer['offerValue'] ?? 0);
	      return $result;
	    }

	    if ($action === 'managerTransferOfferDecision') {
	      $user = require_user($state);
	      $offerId = (string)($data['offerId'] ?? '');
	      $decision = ((string)($data['decision'] ?? '')) === 'accept' ? 'accept' : 'reject';
	      return manager_transfer_offer_response($state, $user, $offerId, $decision, $now);
	    }

	    if ($action === 'managerTransferOfferCounter') {
	      $user = require_user($state);
	      $offerId = (string)($data['offerId'] ?? '');
	      return manager_transfer_offer_counter($state, $user, $offerId, $data, $now);
	    }

	    if ($action === 'managerTransferNoticeSeen') {
      $user = require_user($state);
      $noticeId = (string)($data['noticeId'] ?? '');
      if ($noticeId !== '' && isset($state['managerTransferNotices'][$noticeId]) && (string)($state['managerTransferNotices'][$noticeId]['user'] ?? '') === $user) {
        unset($state['managerTransferNotices'][$noticeId]);
      }
      return ['managerTransferOffers' => public_manager_transfer_offers($state, $user)];
    }

    if ($action === 'removeMarketListing') {
      $user = require_user($state);
      return manager_remove_market_listing($state, $user, (string)($data['playerId'] ?? ''), $now);
    }

    if ($action === 'buyPlayer') {
      $user = require_user($state);
      ensure_user_squad($state['users'][$user]);
      ensure_manager_finance($state['users'][$user]);
      ensure_manager_market($state);
      $season = current_manager_season($state, $user);
	      $playerId = (string)($data['playerId'] ?? '');
	      foreach ($state['managerMarket'] as $index => $player) {
	        if (($player['id'] ?? '') !== $playerId) continue;
		        if (!empty($player['marketListedByUser'])) return manager_create_market_transfer_offer($state, $season, $user, $player, $data, $now);
		        if (manager_team_controlled_by_human($state, $season, (string)($player['sourceTeamId'] ?? ''))) reply(['ok' => false, 'error' => 'human_transfer_locked'], 409);
		        $price = (int)($player['price'] ?? player_value($player));
	        $cashOffer = manager_transfer_cash_offer($data, $price);
	        $offerPlayerId = trim((string)($data['offerPlayerId'] ?? ''));
	        $offerPlayer = manager_transfer_offer_player($state['users'][$user], $offerPlayerId);
	        if ($offerPlayerId !== '' && !$offerPlayer) reply(['ok' => false, 'error' => 'bad_offer_player'], 404);
	        manager_ensure_bench_room_for_receive($state['users'][$user]['squad'] ?? [], $offerPlayer);
	        $offerValue = $offerPlayer ? (int)$offerPlayer['value'] : 0;
	        $offerTotal = $cashOffer + $offerValue;
	        if ((int)$state['users'][$user]['managerFinance']['balance'] < $cashOffer) reply(['ok' => false, 'error' => 'no_money'], 409);
	        if ($offerTotal < $price) reply(['ok' => false, 'error' => 'low_offer', 'basePrice' => $price, 'cashPrice' => $cashOffer, 'offerValue' => $offerValue, 'offerTotal' => $offerTotal], 409);
	        $rawSkillBefore = squad_skill_points($state['users'][$user]['squad'] ?? []);
	        $skillBefore = manager_display_skill($rawSkillBefore);
	        $state['users'][$user]['managerFinance']['balance'] -= $cashOffer;
	        $state['users'][$user]['managerFinance']['transferSpent'] += $cashOffer;
	        $tradedPlayer = manager_take_transfer_offer_player($state['users'][$user], $offerPlayerId, (string)($player['sourceTeam'] ?? 'TRANSFER MARKET'), (string)($player['sourceTeamId'] ?? 'market'), (string)($player['sourceDivision'] ?? ''), $now);
	        $offerValue = $tradedPlayer ? (int)($tradedPlayer['value'] ?? player_value($tradedPlayer)) : 0;
	        ensure_player_id_unique_for_squad($player, $state['users'][$user]['squad'] ?? []);
	        $player['purchasedFrom'] = (string)($player['sourceTeam'] ?? 'TRANSFER MARKET');
	        $player['purchasedFromId'] = (string)($player['sourceTeamId'] ?? '');
	        $player['purchasedFromDivision'] = (string)($player['sourceDivision'] ?? '');
	        $player['purchasedAt'] = $now;
	        $player['purchasePrice'] = $cashOffer;
		        unset($player['price']);
		        $state['users'][$user]['squad']['bench'][] = $player;
		        ensure_squad_stats($state['users'][$user]['squad']);
        array_splice($state['managerMarket'], $index, 1);
        ensure_manager_market($state);
        $boughtPlayer = public_player($player);
        $boughtPlayer['value'] = player_value($boughtPlayer);
        $rawSkillAfter = squad_skill_points($state['users'][$user]['squad'] ?? []);
        $skillAfter = manager_display_skill($rawSkillAfter);
        return [
          'managerSeason' => public_manager_season($season, $state, $user),
          'managerFinance' => public_finance($state['users'][$user]),
          'managerSquad' => public_manager_context_squad($state, $user, isset($season) && is_array($season) ? $season : current_manager_season($state, $user)),
	          'managerMarket' => public_manager_market($state, $season),
	          'managerTransferOffers' => public_manager_transfer_offers($state, $user),
	          'boughtPlayer' => $boughtPlayer,
	          'price' => $cashOffer,
	          'cashPrice' => $cashOffer,
	          'basePrice' => $price,
	          'offerValue' => $offerValue,
	          'offerTotal' => $cashOffer + $offerValue,
	          'skillChange' => [
            'before' => $skillBefore,
            'after' => $skillAfter,
            'delta' => $skillAfter - $skillBefore,
            'rawBefore' => $rawSkillBefore,
            'rawAfter' => $rawSkillAfter,
            'rawDelta' => $rawSkillAfter - $rawSkillBefore,
          ],
        ];
      }
      reply(['ok' => false, 'error' => 'bad_player'], 400);
    }

    if ($action === 'sellPlayer') {
      $user = require_user($state);
      ensure_user_squad($state['users'][$user]);
      ensure_manager_finance($state['users'][$user]);
      $playerId = (string)($data['playerId'] ?? '');
	    foreach (['bench', 'starters'] as $group) {
	        foreach (($state['users'][$user]['squad'][$group] ?? []) as $index => $player) {
	          if (($player['id'] ?? '') !== $playerId) continue;
	          $season = current_manager_season($state, $user);
	          $humanMarketListing = $season && count($season['humanManagers'] ?? []) > 1;
	          if (!$humanMarketListing && $group === 'starters' && empty($state['users'][$user]['squad']['bench'])) reply(['ok' => false, 'error' => 'no_replacement'], 409);
	          $saleBase = manager_player_sale_base($player);
	          $fee = manager_transfer_cash_offer($data, $saleBase);
	          $fee = max(1000, (int)round($fee));
	          if ($humanMarketListing) {
	            $listedPlayer = manager_add_sold_player_to_market($state, $user, $state['users'][$user], $player, $fee, $now, false, $season);
	            manager_generate_cpu_market_offers($state, $season, $now, $user);
	            manager_log($state, 'list_player', ['user' => $user, 'player' => $player['name'] ?? '', 'playerId' => $listedPlayer['id'] ?? '', 'price' => $fee, 'base' => $saleBase, 'marketListed' => true]);
	            return [
	              'managerSeason' => public_manager_season($season, $state, $user),
	              'managerFinance' => public_finance($state['users'][$user]),
	              'managerSquad' => public_manager_context_squad($state, $user, isset($season) && is_array($season) ? $season : current_manager_season($state, $user)),
	              'managerMarket' => public_manager_market($state, $season),
	              'managerTransferOffers' => public_manager_transfer_offers($state, $user),
	              'salePrice' => $fee,
	              'marketListed' => true,
	              'salePending' => true,
	            ];
	          }
	          $chance = manager_sale_chance($fee, $saleBase);
	          if (mt_rand(1, 100) > $chance) {
	            reply(['ok' => false, 'error' => 'no_buyer', 'chance' => $chance, 'saleBase' => $saleBase, 'askPrice' => $fee], 409);
          }
	          array_splice($state['users'][$user]['squad'][$group], $index, 1);
	          if ($group === 'starters') {
	            $replacement = array_shift($state['users'][$user]['squad']['bench']);
	            if ($replacement) {
              $replacement['homeX'] = $player['homeX'] ?? $replacement['homeX'] ?? 0.5;
              $replacement['homeY'] = $player['homeY'] ?? $replacement['homeY'] ?? 0.5;
              $replacement['x'] = $replacement['homeX'];
              $replacement['y'] = $replacement['homeY'];
              array_splice($state['users'][$user]['squad']['starters'], $index, 0, [$replacement]);
            }
	          }
	          ensure_squad_stats($state['users'][$user]['squad']);
	          $listedPlayer = manager_add_sold_player_to_market($state, $user, $state['users'][$user], $player, $fee, $now);
	          $state['users'][$user]['managerFinance']['balance'] += $fee;
	          $state['users'][$user]['managerFinance']['transferIncome'] += $fee;
		          manager_log($state, 'sell_player', ['user' => $user, 'player' => $player['name'] ?? '', 'playerId' => $listedPlayer['id'] ?? '', 'price' => $fee, 'base' => $saleBase, 'chance' => $chance, 'marketListed' => true]);
	          return [
	            'managerSeason' => public_manager_season($season, $state, $user),
	            'managerFinance' => public_finance($state['users'][$user]),
	            'managerSquad' => public_manager_context_squad($state, $user, isset($season) && is_array($season) ? $season : current_manager_season($state, $user)),
	            'managerMarket' => public_manager_market($state, $season),
	            'salePrice' => $fee,
	            'saleChance' => $chance,
	          ];
        }
      }
      reply(['ok' => false, 'error' => 'bad_player'], 400);
    }

    if ($action === 'swapSquadPlayers') {
      $user = require_user($state);
      if (!has_team($state['users'][$user])) reply(['ok' => false, 'error' => 'no_team'], 409);
      $firstId = (string)($data['firstId'] ?? '');
      $secondId = (string)($data['secondId'] ?? '');
      if (!swap_squad_players($state['users'][$user], $firstId, $secondId)) reply(['ok' => false, 'error' => 'bad_player'], 400);
      return [
        'managerSeason' => public_manager_season(current_manager_season($state, $user), $state, $user),
        'managerFinance' => public_finance($state['users'][$user]),
        'managerSquad' => public_manager_context_squad($state, $user, isset($season) && is_array($season) ? $season : current_manager_season($state, $user)),
      ];
    }

    if ($action === 'setSquadFormation') {
      $user = require_user($state);
      if (!has_team($state['users'][$user])) reply(['ok' => false, 'error' => 'no_team'], 409);
      $formation = valid_manager_formation((string)($data['formation'] ?? ''));
      $season = null;
      $dailyForm = [];
      $injuredPlayerIds = [];
      $seasonId = current_manager_season_id($state, $user);
      if ($seasonId !== null && isset($state['managerSeasons'][$seasonId])) {
        $season =& $state['managerSeasons'][$seasonId];
        if (manager_participant($season, $user) !== null) {
          $sideData = manager_side_data($state, $season, $user);
          manager_lock_next_form_for_side($season, $user, $sideData);
          $sideData = manager_apply_next_form_to_side_data($season, $user, $sideData);
          $dailyForm = is_array($sideData['dailyForm'] ?? null) ? $sideData['dailyForm'] : [];
          $injuredPlayerIds = array_keys(manager_active_injuries($season, $user));
        }
      }
      if ($injuredPlayerIds) {
        $fitPlayers = 0;
        $injuredSet = array_fill_keys(array_map('strval', $injuredPlayerIds), true);
        foreach (array_merge($state['users'][$user]['squad']['starters'] ?? [], $state['users'][$user]['squad']['bench'] ?? []) as $player) {
          $id = (string)($player['id'] ?? '');
          if ($id === '' || empty($injuredSet[$id])) $fitPlayers++;
        }
        if ($fitPlayers < 11) reply(['ok' => false, 'error' => 'not_enough_fit_players', 'fitPlayers' => $fitPlayers], 409);
      }
      if (!apply_squad_formation($state['users'][$user], $formation, $dailyForm, $injuredPlayerIds)) reply(['ok' => false, 'error' => 'bad_formation'], 400);
      return [
        'managerSeason' => public_manager_season(current_manager_season($state, $user), $state, $user),
        'managerFinance' => public_finance($state['users'][$user]),
        'managerSquad' => public_manager_context_squad($state, $user, isset($season) && is_array($season) ? $season : current_manager_season($state, $user)),
      ];
    }

    if ($action === 'selectTeam') {
      $user = require_user($state);
      if (has_team($state['users'][$user])) reply(['ok' => false, 'error' => 'team_locked'], 409);
      $team = team_by_id((string)($data['teamId'] ?? ''));
      if (!$team) reply(['ok' => false, 'error' => 'bad_team'], 400);
      $state['users'][$user]['teamId'] = $team['id'];
      $state['users'][$user]['teamName'] = $team['name'];
      $state['users'][$user]['shirt'] = $team['shirt'];
      $state['users'][$user]['trim'] = $team['trim'];
      ensure_user_squad($state['users'][$user]);
    }

    if ($action === 'find') {
      $user = require_user($state);
      if (!has_team($state['users'][$user])) reply(['ok' => false, 'error' => 'no_team'], 409);
      ensure_user_squad($state['users'][$user]);
      if (($existing = user_match_id($state, $user)) !== null) {
        tick_state_match($state, $existing);
        return ['match' => public_match($state['matches'][$existing], $user)];
      }
      foreach ($state['matches'] as &$match) {
        if (($match['status'] ?? '') === 'waiting' && ($match['players']['home'] ?? '') !== $user) {
          start_waiting_match($match, $user, $state['users'][$user]);
          return ['match' => public_match($match, $user)];
        }
      }
      unset($match);
      $match = new_match($user, $state['users'][$user]);
      $state['matches'][$match['id']] = $match;
      return ['match' => public_match($match, $user)];
    }

    if ($action === 'cpu') {
      $user = require_user($state);
      if (!has_team($state['users'][$user])) reply(['ok' => false, 'error' => 'no_team'], 409);
      ensure_user_squad($state['users'][$user]);
      if (($existing = user_match_id($state, $user)) !== null) {
        tick_state_match($state, $existing);
        return ['match' => public_match($state['matches'][$existing], $user)];
      }
      $match = new_cpu_match($user, $state['users'][$user], $state);
      $state['matches'][$match['id']] = $match;
      return ['match' => public_match($match, $user)];
    }

    if ($action === 'start') {
      $user = require_user($state);
      if (($id = user_match_id($state, $user)) === null) reply(['ok' => false, 'error' => 'no_match'], 409);
      if (($state['matches'][$id]['status'] ?? '') === 'ready') {
        $state['matches'][$id]['status'] = 'active';
        begin_kickoff($state['matches'][$id], 'home', 0.45);
        $state['matches'][$id]['lastTick'] = microtime(true);
        $state['matches'][$id]['updated'] = $now;
      }
      tick_state_match($state, $id);
      return ['match' => public_match($state['matches'][$id], $user)];
    }

    if ($action === 'leave') {
      $user = require_user($state);
      if (($id = user_match_id($state, $user)) !== null) {
        $state['matches'][$id]['status'] = 'finished';
        $state['matches'][$id]['updated'] = $now;
        sync_manager_match($state, $id);
      }
    }

    if ($action === 'substitute') {
      $user = require_user($state);
      if (($id = user_match_id($state, $user)) === null) reply(['ok' => false, 'error' => 'no_match'], 409);
      $side = (($state['matches'][$id]['players']['home'] ?? '') === $user) ? 'home' : 'away';
      $sub = make_substitution($state['matches'][$id], $side);
      if (!$sub['ok']) reply($sub, 409);
      $state['matches'][$id]['updated'] = $now;
      tick_state_match($state, $id);
      return ['match' => public_match($state['matches'][$id], $user), 'substitution' => $sub];
    }

    if ($action === 'tactic') {
      $user = require_user($state);
      $mode = (string)($data['mode'] ?? 'balanced');
      if (!in_array($mode, ['defend', 'balanced', 'attack'], true)) $mode = 'balanced';
      if (($id = user_match_id($state, $user)) !== null) {
        $side = (($state['matches'][$id]['players']['home'] ?? '') === $user) ? 'home' : 'away';
        $state['matches'][$id]['tactics'][$side] = $mode;
        tick_state_match($state, $id);
      }
    }

    $logged = is_string($user) && isset($state['users'][$user]);
    $match = null;
    if ($logged && ($id = user_match_id($state, $user)) !== null) {
      tick_state_match($state, $id);
      $match = public_match($state['matches'][$id], $user);
    }

    return [
      'user' => $logged ? public_user($user, $state['users'][$user]) : null,
      'teams' => public_teams($state, $logged ? $user : null),
      'match' => $match,
      'managerInvites' => $logged ? public_manager_invites($state, $user) : [],
      'managerHasSeason' => $logged ? current_manager_season($state, $user) !== null : false,
      'managerCanContinue' => $logged ? manager_user_can_continue($state, $user) : false,
      'managerHasAnySave' => $logged ? manager_user_has_any_save($user) : false,
      'appVersion' => app_version(),
    ];
  });

  if (is_array($result) && !isset($result['appVersion'])) $result['appVersion'] = app_version();
  reply(['ok' => true] + $result);
} catch (Throwable $e) {
  $safeAction = is_scalar($action ?? null) ? (string)$action : 'unknown';
  $safeUser = is_string($_SESSION['user'] ?? null) ? (string)$_SESSION['user'] : '';
  error_log('YSP API error action=' . $safeAction . ' user=' . $safeUser . ' message=' . $e->getMessage() . "\n" . $e->getTraceAsString());
  reply(['ok' => false, 'error' => 'server_error'], 500);
}
