<?php
declare(strict_types=1);

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

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

function legacy_ysp_team_dirs(): array {
  return array_values(array_filter([
    YSP_CODE_ROOT . '/../../backup/ysp/code/gamefiles/data/teams/2024-25/CLUB_TEAMS/EUROPE/ENGLAND',
    YSP_CODE_ROOT . '/../../backup/code/gamefiles/data/teams/2024-25/CLUB_TEAMS/EUROPE/ENGLAND',
    YSP_CODE_ROOT . '/../backup/ysp/code/gamefiles/data/teams/2024-25/CLUB_TEAMS/EUROPE/ENGLAND',
    YSP_CODE_ROOT . '/../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 = [];
  $candidates[] = $direct;
  if (isset($aliases[$sourceId])) $candidates[] = $aliases[$sourceId];
  $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 team_flag_catalog(): array {
  static $data = null;
  if ($data !== null) return $data;
  if (!is_file(TEAM_FLAGS_FILE)) return $data = [];
  $json = json_decode((string)file_get_contents(TEAM_FLAGS_FILE), true);
  return $data = is_array($json) ? $json : [];
}

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

function normalize_hex_color(string $value, string $fallback): string {
  $value = trim($value);
  if (preg_match('/^#?[0-9a-fA-F]{6}$/', $value)) return '#' . strtolower(ltrim($value, '#'));
  if (preg_match('/^#?[0-9a-fA-F]{3}$/', $value)) {
    $short = strtolower(ltrim($value, '#'));
    return '#' . $short[0] . $short[0] . $short[1] . $short[1] . $short[2] . $short[2];
  }
  return $fallback;
}

function normalize_flag_pattern(string $pattern): string {
  $pattern = strtolower(trim($pattern));
  return in_array($pattern, ['plain', 'vertical', 'horizontal', 'stripes-v', 'stripes-h', 'hoops', 'halves', 'quarters', 'sash', 'cross', 'check'], true) ? $pattern : 'plain';
}

function apply_team_flag_fields(array $team): array {
  $sourceId = source_team_id((string)($team['sourceId'] ?? $team['id'] ?? ''));
  $flag = team_flag_catalog()[$sourceId] ?? [];
  if (is_array($flag)) {
    $team['shirt'] = normalize_hex_color((string)($flag['shirt'] ?? $team['shirt'] ?? ''), (string)($team['shirt'] ?? '#ffffff'));
    $team['trim'] = normalize_hex_color((string)($flag['trim'] ?? $team['trim'] ?? ''), (string)($team['trim'] ?? '#111111'));
    $team['flagPattern'] = normalize_flag_pattern((string)($flag['flagPattern'] ?? 'plain'));
    $team['flagAccent'] = normalize_hex_color((string)($flag['flagAccent'] ?? $team['trim'] ?? ''), (string)($team['trim'] ?? '#111111'));
  } else {
    $team['flagPattern'] = 'plain';
    $team['flagAccent'] = (string)($team['trim'] ?? '#111111');
  }
  $logo = team_logo_catalog()[$sourceId] ?? [];
  if (is_array($logo) && !empty($logo['path']) && is_string($logo['path'])) {
    $path = $logo['path'];
    if (preg_match('~^assets/logos/[a-z0-9/_-]+\\.png(?:\\?v=[a-z0-9]+)?$~', $path)) $team['logo'] = $path;
  }
  return $team;
}

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 normalize_legacy_team_player_row($player): ?array {
  if (!is_array($player)) return null;
  $name = trim((string)($player['name'] ?? ''));
  if ($name === '') return null;
  $shirtName = trim((string)($player['shirtName'] ?? $player['shirt_name'] ?? ''));
  $row = [
    'name' => $name,
    'shirtName' => $shirtName !== '' ? $shirtName : strtoupper((string)preg_replace('/^.*\s+/', '', $name)),
    'legacyRole' => trim((string)($player['legacyRole'] ?? $player['role'] ?? $player['position'] ?? '')),
  ];
  if (isset($player['legacyNumber']) || isset($player['number']) || isset($player['shirtNumber'])) {
    $number = (int)($player['legacyNumber'] ?? $player['number'] ?? $player['shirtNumber']);
    if ($number > 0) $row['legacyNumber'] = $number;
  }
  $country = trim((string)($player['country'] ?? $player['nationality'] ?? ''));
  if ($country !== '') {
    $row['country'] = $country;
    $row['nationality'] = $country;
  }
  $code = strtolower(trim((string)($player['countryCode'] ?? $player['nationalityCode'] ?? '')));
  if ($code !== '') {
    $row['countryCode'] = $code;
    $row['nationalityCode'] = $code;
  }
  if (!empty($player['countries']) && is_array($player['countries'])) {
    $row['countries'] = array_values(array_filter(array_map(fn($item) => trim((string)$item), $player['countries']), fn($item) => $item !== ''));
  }
  if (!empty($player['countryCodes']) && is_array($player['countryCodes'])) {
    $row['countryCodes'] = array_values(array_filter(array_map(fn($item) => strtolower(trim((string)$item)), $player['countryCodes']), fn($item) => $item !== ''));
  }
  if (!empty($player['source']) && is_string($player['source'])) $row['source'] = trim($player['source']);
  return $row;
}

function legacy_team_players_from_file(string $file): array {
  if (!is_file($file)) return [];
  $json = json_decode((string)file_get_contents($file), true);
  if (!is_array($json)) return [];
  $rows = is_array($json['players'] ?? null) ? $json['players'] : $json;
  $players = [];
  foreach ($rows as $player) {
    $row = normalize_legacy_team_player_row($player);
    if ($row) $players[] = $row;
  }
  return $players;
}

function legacy_team_player_file_candidates(string $teamId): array {
  if (!is_dir(TEAM_PLAYER_DIR)) return [];
  $files = [];
  foreach (legacy_team_slug_candidates($teamId) as $slug) {
    $slug = trim($slug);
    if ($slug === '') continue;
    $files[] = TEAM_PLAYER_DIR . '/' . $slug . '.json';
  }
  return array_values(array_unique($files));
}

function player_nationality_catalog(): array {
  static $data = null;
  if ($data !== null) return $data;
  if (!is_file(PLAYER_NATIONALITIES_FILE)) return $data = [];
  $json = json_decode((string)file_get_contents(PLAYER_NATIONALITIES_FILE), true);
  if (isset($json['teams']) && is_array($json['teams'])) $json = $json['teams'];
  return $data = is_array($json) ? $json : [];
}

function normalize_player_nationality_key(string $name): string {
  $name = html_entity_decode($name, ENT_QUOTES | ENT_HTML5, 'UTF-8');
  $name = str_replace(["\xc2\xa0", "\u{00a0}", '’', '`'], [' ', ' ', "'", "'"], $name);
  $name = strtolower(trim((string)preg_replace('/\s+/u', ' ', $name)));
  return (string)preg_replace('/[^a-z0-9]+/u', '', $name);
}

function player_country_code(string $country): string {
  $key = strtolower(trim($country));
  $codes = [
    'afghanistan' => 'af',
    'albania' => 'al',
    'angola' => 'ao',
    'antigua and barbuda' => 'ag',
    'australia' => 'au',
    'belgium' => 'be',
    'belize' => 'bz',
    'bulgaria' => 'bg',
    'burundi' => 'bi',
    'cameroon' => 'cm',
    'colombia' => 'co',
    'congo' => 'cg',
    "cote d'ivoire" => 'ci',
    'cyprus' => 'cy',
    'dominica' => 'dm',
    'dr congo' => 'cd',
    'ecuador' => 'ec',
    'england' => 'gb-eng',
    'finland' => 'fi',
    'france' => 'fr',
    'ghana' => 'gh',
    'gibraltar' => 'gi',
    'greece' => 'gr',
    'grenada' => 'gd',
    'guyana' => 'gy',
    'india' => 'in',
    'ireland' => 'ie',
    'jamaica' => 'jm',
    'kenya' => 'ke',
    'kosovo' => 'xk',
    'latvia' => 'lv',
    'lebanon' => 'lb',
    'mali' => 'ml',
    'malta' => 'mt',
    'montserrat' => 'ms',
    'nepal' => 'np',
    'netherlands' => 'nl',
    'new zealand' => 'nz',
    'nigeria' => 'ng',
    'northern ireland' => 'gb-nir',
    'pakistan' => 'pk',
    'poland' => 'pl',
    'scotland' => 'gb-sct',
    'somalia' => 'so',
    'south africa' => 'za',
    'spain' => 'es',
    'st. lucia' => 'lc',
    'st. vincent & grenadinen' => 'vc',
    'suriname' => 'sr',
    'turkey' => 'tr',
    'turkiye' => 'tr',
    'uganda' => 'ug',
    'united states' => 'us',
    'wales' => 'gb-wls',
    'zimbabwe' => 'zw',
  ];
  return $codes[$key] ?? '';
}

function normalize_player_nationality_row($row): array {
  if (is_string($row)) $row = ['country' => $row, 'countries' => [$row]];
  if (!is_array($row)) return [];
  $country = trim((string)($row['country'] ?? $row['nationality'] ?? ''));
  if ($country === '') return [];
  $countries = $row['countries'] ?? [$country];
  if (!is_array($countries)) $countries = [$country];
  $countries = array_values(array_filter(array_map(fn($item) => trim((string)$item), $countries), fn($item) => $item !== ''));
  $code = strtolower(trim((string)($row['code'] ?? $row['countryCode'] ?? $row['nationalityCode'] ?? '')));
  if ($code === '') $code = player_country_code($country);
  $countryCodes = $row['countryCodes'] ?? [];
  if (!is_array($countryCodes)) $countryCodes = [];
  if (!$countryCodes && $countries) {
    foreach ($countries as $item) {
      $itemCode = player_country_code((string)$item);
      if ($itemCode !== '') $countryCodes[] = $itemCode;
    }
  }
  return [
    'country' => $country,
    'code' => $code,
    'countries' => $countries,
    'countryCodes' => array_values(array_unique(array_filter(array_map(fn($item) => strtolower(trim((string)$item)), $countryCodes)))),
  ];
}

function player_nationality_for(string $teamId, string $teamName, string $playerName): array {
  $catalog = player_nationality_catalog();
  if (!$catalog || trim($playerName) === '') return [];
  $candidates = legacy_team_slug_candidates($teamId);
  if ($teamName !== '') $candidates[] = str_replace('-', '_', source_team_id($teamName));
  $targetKey = normalize_player_nationality_key($playerName);
  foreach (array_values(array_unique(array_filter($candidates))) as $slug) {
    if (empty($catalog[$slug]) || !is_array($catalog[$slug])) continue;
    $players = $catalog[$slug];
    if (isset($players[$playerName])) return normalize_player_nationality_row($players[$playerName]);
    foreach ($players as $name => $row) {
      if (normalize_player_nationality_key((string)$name) === $targetKey) return normalize_player_nationality_row($row);
    }
  }
  return [];
}

function apply_player_nationality_fields(array &$player, string $teamId = '', string $teamName = ''): void {
  $playerName = (string)($player['originName'] ?? $player['name'] ?? '');
  if ($teamId === '') $teamId = (string)($player['originTeamId'] ?? $player['sourceTeamId'] ?? $player['purchasedFromId'] ?? '');
  if ($teamName === '') $teamName = (string)($player['originTeamName'] ?? $player['sourceTeam'] ?? $player['purchasedFrom'] ?? '');
  $row = player_nationality_for($teamId, $teamName, $playerName);
  if (!$row) return;
  $player['nationality'] = $row['country'];
  $player['country'] = $row['country'];
  if ($row['code'] !== '') {
    $player['nationalityCode'] = $row['code'];
    $player['countryCode'] = $row['code'];
  }
  if ($row['countries']) $player['countries'] = $row['countries'];
  if ($row['countryCodes']) $player['countryCodes'] = $row['countryCodes'];
}

function apply_squad_nationalities(array &$squad, string $teamId, string $teamName = ''): void {
  foreach (['starters', 'bench'] as $group) {
    if (empty($squad[$group]) || !is_array($squad[$group])) continue;
    foreach ($squad[$group] as &$player) {
      if (is_array($player)) apply_player_nationality_fields($player, $teamId, $teamName);
    }
    unset($player);
  }
}

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

  foreach (legacy_team_player_file_candidates($teamId) as $file) {
    $players = legacy_team_players_from_file($file);
    if ($players) return $cache[$cacheKey] = $players;
  }

  $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'];
        $player['shirtNumber'] = max(1, (int)$legacy[$index]['legacyNumber']);
      }
      if (function_exists('squad_role_from_legacy_row')) {
        $role = squad_role_from_legacy_row($legacy[$index], (string)($player['naturalRole'] ?? $player['role'] ?? 'CM'), (int)crc32((string)($teamId . '|' . $teamName . '|' . $index)));
        if ($role !== '') {
          $player['role'] = $role;
          $player['naturalRole'] = $role;
          $player['displayRole'] = $role;
          if (empty($player['slotRole'])) $player['slotRole'] = $role;
        }
      }
      if (!empty($legacy[$index]['country'])) {
        $player['nationality'] = (string)$legacy[$index]['country'];
        $player['country'] = (string)$legacy[$index]['country'];
      }
      if (!empty($legacy[$index]['countryCode'])) {
        $player['nationalityCode'] = (string)$legacy[$index]['countryCode'];
        $player['countryCode'] = (string)$legacy[$index]['countryCode'];
      }
      if (!empty($legacy[$index]['countries']) && is_array($legacy[$index]['countries'])) $player['countries'] = $legacy[$index]['countries'];
      if (!empty($legacy[$index]['countryCodes']) && is_array($legacy[$index]['countryCodes'])) $player['countryCodes'] = $legacy[$index]['countryCodes'];
      $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 FC', '#d71920', '#ffffff'],
    ['barrow', 'BARROW AFC', '#0057b8', '#ffffff'],
    ['boreham-wood', 'BOREHAM WOOD FC', '#ffffff', '#1e1e1e'],
    ['boston-united', 'BOSTON UNITED', '#f2a900', '#1e1e1e'],
    ['carlisle-united', 'CARLISLE UNITED', '#0057b8', '#ffffff'],
    ['eastleigh', 'EASTLEIGH FC', '#0057b8', '#ffffff'],
    ['fc-halifax-town', 'FC HALIFAX TOWN', '#0057b8', '#ffffff'],
    ['forest-green-rovers', 'FOREST GREEN ROVERS', '#78be20', '#1e1e1e'],
    ['gateshead', 'GATESHEAD FC', '#ffffff', '#1e1e1e'],
    ['harrogate-town', 'HARROGATE TOWN', '#f3c300', '#1e1e1e'],
    ['hartlepool-united', 'HARTLEPOOL UNITED', '#003b79', '#ffffff'],
    ['hornchurch', 'HORNCHURCH FC', '#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 FC', '#d71920', '#1e1e1e'],
    ['wealdstone', 'WEALDSTONE FC', '#0057b8', '#ffffff'],
    ['woking', 'WOKING FC', '#d71920', '#ffffff'],
    ['worthing', 'WORTHING FC', '#d71920', '#ffffff'],
    ['yeovil-town', 'YEOVIL TOWN', '#15803d', '#ffffff'],
  ];
  $teams = array_map(fn($row) => apply_team_flag_fields(['id' => $row[0], 'sourceId' => $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 FC', '#f5732c', '#1e1e1e'],
      ['bristol-rovers', 'BRISTOL ROVERS', '#0057b8', '#ffffff'],
      ['cheltenham-town', 'CHELTENHAM TOWN', '#d71920', '#ffffff'],
      ['chesterfield', 'CHESTERFIELD FC', '#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 FC', '#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 FC', '#ffffff', '#1e1e1e'],
      ['rochdale', 'ROCHDALE AFC', '#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 FC', '#d71920', '#ffffff'],
      ['york-city', 'YORK CITY', '#c8102e', '#ffffff'],
    ]],
    ['league-one', 'League One', 59, [
      ['afc-wimbledon', 'AFC WIMBLEDON', '#0050a4', '#f3c300'],
      ['barnsley', 'BARNSLEY FC', '#d71920', '#ffffff'],
      ['blackpool', 'BLACKPOOL FC', '#f5732c', '#ffffff'],
      ['bradford-city', 'BRADFORD CITY', '#7a263a', '#f3c300'],
      ['bromley', 'BROMLEY FC', '#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', 'MILTON KEYNES 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 FC', '#0057b8', '#ffffff'],
      ['sheffield-wednesday', 'SHEFFIELD WEDNESDAY', '#0057b8', '#ffffff'],
      ['stevenage', 'STEVENAGE FC', '#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 FC', '#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 FC', '#d71920', '#ffffff'],
      ['millwall', 'MILLWALL FC', '#003b79', '#ffffff'],
      ['norwich-city', 'NORWICH CITY', '#f3c300', '#15803d'],
      ['portsmouth', 'PORTSMOUTH FC', '#0057b8', '#ffffff'],
      ['preston-north-end', 'PRESTON NORTH END', '#ffffff', '#003b79'],
      ['qpr', 'QUEENS PARK RANGERS', '#0057b8', '#ffffff'],
      ['sheffield-united', 'SHEFFIELD UNITED', '#d71920', '#ffffff'],
      ['southampton', 'SOUTHAMPTON FC', '#d71920', '#ffffff'],
      ['stoke-city', 'STOKE CITY', '#d71920', '#ffffff'],
      ['swansea-city', 'SWANSEA CITY', '#ffffff', '#1e1e1e'],
      ['watford', 'WATFORD FC', '#f3c300', '#1e1e1e'],
      ['west-bromwich-albion', 'WEST BROMWICH ALBION', '#003b79', '#ffffff'],
      ['west-ham-united', 'WEST HAM UNITED', '#7a263a', '#95bfe5'],
      ['wolves', 'WOLVERHAMPTON WANDERERS', '#f2a900', '#1e1e1e'],
      ['wrexham', 'WREXHAM AFC', '#d71920', '#ffffff'],
    ]],
    ['premier-league', 'Premier League', 80, [
      ['arsenal', 'ARSENAL FC', '#d71920', '#ffffff'],
      ['aston-villa', 'ASTON VILLA', '#95bfe5', '#7a263a'],
      ['bournemouth', 'AFC BOURNEMOUTH', '#d71920', '#1e1e1e'],
      ['brentford', 'BRENTFORD FC', '#d71920', '#ffffff'],
      ['brighton', 'BRIGHTON & HOVE ALBION', '#0057b8', '#ffffff'],
      ['chelsea', 'CHELSEA FC', '#034694', '#ffffff'],
      ['coventry-city', 'COVENTRY CITY', '#78c7ef', '#ffffff'],
      ['crystal-palace', 'CRYSTAL PALACE', '#0057b8', '#d71920'],
      ['everton', 'EVERTON FC', '#0057b8', '#ffffff'],
      ['fulham', 'FULHAM FC', '#ffffff', '#1e1e1e'],
      ['hull-city', 'HULL CITY', '#f28c28', '#1e1e1e'],
      ['ipswich-town', 'IPSWICH TOWN', '#0057b8', '#ffffff'],
      ['leeds-united', 'LEEDS UNITED', '#ffffff', '#f3c300'],
      ['liverpool', 'LIVERPOOL FC', '#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 AFC', '#d71920', '#ffffff'],
      ['tottenham-hotspur', 'TOTTENHAM HOTSPUR', '#ffffff', '#132257'],
    ]],
  ];
  $rows[0][3] = array_map(fn($team) => [$team['id'], $team['name'], $team['shirt'], $team['trim'], $team['flagPattern'] ?? 'plain', $team['flagAccent'] ?? ($team['trim'] ?? '#111111')], 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[] = apply_team_flag_fields([
        '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,
    'managerHumanSnapshotVersion' => 0,
    'managerYear' => 2026,
    'teamTrophies' => [],
    'managerLog' => [],
    'managerInvites' => [],
    'managerChats' => [],
    'managerPrivateChats' => [],
    'managerMarket' => [],
    'managerBrowseOffers' => [],
    'managerTransferOffers' => [],
    'managerTransferNotices' => [],
    'managerBrowseSold' => [],
  ];
}

function migrate_state(array $state): array {
  $defaults = default_state();
  $state += $defaults;
  foreach (['users', 'matches', 'managerSeasons', 'managerCurrentByUser', 'teamTrophies', 'managerLog', 'managerInvites', 'managerChats', 'managerPrivateChats', '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;
  foreach ($state['users'] as $name => &$account) {
    if (is_array($account)) $account['accountName'] = (string)$name;
  }
  unset($account);
  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;
  }
  if ((int)($state['managerHumanSnapshotVersion'] ?? 0) < MANAGER_HUMAN_SNAPSHOT_VERSION) {
    foreach ($state['managerSeasons'] as &$season) {
      if (is_array($season)) manager_refresh_human_participant_snapshots($state, $season);
    }
    unset($season);
    $state['managerHumanSnapshotVersion'] = MANAGER_HUMAN_SNAPSHOT_VERSION;
  }
  foreach ($state['managerSeasons'] as &$season) {
    if (!is_array($season)) continue;
    foreach (array_values($season['humanManagers'] ?? []) as $name) {
      manager_prune_invalid_training($state, $season, (string)$name);
    }
  }
  unset($season);
  manager_repair_human_owned_cpu_conflicts($state);
  $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 state_json_is_valid(string $raw): bool {
  if (trim($raw) === '') return false;
  if (function_exists('json_validate')) return json_validate($raw);
  json_decode($raw, true);
  return json_last_error() === JSON_ERROR_NONE;
}

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 (state_json_is_valid($current)) {
    @file_put_contents(STATE_BACKUP_FILE, $current, LOCK_EX);
    @chmod(STATE_BACKUP_FILE, 0666);
  }
  if (!state_json_is_valid($encoded)) return false;
  $written = @file_put_contents($tmp, $encoded, LOCK_EX);
  if ($written === false || $written !== strlen($encoded)) {
    @unlink($tmp);
    return false;
  }
  @chmod($tmp, 0666);
  if (!@rename($tmp, STATE_FILE)) {
    @unlink($tmp);
    return false;
  }
  @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);
  if (function_exists('ysp_db_sync_users_into_state')) ysp_db_sync_users_into_state($state);
  $result = $fn($state);
  manager_sync_current_user_scout_finance_to_participants($state);
  if (function_exists('ysp_db_sync_users_from_state')) ysp_db_sync_users_from_state($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 with_state_read(callable $fn): mixed {
  $fh = @fopen(STATE_LOCK_FILE, 'c');
  if (!$fh) $fh = @fopen(STATE_FILE, 'r');
  if (!$fh) reply(['ok' => false, 'error' => 'state_lock_failed'], 500);
  @chmod(STATE_LOCK_FILE, 0666);
  flock($fh, LOCK_SH);
  $rawState = '';
  $state = read_state_file_locked($fh, $rawState);
  $state = migrate_state($state);
  if (function_exists('ysp_db_sync_users_into_state')) ysp_db_sync_users_into_state($state);
  $result = $fn($state);
  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]);
  }
  manager_cleanup_stale_market_listings($state, $now);
  manager_sync_current_user_scout_finance_to_participants($state);
}

function manager_copy_scout_finance_fields(array $sourceFinance, array &$targetFinance): void {
  $targetFinance['hiredScout'] = manager_public_hired_scout($sourceFinance);
  $targetFinance['scoutSearch'] = manager_public_scout_search($sourceFinance);
  $targetFinance['scoutSpent'] = (int)($sourceFinance['scoutSpent'] ?? 0);
}

function manager_sync_current_user_scout_finance_to_participants(array &$state): void {
  if (empty($state['users']) || 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) || ($participant['managerType'] ?? '') !== 'human') continue;
        $name = clean_name((string)($participant['user'] ?? ''));
        if ($name === '' || empty($state['users'][$name]) || !is_array($state['users'][$name])) continue;
        if (!manager_global_user_matches_season($state, $season, $name)) continue;
        ensure_manager_finance($state['users'][$name]);
        $sourceFinance = $state['users'][$name]['managerFinance'];
        if (!isset($participant['managerFinance']) || !is_array($participant['managerFinance'])) $participant['managerFinance'] = [];
        manager_copy_scout_finance_fields($sourceFinance, $participant['managerFinance']);
        if (!isset($participant['userSnapshot']) || !is_array($participant['userSnapshot'])) $participant['userSnapshot'] = [];
        if (!isset($participant['userSnapshot']['managerFinance']) || !is_array($participant['userSnapshot']['managerFinance'])) $participant['userSnapshot']['managerFinance'] = [];
        manager_copy_scout_finance_fields($sourceFinance, $participant['userSnapshot']['managerFinance']);
      }
      unset($participant);
    }
  }
  unset($season);
}

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])) {
    unset($_SESSION['user'], $_SESSION['csrfToken']);
    reply(['ok' => false, 'error' => 'not_logged_in', 'loggedOut' => true], 401);
  }
  return $user;
}

function admin_owner_name(array $state): string {
  $owner = '';
  $bestCreated = PHP_INT_MAX;
  $bestIndex = PHP_INT_MAX;
  $index = 0;
  foreach (($state['users'] ?? []) as $name => $account) {
    if (!is_array($account) || empty($account['password'])) {
      $index++;
      continue;
    }
    $created = (int)($account['created'] ?? 0);
    if ($created <= 0) $created = PHP_INT_MAX;
    if ($created < $bestCreated || ($created === $bestCreated && $index < $bestIndex)) {
      $owner = (string)$name;
      $bestCreated = $created;
      $bestIndex = $index;
    }
    $index++;
  }
  return $owner;
}

function admin_user_can_manage(array $state, string $user): bool {
  $owner = admin_owner_name($state);
  return $owner !== '' && clean_name($user) === clean_name($owner);
}

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

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

function public_admin_users(array $state): array {
  $out = [];
  foreach (($state['users'] ?? []) as $name => $account) {
    if (!is_array($account)) continue;
    $team = has_team($account) ? user_team($account) : null;
    $out[] = [
      'name' => (string)$name,
      'team' => $team,
      'hasTeam' => $team !== null,
      'created' => (int)($account['created'] ?? 0),
      'lastSeen' => (int)($account['lastSeen'] ?? 0),
      'canDelete' => true,
    ];
  }
  usort($out, fn($a, $b) => [(int)($a['created'] ?? 0), (string)$a['name']] <=> [(int)($b['created'] ?? 0), (string)$b['name']]);
  return $out;
}

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 {
  $team = [
    'id' => $user['teamId'],
    'name' => $user['teamName'],
    'shirt' => $user['shirt'] ?? '#f5732c',
    'trim' => $user['trim'] ?? '#1e1e1e',
  ];
  $source = team_by_id((string)($user['teamId'] ?? ''));
  if (is_array($source)) {
    $team['shirt'] = $user['shirt'] ?? ($source['shirt'] ?? $team['shirt']);
    $team['trim'] = $user['trim'] ?? ($source['trim'] ?? $team['trim']);
    $team['flagPattern'] = $user['flagPattern'] ?? ($source['flagPattern'] ?? 'plain');
    $team['flagAccent'] = $user['flagAccent'] ?? ($source['flagAccent'] ?? ($team['trim'] ?? '#111111'));
  }
  return apply_team_flag_fields($team);
}

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 manager_slot_role_for_starter_position(array $user, array $player, string $fallback = ''): string {
  $fallback = canonical_player_role($fallback, player_slot_role($player));
  $x = $player['homeX'] ?? $player['x'] ?? null;
  $y = $player['homeY'] ?? $player['y'] ?? null;
  if (!is_numeric($x) || !is_numeric($y)) return $fallback;
  $formation = manager_user_formation($user);
  $slots = manager_formations()[$formation] ?? manager_formations()['4-4-2'];
  $x = (float)$x;
  $y = (float)$y;
  $bestRole = $fallback;
  $bestDistance = 999.0;
  foreach ($slots as $slot) {
    [$role, $slotX, $slotY] = $slot;
    $dx = ($x - (float)$slotX) * 1.2;
    $dy = $y - (float)$slotY;
    $distance = sqrt($dx * $dx + $dy * $dy);
    if ($distance < $bestDistance) {
      $bestDistance = $distance;
      $bestRole = (string)$role;
    }
  }
  return $bestDistance <= 0.13 ? canonical_player_role($bestRole, $fallback) : $fallback;
}

function normalize_squad_starter_slots(array &$user): void {
  if (empty($user['squad']['starters']) || !is_array($user['squad']['starters'])) return;
  foreach ($user['squad']['starters'] as &$player) {
    ensure_player_roles($player);
    $player['slotRole'] = manager_slot_role_for_starter_position($user, $player, player_slot_role($player));
    $player['role'] = player_natural_role($player);
  }
  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 manager_is_emergency_loan_player(array $player): bool {
  $id = (string)($player['id'] ?? '');
  return !empty($player['emergencyLoan']) || !empty($player['loanPlayer']) || strncmp($id, 'loan_', 5) === 0;
}

function manager_emergency_loan_player(array $user, int $slotIndex): array {
  $teamId = (string)($user['teamId'] ?? '');
  $teamName = (string)($user['teamName'] ?? 'Team');
  $seed = (int)crc32($teamId . '|' . $teamName . '|loan|' . $slotIndex);
  $roles = ['GK', 'CB', 'LB', 'RB', 'CM', 'LM', 'RM', 'ST', 'CM'];
  $role = $roles[$slotIndex % count($roles)];
  $base = clamp_int(team_base_rating($teamId) - 12 - ($seed % 7), 28, 55);
  $stats = [];
  foreach (player_stat_keys($role === 'GK') as $i => $stat) {
    $stats[$stat] = clamp_int($base + role_stat_bonus($role, $stat) + (((int)($seed >> ($i % 16))) % 11) - 5, 0, 100);
  }
  $names = ['Emergency Loan', 'Short Loan', 'Loan Cover', 'Reserve Loan', 'Trial Loan', 'Cover Loan', 'Loan Backup', 'Temp Loan', 'Loan Reserve'];
  $name = $names[$slotIndex % count($names)] . ' ' . ($slotIndex + 1);
  $player = [
    'id' => 'loan_' . (player_team_id_key($teamId, $teamName, 'home') ?: 'team') . '_' . str_pad((string)($slotIndex + 1), 2, '0', STR_PAD_LEFT),
    'shirtNumber' => 90 + $slotIndex,
    'name' => $name,
    'role' => $role,
    'naturalRole' => $role,
    'slotRole' => $role,
    'rating' => average_stats($stats),
    'stats' => $stats,
    'weightClass' => ['light', 'medium', 'heavy'][$seed % 3],
    'homeX' => 0.5,
    'homeY' => 0.9,
    'x' => 0.5,
    'y' => 0.9,
    'emergencyLoan' => true,
    'transferLocked' => true,
    'temporaryLoan' => true,
    'sourceTeam' => 'Emergency Loan Pool',
    'sourceTeamId' => 'loan-pool',
    'sourceDivision' => 'Temporary Cover',
    'value' => 0,
    'wage' => 0,
  ];
  ensure_player_stats($player);
  $player['value'] = 0;
  $player['wage'] = 0;
  return $player;
}

function manager_sync_emergency_loan_bench(array &$user): void {
  if (!has_team($user)) return;
  if (!isset($user['squad']) || !is_array($user['squad'])) return;
  if (!isset($user['squad']['bench']) || !is_array($user['squad']['bench'])) $user['squad']['bench'] = [];
  $realBench = [];
  $loans = [];
  foreach ($user['squad']['bench'] as $player) {
    if (!is_array($player)) continue;
    if (manager_is_emergency_loan_player($player)) $loans[] = $player;
    else $realBench[] = $player;
  }
  $targetBench = MANAGER_MAX_BENCH_PLAYERS;
  if (function_exists('squad_reserve_count')) {
    $targetBench = min(MANAGER_MAX_BENCH_PLAYERS, squad_reserve_count((string)($user['teamId'] ?? ''), (string)($user['teamName'] ?? '')));
  }
  $missing = max(0, $targetBench - count($realBench));
  $loans = array_slice($loans, 0, $missing);
  while (count($loans) < $missing) $loans[] = manager_emergency_loan_player($user, count($loans));
  $user['squad']['bench'] = array_values(array_merge($realBench, $loans));
}

function manager_remove_emergency_loan_from_bench(array &$user, int $count = 1): int {
  if ($count <= 0 || empty($user['squad']['bench']) || !is_array($user['squad']['bench'])) return 0;
  $removed = 0;
  for ($i = count($user['squad']['bench']) - 1; $i >= 0 && $removed < $count; $i--) {
    if (!manager_is_emergency_loan_player(is_array($user['squad']['bench'][$i] ?? null) ? $user['squad']['bench'][$i] : [])) continue;
    array_splice($user['squad']['bench'], $i, 1);
    $removed++;
  }
  return $removed;
}

function manager_prepare_bench_for_real_receive(array &$user, int $count = 1): void {
  manager_remove_emergency_loan_from_bench($user, $count);
}

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']);
          ensure_squad_age_profiles($participant['squad'], (string)($participant['user'] ?? ''), team_source_key($team), (string)($team['name'] ?? ''), (string)($participant['divisionName'] ?? $team['divisionName'] ?? ''));
          apply_squad_nationalities($participant['squad'], team_source_key($team), (string)($team['name'] ?? ''));
          $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'];
  $ownerId = clean_name((string)($user['accountName'] ?? ''));
  $teamName = (string)($user['teamName'] ?? '');
  $divisionName = manager_division_name(clamp_int((int)($user['managerDivisionLevel'] ?? 5), 1, 5));
  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');
	    normalize_squad_left_right($user['squad']);
	    normalize_squad_starter_slots($user);
	    ensure_squad_stats($user['squad']);
	    ensure_squad_age_profiles($user['squad'], $ownerId, $teamId, $teamName, $divisionName);
	    apply_squad_nationalities($user['squad'], $teamId, $teamName);
	    if ((int)($user['squadNameVersion'] ?? 0) < SQUAD_NAME_VERSION) {
	      apply_legacy_names_to_squad($user['squad'], $teamId, (string)($user['teamName'] ?? ''));
	      apply_squad_nationalities($user['squad'], $teamId, $teamName);
	      $user['squadNameVersion'] = SQUAD_NAME_VERSION;
    }
    $user['squadIdVersion'] = SQUAD_ID_VERSION;
    $user['manager']['formation'] = manager_user_formation($user);
    manager_sync_emergency_loan_bench($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;
	  normalize_squad_left_right($user['squad']);
	  normalize_squad_starter_slots($user);
	  ensure_squad_stats($user['squad']);
	  ensure_squad_age_profiles($user['squad'], $ownerId, $teamId, $teamName, $divisionName);
	  apply_squad_nationalities($user['squad'], $teamId, $teamName);
	  manager_sync_emergency_loan_bench($user);
  $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 manager_item_mentions_user($item, string $user): bool {
  if (!is_array($item)) return false;
  foreach (['user', 'owner', 'savedBy', 'buyer', 'seller', 'fromUser', 'toUser', 'home', 'away', 'winner', 'runnerUp'] as $key) {
    if (clean_name((string)($item[$key] ?? '')) === $user) return true;
  }
  foreach (['users', 'humanManagers', 'participants'] as $key) {
    foreach (($item[$key] ?? []) as $name) {
      if (clean_name((string)$name) === $user) return true;
    }
  }
  return false;
}

function admin_delete_user_from_state(array &$state, string $target): array {
  $target = clean_name($target);
  if ($target === '' || !isset($state['users'][$target])) reply(['ok' => false, 'error' => 'bad_user'], 404);

  $targetTrophyKey = has_team($state['users'][$target]) ? team_trophy_key(user_team($state['users'][$target])) : '';
  reset_user_manager_transfer_state($state, $target);
  unset($state['users'][$target], $state['managerCurrentByUser'][$target]);

  foreach (($state['matches'] ?? []) as $matchId => $match) {
    if (!is_array($match)) continue;
    $players = is_array($match['players'] ?? null) ? $match['players'] : [];
    $managerFixture = is_array($match['managerFixture'] ?? null) ? $match['managerFixture'] : [];
    if (
      clean_name((string)($players['home'] ?? '')) === $target
      || clean_name((string)($players['away'] ?? '')) === $target
      || clean_name((string)($managerFixture['home'] ?? '')) === $target
      || clean_name((string)($managerFixture['away'] ?? '')) === $target
    ) {
      unset($state['matches'][$matchId]);
    }
  }

  $removedSeasonIds = [];
  foreach (($state['managerSeasons'] ?? []) as $seasonId => $season) {
    if (manager_item_mentions_user($season, $target)) {
      unset($state['managerSeasons'][$seasonId]);
      $removedSeasonIds[(string)$seasonId] = true;
    }
  }
  if (isset($removedSeasonIds[(string)($state['managerCurrentId'] ?? '')])) $state['managerCurrentId'] = null;
  foreach (($state['managerCurrentByUser'] ?? []) as $name => $seasonId) {
    if (isset($removedSeasonIds[(string)$seasonId])) unset($state['managerCurrentByUser'][$name]);
  }

  foreach (($state['managerInvites'] ?? []) as $inviteId => $invite) {
    if (manager_item_mentions_user($invite, $target) || clean_name((string)($invite['creator'] ?? '')) === $target) unset($state['managerInvites'][$inviteId]);
  }
  if ($targetTrophyKey !== '' && isset($state['teamTrophies'][$targetTrophyKey])) unset($state['teamTrophies'][$targetTrophyKey]);
  if (!empty($state['teamTrophies']) && is_array($state['teamTrophies'])) {
    foreach ($state['teamTrophies'] as $key => $items) {
      if (!is_array($items)) continue;
      $state['teamTrophies'][$key] = array_values(array_filter($items, fn($item) => !manager_item_mentions_user($item, $target)));
      if (!$state['teamTrophies'][$key]) unset($state['teamTrophies'][$key]);
    }
  }
  if (!empty($state['managerBrowseSold']) && is_array($state['managerBrowseSold'])) {
    $state['managerBrowseSold'] = array_values(array_filter($state['managerBrowseSold'], fn($item) => !manager_item_mentions_user($item, $target)));
  }
  if (!empty($state['managerPrivateChats']) && is_array($state['managerPrivateChats'])) {
    foreach ($state['managerPrivateChats'] as $key => $thread) {
      if (manager_item_mentions_user($thread, $target)) unset($state['managerPrivateChats'][$key]);
    }
  }

  $deletedSaves = manager_delete_saves_for_user($target);
  return ['deletedSaves' => $deletedSaves, 'removedSeasonIds' => array_keys($removedSeasonIds)];
}

function reset_manager_player_for_new_league(array &$player): void {
  foreach ([
    'goals',
    'assists',
    'cards',
    'yellows',
    'reds',
    'secondYellowReds',
    'directReds',
    'matchCards',
    'sentOff',
    'marketListedByUser',
    'marketListedAt',
    'marketSalePrice',
    'marketPayoutSettled',
    'marketManagerGameId',
    'marketHumanManagers',
    'marketSalePending',
    'purchasedFrom',
    'purchasedFromId',
    'purchasedFromDivision',
    'purchasePrice',
    'purchasedAt',
    'transferInfo',
    'transferOfferId',
    'scoutFoundAt',
    'scoutSource',
  ] as $key) {
    unset($player[$key]);
  }
}

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

function clear_manager_table_debug_overrides(): void {
  $root = defined('YSP_RUNTIME_ROOT') ? YSP_RUNTIME_ROOT : dirname(__DIR__, 2);
  $file = $root . '/manager_table_debug.json';
  if (is_file($file)) @unlink($file);
}

function reset_active_manager_game(array &$state, bool $preserveCurrentSquads = false): void {
  clear_manager_table_debug_overrides();
  $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) {
    $formation = (string)($state['users'][$name]['manager']['formation'] ?? '');
    if ($preserveCurrentSquads && !empty($state['users'][$name]['squad']) && is_array($state['users'][$name]['squad'])) {
      reset_squad_for_new_league($state['users'][$name]['squad']);
      unset(
        $state['users'][$name]['managerFinance'],
        $state['users'][$name]['managerDivisionLevel']
      );
      $state['users'][$name]['manager'] = [];
      if ($formation !== '') $state['users'][$name]['manager']['formation'] = $formation;
    } else {
      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])));
	    $teamsByUser = manager_invite_team_choices($state, $invite);
	    $out[] = [
	      'id' => (string)$id,
	      'from' => $from,
	      'users' => $users,
	      'accepted' => $accepted,
	      'teamsByUser' => $teamsByUser,
	      '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_pending_sent_invite(array $state, string $user): ?array {
  $user = clean_name($user);
  if ($user === '') return null;
  foreach (($state['managerInvites'] ?? []) as $id => $invite) {
    if (($invite['status'] ?? 'pending') !== 'pending') continue;
    if (clean_name((string)($invite['from'] ?? '')) !== $user) continue;
    $users = array_values(array_map(fn($name) => clean_name((string)$name), is_array($invite['users'] ?? null) ? $invite['users'] : []));
    $accepted = is_array($invite['accepted'] ?? null) ? $invite['accepted'] : [];
    $waiting = array_values(array_filter($users, fn($name) => $name !== '' && empty($accepted[$name])));
    if (!$waiting) continue;
    return [
      'id' => (string)$id,
      'waiting' => $waiting,
      'users' => $users,
    ];
  }
  return null;
}

function manager_invite_pending_payload(array $state, string $user, ?array $season = null): array {
  $pending = manager_pending_sent_invite($state, $user);
  return [
    'ok' => false,
    'error' => 'invite_pending',
    'inviteWaiting' => $pending['waiting'] ?? [],
    'managerInvites' => public_manager_invites($state, $user),
    'managerSeason' => $season ? public_manager_season($season, $state, $user) : null,
    'managerFinance' => isset($state['users'][$user]) ? public_finance($state['users'][$user]) : null,
    'managerSquad' => $season ? public_manager_context_squad($state, $user, $season) : null,
    'managerMarket' => $season ? public_manager_market($state, $season, $user) : [],
    'managerTransferOffers' => public_manager_transfer_offers($state, $user),
    'managerCanContinue' => manager_user_can_continue($state, $user),
    'managerHasAnySave' => manager_user_has_any_save($user),
  ];
}
