<?php
declare(strict_types=1);

$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()]);
  }

  if ($action === 'clientDebugLog') {
    $debugLog = client_debug_log($data);
    $stored = 0;
    if (!empty($debugLog['entries']) && is_array($debugLog['entries'])) {
      $storedResult = with_state(function (&$state) use ($debugLog) {
        if (!isset($state['clientDebugLog']) || !is_array($state['clientDebugLog'])) $state['clientDebugLog'] = [];
        foreach ($debugLog['entries'] as $entry) {
          if (is_array($entry)) $state['clientDebugLog'][] = $entry;
        }
        if (count($state['clientDebugLog']) > 900) $state['clientDebugLog'] = array_slice($state['clientDebugLog'], -900);
        return ['stored' => count($debugLog['entries'])];
      });
      $stored = (int)($storedResult['stored'] ?? 0);
    }
    unset($debugLog['entries']);
    reply(['ok' => true, 'debugLog' => $debugLog + ['stored' => $stored]]);
  }

  if ($action === 'managerStatus') {
    $status = with_state_read(function ($state) {
      $user = require_user($state);
      return public_manager_status($state, $user);
    });
    reply(['ok' => true] + $status);
  }

  $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),
        'accountName' => $name,
        '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 (!admin_user_can_manage($state, $user)) reply(['ok' => false, 'error' => 'forbidden'], 403);
      reset_active_manager_game($state, $action === 'resetManagerLeague');
      return [
        'user' => public_user($user, $state['users'][$user], $state),
        'teams' => public_teams($state, $user),
        'match' => null,
        'managerInvites' => [],
      ];
    }

    if ($action === 'resetEverything') {
      $user = require_user($state);
      if (!admin_user_can_manage($state, $user)) reply(['ok' => false, 'error' => 'forbidden'], 403);
      clear_manager_table_debug_overrides();
      if (function_exists('ysp_db_delete_user')) {
        foreach (array_keys($state['users'] ?? []) as $existingUser) ysp_db_delete_user((string)$existingUser);
      }
      $state = default_state();
      $state['managerInvites'] = [];
      $state['managerMarket'] = [];
      $state['managerBrowseOffers'] = [];
      $state['managerTransferOffers'] = [];
      $state['managerTransferNotices'] = [];
      $state['managerBrowseSold'] = [];
      $deletedManagerSaves = manager_delete_all_saves();
      unset($_SESSION['user'], $_SESSION['csrfToken']);
      return [
        'user' => null,
        'teams' => [],
        'match' => null,
        'managerInvites' => [],
        'managerSaves' => [],
        'managerHasAnySave' => false,
        'loggedOut' => true,
        'deletedManagerSaves' => $deletedManagerSaves,
      ];
    }

    if ($action === 'adminUsers') {
      $user = require_user($state);
      if (!admin_user_can_manage($state, $user)) reply(['ok' => false, 'error' => 'forbidden'], 403);
      return ['adminUsers' => public_admin_users($state)];
    }

    if ($action === 'managerRegisteredUsers') {
      $user = require_user($state);
      return [
        'managerUsers' => public_manager_users($state),
        'managerPrivateChatAlerts' => public_manager_private_chat_alerts($state, $user),
      ];
    }

    if ($action === 'adminDeleteUser') {
      $user = require_user($state);
      if (!admin_user_can_manage($state, $user)) reply(['ok' => false, 'error' => 'forbidden'], 403);
      $target = clean_name((string)($data['user'] ?? ''));
      $selfDelete = $target === $user;
      $deleted = admin_delete_user_from_state($state, $target);
      if (function_exists('ysp_db_delete_user')) ysp_db_delete_user($target);
      if ($selfDelete) {
        unset($_SESSION['user'], $_SESSION['csrfToken']);
        return [
          'user' => null,
          'teams' => [],
          'adminUsers' => [],
          'managerUsers' => public_manager_users($state),
          'match' => null,
          'managerInvites' => [],
          'managerHasSeason' => false,
          'managerCanContinue' => false,
          'managerHasAnySave' => false,
          'managerPrivateChatAlerts' => [],
          'loggedOut' => true,
        ] + $deleted;
      }
      return [
        'user' => public_user($user, $state['users'][$user], $state),
        'teams' => public_teams($state, $user),
        'adminUsers' => public_admin_users($state),
        'managerUsers' => public_manager_users($state),
        'match' => null,
        'managerInvites' => public_manager_invites($state, $user),
        'managerHasSeason' => current_manager_season($state, $user) !== null,
        'managerCanContinue' => manager_user_can_continue($state, $user),
        'managerHasAnySave' => manager_user_has_any_save($user),
        'managerPrivateChatAlerts' => public_manager_private_chat_alerts($state, $user),
      ] + $deleted;
    }

    $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, $state);
      return ['managerSaves' => $managerSaves, 'managerHasAnySave' => count($managerSaves) > 0, 'managerTurnAlerts' => manager_turn_alerts($state, $user), 'managerPrivateChatAlerts' => public_manager_private_chat_alerts($state, $user)];
    }

    if ($action === 'deleteManagerSave') {
      $user = require_user($state);
      $saveId = (string)($data['saveId'] ?? '');
      $save = read_manager_save($saveId);
      if (!manager_save_can_delete($save, $user, $state)) reply(['ok' => false, 'error' => 'forbidden'], 403);
      $seasonId = (string)($save['meta']['seasonId'] ?? '');
      $savePath = manager_save_path($saveId);
      manager_backup_save_file($savePath, 'delete-manual');
      if (@unlink($savePath)) {
        if (function_exists('ysp_db_delete_manager_save_meta')) ysp_db_delete_manager_save_meta($saveId);
        manager_save_index_invalidate();
      }
      @unlink(manager_save_sidecar_path($saveId));
      manager_remove_active_season_if_unsaved($state, $seasonId);
      $managerSaves = public_manager_saves($user, $state);
      $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),
        'managerTurnAlerts' => manager_turn_alerts($state, $user),
        'managerPrivateChatAlerts' => public_manager_private_chat_alerts($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, $user);
      $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);
      if (is_array($season)) {
        $seasonId = (string)($season['id'] ?? '');
        if ($seasonId !== '' && isset($state['managerSeasons'][$seasonId])) {
          $repairedContinues = manager_auto_mark_played_round_continues($state['managerSeasons'][$seasonId], $now);
          if ($repairedContinues > 0) {
            $state['managerSeasons'][$seasonId]['updated'] = $now;
            manager_log($state, 'auto_round_continue_repair', ['seasonId' => $seasonId, 'user' => $user, 'count' => $repairedContinues]);
            $season = $state['managerSeasons'][$seasonId];
          }
        }
      }
      if (is_array($season) && ($seasonId ?? '') !== '' && isset($state['managerSeasons'][$seasonId])) {
        $season =& $state['managerSeasons'][$seasonId];
        if (($season['status'] ?? '') === 'finished') apply_manager_progression($state, $season);
        manager_prepare_injuries_for_current_round($state, $season, $now);
        unset($season);
        $season = current_manager_season($state, $user);
      }
      manager_generate_cpu_market_offers($state, $season, $now, $user);
      return [
        'user' => public_user($user, $state['users'][$user], $state),
        'managerHasSeason' => is_array($season),
        'managerInvites' => public_manager_invites($state, $user),
        'managerSaveUsers' => manager_current_save_users($state, $user),
        'managerCanContinue' => manager_user_can_continue($state, $user),
        'managerHasAnySave' => manager_user_has_any_save($user),
        'managerTurnAlerts' => manager_turn_alerts($state, $user),
        'managerPrivateChatAlerts' => public_manager_private_chat_alerts($state, $user),
        'loadedSave' => $meta,
      ];
    }

    if ($action === 'managerSwitchTurnAlert') {
      $user = require_user($state);
      $seasonId = (string)($data['seasonId'] ?? '');
      $gameId = (string)($data['managerGameId'] ?? '');
      $targetId = '';
      foreach (manager_turn_alerts($state, $user) as $alert) {
        if (($seasonId !== '' && (string)($alert['seasonId'] ?? '') === $seasonId) || ($gameId !== '' && (string)($alert['managerGameId'] ?? '') === $gameId)) {
          $targetId = (string)($alert['seasonId'] ?? '');
          break;
        }
      }
      if ($targetId === '' && $seasonId !== '' && is_array($state['managerSeasons'][$seasonId] ?? null) && manager_season_visible_to_user($state['managerSeasons'][$seasonId], $user)) {
        $targetId = $seasonId;
      }
      if ($targetId === '' || !is_array($state['managerSeasons'][$targetId] ?? null)) reply(['ok' => false, 'error' => 'turn_save_not_active'], 404);
      manager_store_current_user_snapshot($state, $user);
      if (!isset($state['managerCurrentByUser']) || !is_array($state['managerCurrentByUser'])) $state['managerCurrentByUser'] = [];
      $state['managerCurrentByUser'][$user] = $targetId;
      $state['managerCurrentId'] = $targetId;
      $season =& $state['managerSeasons'][$targetId];
      manager_restore_user_from_season_snapshot($state, $season, $user);
      ensure_user_squad($state['users'][$user]);
      ensure_manager_finance($state['users'][$user]);
      ensure_manager_market($state);
      if (($season['status'] ?? '') === 'finished') apply_manager_progression($state, $season);
      $repairedContinues = manager_auto_mark_played_round_continues($season, $now);
      if ($repairedContinues > 0) {
        $season['updated'] = $now;
        manager_log($state, 'auto_round_continue_repair', ['seasonId' => (string)($season['id'] ?? $targetId), 'user' => $user, 'count' => $repairedContinues]);
      }
      manager_prepare_injuries_for_current_round($state, $season, $now);
      if (manager_participant($season, $user) !== null) {
        manager_staff_auto_lineup_for_user($state, $season, $user, manager_pending_round_index($season) ?? -1, $now);
        $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);
      manager_log($state, 'switch_turn_alert', ['user' => $user, 'seasonId' => $targetId, 'managerGameId' => manager_game_id($season)]);
      return [
        'user' => public_user($user, $state['users'][$user], $state),
        'teams' => public_teams($state, $user),
        '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, $season),
        'managerMarket' => public_manager_market($state, $season, $user),
        'managerDivisions' => public_manager_divisions($state, $season, $user),
        'managerTransferOffers' => public_manager_transfer_offers($state, $user),
        'managerTurnAlerts' => manager_turn_alerts($state, $user),
        'managerPrivateChatAlerts' => public_manager_private_chat_alerts($state, $user),
      ];
    }

	    if ($action === 'managerNewGameSetup') {
	      $user = require_user($state);
	      return [
	        'teams' => public_teams($state, $user),
	        'managerUsers' => public_manager_users($state),
	        'managerInvites' => public_manager_invites($state, $user),
	        'managerHasSeason' => current_manager_season($state, $user) !== null,
	        'managerSaveUsers' => manager_current_save_users($state, $user),
	        'managerCanContinue' => manager_user_can_continue($state, $user),
	        'managerHasAnySave' => manager_user_has_any_save($user),
        'managerTurnAlerts' => manager_turn_alerts($state, $user),
        'managerPrivateChatAlerts' => public_manager_private_chat_alerts($state, $user),
      ];
    }

    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') apply_manager_progression($state, $season);
        $repairedContinues = manager_auto_mark_played_round_continues($season, $now);
        if ($repairedContinues > 0) {
          $season['updated'] = $now;
          manager_log($state, 'auto_round_continue_repair', ['seasonId' => (string)($season['id'] ?? $seasonId), 'user' => $user, 'count' => $repairedContinues]);
        }
        manager_prepare_injuries_for_current_round($state, $season, $now);
        if (manager_participant($season, $user) !== null) {
          manager_staff_auto_lineup_for_user($state, $season, $user, manager_pending_round_index($season) ?? -1, $now);
          $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);
	      $status = public_manager_status($state, $user);
	      return [
	        'managerRevision' => (string)($status['managerRevision'] ?? ''),
	        'teams' => !empty($status['managerInvites']) ? public_teams($state, $user) : [],
	        'managerUsers' => $status['managerUsers'] ?? [],
	        'managerSeason' => public_manager_season($season, $state, $user),
        'managerInvites' => $status['managerInvites'] ?? [],
        'managerSaveUsers' => $status['managerSaveUsers'] ?? [],
        'managerCanContinue' => $status['managerCanContinue'] ?? manager_user_can_continue($state, $user),
        'managerHasAnySave' => $status['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, $user),
        'managerDivisions' => public_manager_divisions($state, $season, $user),
        'managerTransferOffers' => public_manager_transfer_offers($state, $user),
        'managerTurnAlerts' => $status['managerTurnAlerts'] ?? [],
        'managerPrivateChatAlerts' => $status['managerPrivateChatAlerts'] ?? [],
      ];
    }

    if ($action === 'managerChatPost') {
      $user = require_user($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 (!in_array($user, manager_chat_participants($state, $season), true)) reply(['ok' => false, 'error' => 'not_your_season'], 403);
      $text = manager_chat_text((string)($data['text'] ?? ''));
      if ($text === '') reply(['ok' => false, 'error' => 'empty_message'], 400);
      $preview = manager_side_preview($state, $season, $user);
      $entry = [
        'id' => 'chat' . bin2hex(random_bytes(6)),
        'user' => $user,
        'team' => $preview['team'] ?? ($state['users'][$user]['team'] ?? null),
        'text' => $text,
        'sentAt' => $now,
      ];
      manager_chat_store_message($state, $season, $entry, $now);
      manager_log($state, 'manager_chat_post', ['seasonId' => (string)($season['id'] ?? $seasonId), 'chatKey' => manager_chat_key($state, $season), 'user' => $user]);
      return [
        'managerSeason' => public_manager_season($season, $state, $user),
        'chatMessages' => public_manager_chat_messages($season, $user, 80, $state),
        'managerPrivateChatAlerts' => public_manager_private_chat_alerts($state, $user),
      ];
    }

    if ($action === 'managerChatUpload') {
      $user = require_user($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 (!in_array($user, manager_chat_participants($state, $season), true)) reply(['ok' => false, 'error' => 'not_your_season'], 403);
      $image = manager_chat_decode_image($data, $now);
      $caption = manager_chat_text((string)($data['text'] ?? ''));
      $preview = manager_side_preview($state, $season, $user);
      $entry = [
        'id' => 'chat' . bin2hex(random_bytes(6)),
        'user' => $user,
        'team' => $preview['team'] ?? ($state['users'][$user]['team'] ?? null),
        'text' => $caption,
        'image' => $image,
        'sentAt' => $now,
      ];
      manager_chat_store_message($state, $season, $entry, $now);
      manager_log($state, 'manager_chat_upload', ['seasonId' => (string)($season['id'] ?? $seasonId), 'chatKey' => manager_chat_key($state, $season), 'user' => $user, 'file' => $image['url'] ?? '']);
      return [
        'managerSeason' => public_manager_season($season, $state, $user),
        'chatMessages' => public_manager_chat_messages($season, $user, 80, $state),
        'managerPrivateChatAlerts' => public_manager_private_chat_alerts($state, $user),
      ];
    }

    if ($action === 'managerPrivateChatOpen') {
      $user = require_user($state);
      $target = manager_private_chat_target($state, $user, (string)($data['target'] ?? ''));
      manager_private_chat_mark_seen($state, $user, $target, $now);
      return [
        'privateChatTarget' => $target,
        'privateChatMessages' => manager_private_chat_messages_for($state, $user, $target, 80),
        'managerPrivateChatAlerts' => public_manager_private_chat_alerts($state, $user),
        'managerUsers' => public_manager_users($state),
      ];
    }

    if ($action === 'managerPrivateChatPost') {
      $user = require_user($state);
      $target = manager_private_chat_target($state, $user, (string)($data['target'] ?? ''));
      $text = manager_chat_text((string)($data['text'] ?? ''));
      if ($text === '') reply(['ok' => false, 'error' => 'empty_message'], 400);
      $entry = [
        'id' => 'pm' . bin2hex(random_bytes(6)),
        'user' => $user,
        'team' => has_team($state['users'][$user]) ? user_team($state['users'][$user]) : null,
        'text' => $text,
        'sentAt' => $now,
      ];
      manager_private_chat_store_message($state, $user, $target, $entry, $now);
      manager_log($state, 'manager_private_chat_post', ['user' => $user, 'target' => $target]);
      return [
        'privateChatTarget' => $target,
        'privateChatMessages' => manager_private_chat_messages_for($state, $user, $target, 80),
        'managerPrivateChatAlerts' => public_manager_private_chat_alerts($state, $user),
      ];
    }

    if ($action === 'managerPrivateChatUpload') {
      $user = require_user($state);
      $target = manager_private_chat_target($state, $user, (string)($data['target'] ?? ''));
      $image = manager_chat_decode_image($data, $now);
      $caption = manager_chat_text((string)($data['text'] ?? ''));
      $entry = [
        'id' => 'pm' . bin2hex(random_bytes(6)),
        'user' => $user,
        'team' => has_team($state['users'][$user]) ? user_team($state['users'][$user]) : null,
        'text' => $caption,
        'image' => $image,
        'sentAt' => $now,
      ];
      manager_private_chat_store_message($state, $user, $target, $entry, $now);
      manager_log($state, 'manager_private_chat_upload', ['user' => $user, 'target' => $target, 'file' => $image['url'] ?? '']);
      return [
        'privateChatTarget' => $target,
        'privateChatMessages' => manager_private_chat_messages_for($state, $user, $target, 80),
        'managerPrivateChatAlerts' => public_manager_private_chat_alerts($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 === 'managerUpgradeMerch') {
	      $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]);
      $level = clamp_int((int)($state['users'][$user]['managerDivisionLevel'] ?? 5), 1, 5);
      $finance =& $state['users'][$user]['managerFinance'];
      $option = manager_merch_upgrade_option($finance, $level);
      if (!$option) reply(['ok' => false, 'error' => 'merch_max'], 409);
      if ((int)($finance['balance'] ?? 0) < (int)$option['cost']) reply(['ok' => false, 'error' => 'no_money'], 409);
      $finance['balance'] = (int)($finance['balance'] ?? 0) - (int)$option['cost'];
      $finance['merchLevel'] = (int)$option['level'];
      $finance['merchSpent'] = (int)($finance['merchSpent'] ?? 0) + (int)$option['cost'];
      manager_log($state, 'merch_upgrade', ['seasonId' => (string)($season['id'] ?? ''), 'user' => $user, 'level' => (int)$option['level'], 'cost' => (int)$option['cost'], 'boostPct' => (int)$option['boostPct']]);
      unset($finance);
      return [
	        'managerSeason' => public_manager_season(current_manager_season($state, $user), $state, $user),
	        'managerFinance' => public_finance($state['users'][$user]),
	      ];
	    }

	    if ($action === 'managerTakeLoan') {
	      $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['loan'] ?? null) && (int)($finance['loan']['remaining'] ?? 0) > 0) reply(['ok' => false, 'error' => 'active_loan'], 409);
	      $level = clamp_int((int)($state['users'][$user]['managerDivisionLevel'] ?? 5), 1, 5);
	      $loanInfo = manager_public_loan($finance, $level);
	      $step = max(1, (int)($loanInfo['step'] ?? 50000));
	      $amount = (int)round(max(0, (float)($data['amount'] ?? 0)) / $step) * $step;
	      $years = clamp_int((int)($data['years'] ?? 1), 1, 5);
	      $maxAmount = (int)($loanInfo['maxAmount'] ?? 0);
	      if ($amount < (int)($loanInfo['minAmount'] ?? $step)) reply(['ok' => false, 'error' => 'bad_amount'], 400);
	      if ($amount > $maxAmount) reply(['ok' => false, 'error' => 'loan_too_high', 'maxAmount' => $maxAmount], 409);
	      $ratePct = manager_loan_rate_pct($years);
	      $interest = manager_round_money((int)round($amount * ($ratePct / 100)), 5000);
	      $totalDue = $amount + $interest;
	      $paymentsTotal = max(1, $years * manager_loan_payments_per_year());
	      $paymentPerMatch = max(500, manager_round_money((int)ceil($totalDue / $paymentsTotal), 500));
	      $finance['balance'] = (int)($finance['balance'] ?? 0) + $amount;
	      $finance['loanBorrowed'] = (int)($finance['loanBorrowed'] ?? 0) + $amount;
	      $finance['loan'] = [
	        'principal' => $amount,
	        'principalRemaining' => $amount,
	        'cashBorrowed' => $amount,
	        'interestTotal' => $interest,
	        'interestRemaining' => $interest,
	        'totalDue' => $totalDue,
	        'remaining' => $totalDue,
	        'paidTotal' => 0,
	        'termYears' => $years,
	        'ratePct' => $ratePct,
	        'paymentsTotal' => $paymentsTotal,
	        'paymentsLeft' => $paymentsTotal,
	        'paymentPerMatch' => $paymentPerMatch,
	        'startedAt' => $now,
	      ];
	      $seasonId = current_manager_season_id($state, $user);
	      if ($seasonId !== null && isset($state['managerSeasons'][$seasonId])) $state['managerSeasons'][$seasonId]['updated'] = $now;
	      manager_log($state, 'loan_taken', ['seasonId' => (string)($season['id'] ?? $seasonId ?? ''), 'user' => $user, 'amount' => $amount, 'years' => $years, 'ratePct' => $ratePct, 'totalDue' => $totalDue, 'paymentPerMatch' => $paymentPerMatch]);
	      unset($finance);
	      return [
	        'managerSeason' => public_manager_season(current_manager_season($state, $user), $state, $user),
	        'managerFinance' => public_finance($state['users'][$user]),
	      ];
	    }

	    if ($action === 'hireStaffManager') {
	      $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);
	      if (!has_team($state['users'][$user])) reply(['ok' => false, 'error' => 'no_team'], 409);
	      ensure_user_squad($state['users'][$user]);
	      ensure_manager_finance($state['users'][$user]);
	      $option = manager_staff_manager_option((string)($data['managerId'] ?? ''));
	      if (!$option) reply(['ok' => false, 'error' => 'bad_manager'], 400);
	      $finance =& $state['users'][$user]['managerFinance'];
	      $current = manager_public_hired_manager($finance);
	      $currentBoost = (float)($current['boostPct'] ?? 0);
	      if ($currentBoost >= (float)$option['boostPct']) reply(['ok' => false, 'error' => 'already_hired'], 409);
	      $currentLevel = manager_staff_manager_level($current);
	      $optionLevel = (int)($option['level'] ?? 0);
	      if ($optionLevel !== $currentLevel + 1) reply(['ok' => false, 'error' => 'locked_step', 'requiredLevel' => $currentLevel + 1], 409);
	      $price = (int)$option['cost'];
	      if ((int)($finance['balance'] ?? 0) < $price) reply(['ok' => false, 'error' => 'no_money'], 409);

	      $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 = manager_unavailable_player_ids($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);
	      }
	      $formation = manager_user_formation($state['users'][$user]);
	      if (!apply_squad_formation($state['users'][$user], $formation, $dailyForm, $injuredPlayerIds)) reply(['ok' => false, 'error' => 'bad_formation'], 400);

	      $finance['balance'] = (int)($finance['balance'] ?? 0) - $price;
	      $finance['staffSpent'] = (int)($finance['staffSpent'] ?? 0) + $price;
	      $finance['hiredManager'] = [
	        'id' => (string)$option['id'],
	        'name' => (string)$option['name'],
	        'boostPct' => (float)$option['boostPct'],
	        'cost' => (int)$option['cost'],
	        'hiredAt' => $now,
	      ];
	      unset($finance);
	      manager_log($state, 'hire_staff_manager', ['user' => $user, 'managerId' => (string)$option['id'], 'boostPct' => (float)$option['boostPct'], 'price' => $price]);
	      return [
	        'status' => 'hired',
	        'price' => $price,
	        'hiredManager' => $option,
	        '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 === 'hireScout') {
	      $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);
	      if (!has_team($state['users'][$user])) reply(['ok' => false, 'error' => 'no_team'], 409);
	      ensure_manager_finance($state['users'][$user]);
	      $option = manager_scout_option((string)($data['scoutId'] ?? ''));
	      if (!$option) reply(['ok' => false, 'error' => 'bad_scout'], 400);
	      $finance =& $state['users'][$user]['managerFinance'];
	      $current = manager_public_hired_scout($finance);
	      $currentLevel = (int)($current['level'] ?? 0);
	      $currentCost = (int)($current['cost'] ?? 0);
	      if ($currentLevel >= (int)$option['level']) reply(['ok' => false, 'error' => 'already_hired'], 409);
	      $price = max(0, (int)$option['cost'] - $currentCost);
	      if ((int)($finance['balance'] ?? 0) < $price) reply(['ok' => false, 'error' => 'no_money'], 409);
	      $finance['balance'] = (int)($finance['balance'] ?? 0) - $price;
	      $finance['staffSpent'] = (int)($finance['staffSpent'] ?? 0) + $price;
	      $finance['hiredScout'] = [
	        'id' => (string)$option['id'],
	        'name' => (string)$option['name'],
	        'level' => (int)$option['level'],
	        'cost' => (int)$option['cost'],
	        'feePct' => (float)$option['feePct'],
	        'canBrowse' => !empty($option['canBrowse']),
	        'canAutoBuy' => !empty($option['canAutoBuy']),
	        'hiredAt' => $now,
	      ];
	      $finance['scoutSearch'] = manager_normalize_scout_search(is_array($finance['scoutSearch'] ?? null) ? $finance['scoutSearch'] : manager_scout_search_defaults(), manager_public_hired_scout($finance));
	      unset($finance);
	      manager_log($state, 'hire_scout', ['user' => $user, 'scoutId' => (string)$option['id'], 'level' => (int)$option['level'], 'price' => $price]);
	      return [
	        'status' => 'hired',
	        'price' => $price,
	        'hiredScout' => $option,
	        'managerSeason' => public_manager_season(current_manager_season($state, $user), $state, $user),
	        'managerFinance' => public_finance($state['users'][$user]),
	      ];
	    }

	    if ($action === 'saveScoutSearch') {
	      $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]);
	      $scout = manager_public_hired_scout($state['users'][$user]['managerFinance']);
	      if (!$scout) reply(['ok' => false, 'error' => 'no_scout'], 409);
	      $searchData = is_array($data['search'] ?? null) ? $data['search'] : $data;
	      $search = manager_normalize_scout_search($searchData, $scout);
	      $search['updatedAt'] = $now;
	      $state['users'][$user]['managerFinance']['scoutSearch'] = $search;
	      $autoTraining = [];
	      $seasonId = current_manager_season_id($state, $user);
	      if ($seasonId !== null && isset($state['managerSeasons'][$seasonId]) && is_array($state['managerSeasons'][$seasonId])) {
	        $season =& $state['managerSeasons'][$seasonId];
	        $autoTraining = manager_scout_auto_train_bench($state, $season, $user, $now);
	        unset($season);
	      }
	      return [
	        'status' => 'saved',
	        'managerFinance' => public_finance($state['users'][$user]),
	        'managerSeason' => public_manager_season(current_manager_season($state, $user), $state, $user),
	        'managerSquad' => public_manager_context_squad($state, $user, current_manager_season($state, $user)),
	        'autoTraining' => $autoTraining,
	      ];
	    }

	    if ($action === 'runScout') {
	      $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);
	      if (!has_team($state['users'][$user])) reply(['ok' => false, 'error' => 'no_team'], 409);
	      $seasonId = current_manager_season_id($state, $user);
	      if ($seasonId !== null && isset($state['managerSeasons'][$seasonId]) && is_array($state['managerSeasons'][$seasonId])) {
	        $season =& $state['managerSeasons'][$seasonId];
	      }
	      ensure_user_squad($state['users'][$user]);
	      ensure_manager_finance($state['users'][$user]);
	      $scout = manager_public_hired_scout($state['users'][$user]['managerFinance']);
	      if (!$scout) reply(['ok' => false, 'error' => 'no_scout'], 409);
	      $searchData = is_array($data['search'] ?? null) ? $data['search'] : $data;
	      $search = manager_normalize_scout_search($searchData ?: manager_public_scout_search($state['users'][$user]['managerFinance']), $scout);
	      $search['updatedAt'] = $now;
	      $state['users'][$user]['managerFinance']['scoutSearch'] = $search;
	      $candidates = manager_scout_candidates($state, $season, $user, $search, $scout);
	      $publicResults = array_map(fn($candidate) => manager_public_scout_candidate($candidate, $search), array_slice($candidates, 0, 12));
	      if (!empty($search['autoBuy']) && !empty($scout['canAutoBuy'])) {
	        foreach ($candidates as $candidate) {
	          $price = max(0, (int)($candidate['price'] ?? 0));
	          $fee = manager_scout_fee($price, $scout);
	          if ((int)$state['users'][$user]['managerFinance']['balance'] < $price + $fee) continue;
	          if ((string)($candidate['source'] ?? '') === 'market') {
	            $result = complete_manager_market_purchase($state, $season, $user, (int)($candidate['marketIndex'] ?? -1), is_array($candidate['player'] ?? null) ? $candidate['player'] : [], $price, $now, $fee);
	          } else {
	            $team = ['name' => (string)($candidate['teamName'] ?? 'TRANSFER BROWSE')];
	            $result = complete_manager_browse_purchase($state, $user, (string)($candidate['teamUser'] ?? ''), $team, (string)($candidate['divisionName'] ?? ''), is_array($candidate['player'] ?? null) ? $candidate['player'] : [], $price, $now);
	            $state['users'][$user]['managerFinance']['balance'] -= $fee;
	            $state['users'][$user]['managerFinance']['scoutSpent'] = (int)($state['users'][$user]['managerFinance']['scoutSpent'] ?? 0) + $fee;
	            $result['managerFinance'] = public_finance($state['users'][$user]);
	            $result['managerMarket'] = public_manager_market($state, $season, $user);
	            $result['managerTransferOffers'] = public_manager_transfer_offers($state, $user);
	            $result['scoutFee'] = $fee;
	          }
	          $result['status'] = 'auto_bought';
	          $result['scoutResults'] = $publicResults;
	          $result['scoutSearch'] = manager_public_scout_search($state['users'][$user]['managerFinance']);
	          $result['autoTraining'] = manager_scout_auto_train_bench($state, $season, $user, $now);
	          $result['managerSeason'] = public_manager_season($season, $state, $user);
	          $result['managerSquad'] = public_manager_context_squad($state, $user, $season);
	          manager_log($state, 'scout_auto_buy', ['user' => $user, 'scoutId' => (string)$scout['id'], 'player' => $result['boughtPlayer']['name'] ?? '', 'price' => $price, 'fee' => $fee, 'source' => (string)($candidate['source'] ?? '')]);
	          return $result;
	        }
	      }
	      $autoTraining = manager_scout_auto_train_bench($state, $season, $user, $now);
	      manager_log($state, 'run_scout', ['user' => $user, 'scoutId' => (string)$scout['id'], 'results' => count($candidates), 'autoBuy' => !empty($search['autoBuy'])]);
	      return [
	        'status' => $candidates ? 'found' : 'none',
	        '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, $user),
	        'managerTransferOffers' => public_manager_transfer_offers($state, $user),
	        'scoutResults' => $publicResults,
	        'scoutSearch' => manager_public_scout_search($state['users'][$user]['managerFinance']),
	        'autoTraining' => $autoTraining,
	        'autoBuySkipped' => !empty($search['autoBuy']) && !empty($scout['canAutoBuy']) && $candidates ? 'no_affordable_match' : '',
	      ];
	    }

	    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'] ?? '');
      $targetSeasonId = manager_related_current_season_id_for_target($state, $season, $target);
      if ($targetSeasonId !== null && isset($state['managerSeasons'][$targetSeasonId])) {
        $season =& $state['managerSeasons'][$targetSeasonId];
      }
      if (manager_participant($season, $target) === null && str_starts_with($target, 'cpu:')) {
        return ['teamDetails' => public_manager_division_team_details($target, $state, $season)];
      }
      if (manager_participant($season, $target) !== null) {
        manager_staff_auto_lineup_for_user($state, $season, $target, manager_pending_round_index($season) ?? -1, $now);
        $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);
	      }
	      $teamChoices = manager_team_choices_from_payload($state, $selected, $data['teamsByUser'] ?? []);
	      if (count($teamChoices) !== count($selected)) reply(['ok' => false, 'error' => 'user_without_team'], 409);
	      if ($duplicate = manager_duplicate_team_choices($teamChoices)) {
	        reply(['ok' => false, 'error' => 'duplicate_human_team', 'team' => $duplicate['team'], 'users' => $duplicate['users']], 409);
	      }
	      $currentSeason = current_manager_season($state, $user);
	      if (count($selected) > 1 && $currentSeason !== null && manager_season_has_started($currentSeason)) {
	        reply(['ok' => false, 'error' => 'season_started'], 409);
	      }
	      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],
	          'teamsByUser' => $teamChoices,
	          'divisionLevel' => $divisionLevel,
	          'freshStart' => true,
          'seasonYear' => manager_state_year($state),
          'created' => $now,
        ];
        return [
          'managerInvite' => $state['managerInvites'][$inviteId],
          'managerInvites' => public_manager_invites($state, $user),
          'managerCanContinue' => manager_user_can_continue($state, $user),
          'managerHasAnySave' => manager_user_has_any_save($user),
        ];
	      }
	      if ($freshStart) reset_active_manager_game($state);
	      foreach ($teamChoices as $name => $team) {
	        if (!manager_apply_team_choice_to_user($state, $name, $team, (int)($state['users'][$name]['managerDivisionLevel'] ?? 5))) {
	          reply(['ok' => false, 'error' => 'bad_team'], 400);
	        }
	      }
	      $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 ['user' => public_user($user, $state['users'][$user], $state), 'teams' => public_teams($state, $user), '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);
	      $inviteUsers = array_values(array_unique(array_map(fn($name) => clean_name((string)$name), $invite['users'] ?? [])));
	      $team = manager_team_choice_for_user($state, $user, $data['teamId'] ?? ($data['team'] ?? null));
	      if (!$team) reply(['ok' => false, 'error' => 'user_without_team'], 409);
	      if (!isset($invite['teamsByUser']) || !is_array($invite['teamsByUser'])) $invite['teamsByUser'] = manager_invite_team_choices($state, $invite);
	      $invite['teamsByUser'][$user] = $team;
	      $invite['accepted'][$user] = true;
	      $teamChoices = manager_invite_team_choices($state, $invite);
	      $acceptedUsers = array_values(array_filter($inviteUsers, fn($name) => !empty($invite['accepted'][$name])));
	      if ($duplicate = manager_duplicate_team_choices($teamChoices, $acceptedUsers)) {
	        reply(['ok' => false, 'error' => 'duplicate_human_team', 'team' => $duplicate['team'], 'users' => $duplicate['users']], 409);
	      }
	      if (manager_invite_ready($invite)) {
	        $creator = clean_name((string)($invite['from'] ?? $user));
	        if ($creator === '') $creator = $user;
	        $freshInvite = !empty($invite['freshStart']) || count($inviteUsers) > 1;
	        $seasonYear = $freshInvite ? manager_base_season_year() : (int)($invite['seasonYear'] ?? manager_state_year($state));
	        $divisionLevel = clamp_int((int)($invite['divisionLevel'] ?? 5), 1, 5);
	        foreach ($inviteUsers as $name) {
	          if (empty($teamChoices[$name])) reply(['ok' => false, 'error' => 'user_without_team', 'user' => $name], 409);
	        }
	        if ($duplicate = manager_duplicate_team_choices($teamChoices, $inviteUsers)) {
	          reply(['ok' => false, 'error' => 'duplicate_human_team', 'team' => $duplicate['team'], 'users' => $duplicate['users']], 409);
	        }
	        unset($invite);
	        if ($freshInvite) reset_active_manager_game($state);
	        foreach ($teamChoices as $name => $choiceTeam) {
	          if (!manager_apply_team_choice_to_user($state, $name, $choiceTeam, $divisionLevel)) {
	            reply(['ok' => false, 'error' => 'bad_team', 'user' => $name], 400);
	          }
	        }
	        $season = create_manager_season_array($state, $inviteUsers, $creator, $now, $seasonYear);
        $id = (string)$season['id'];
        $state['managerSeasons'][$id] = $season;
        set_manager_current($state, $id, $season['humanManagers'] ?? $inviteUsers);
        manager_autosave_season($state, $state['managerSeasons'][$id], null, $creator);
        manager_log($state, 'invite_started', ['seasonId' => $id, 'inviteId' => $inviteId, 'user' => $user, 'humans' => $season['humanManagers'] ?? []]);
        return [
	          'user' => public_user($user, $state['users'][$user], $state),
	          'teams' => public_teams($state, $user),
	          '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 === 'managerJobOfferDecision') {
      $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);
      $season =& $state['managerSeasons'][$currentId];
      if (!in_array($user, $season['humanManagers'] ?? [], true)) reply(['ok' => false, 'error' => 'not_your_season'], 403);
      if (($season['status'] ?? '') !== 'finished') reply(['ok' => false, 'error' => 'season_not_finished'], 409);
      apply_manager_progression($state, $season);
      $offerId = (string)($data['offerId'] ?? '');
      $decision = strtolower(trim((string)($data['decision'] ?? '')));
      if ($offerId === '' || !in_array($decision, ['accept', 'decline'], true)) reply(['ok' => false, 'error' => 'bad_offer'], 400);
      if (!isset($season['jobOffers'][$user]) || !is_array($season['jobOffers'][$user])) reply(['ok' => false, 'error' => 'bad_offer'], 404);
      $foundIndex = null;
      foreach ($season['jobOffers'][$user] as $index => $offer) {
        if (is_array($offer) && (string)($offer['id'] ?? '') === $offerId) {
          $foundIndex = $index;
          break;
        }
      }
      if ($foundIndex === null) reply(['ok' => false, 'error' => 'bad_offer'], 404);
      $offer =& $season['jobOffers'][$user][$foundIndex];
      if (($offer['status'] ?? 'pending') !== 'pending') reply(['ok' => false, 'error' => 'offer_closed'], 409);
      if ($decision === 'decline') {
        $offer['status'] = 'rejected';
        $offer['decidedAt'] = $now;
        $season['updated'] = $now;
        manager_log($state, 'job_offer_declined', ['seasonId' => $currentId, 'user' => $user, 'offerId' => $offerId]);
      } else {
        $team = is_array($offer['team'] ?? null) ? $offer['team'] : [];
        $blocked = manager_game_human_team_keys($state, manager_game_id($season), [], $season);
        if (!$team || manager_team_key_blocked($team, $blocked)) reply(['ok' => false, 'error' => 'team_taken'], 409);
        manager_apply_job_offer_to_user($state, $user, $offer);
        $offer['status'] = 'accepted';
        $offer['decidedAt'] = $now;
        foreach ($season['jobOffers'][$user] as $index => &$otherOffer) {
          if ($index === $foundIndex || !is_array($otherOffer)) continue;
          if (($otherOffer['status'] ?? 'pending') === 'pending') {
            $otherOffer['status'] = 'superseded';
            $otherOffer['decidedAt'] = $now;
          }
        }
        unset($otherOffer);
        $season['jobOfferAccepted'][$user] = [
          'offerId' => $offerId,
          'team' => $team,
          'targetLevel' => clamp_int((int)($offer['targetLevel'] ?? 5), 1, 5),
          'acceptedAt' => $now,
        ];
        $season['updated'] = $now;
        manager_log($state, 'job_offer_accepted', ['seasonId' => $currentId, 'user' => $user, 'offerId' => $offerId, 'team' => $team['name'] ?? '', 'division' => $offer['targetDivision'] ?? '']);
      }
      unset($offer);
      manager_autosave_season($state, $season, null, $user);
      return [
        'user' => public_user($user, $state['users'][$user], $state),
        'teams' => public_teams($state, $user),
        '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, $user),
        'managerTransferOffers' => public_manager_transfer_offers($state, $user),
        'managerTurnAlerts' => manager_turn_alerts($state, $user),
        'managerCanContinue' => manager_user_can_continue($state, $user),
        'managerHasAnySave' => manager_user_has_any_save($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 (manager_pending_sent_invite($state, $user)) reply(manager_invite_pending_payload($state, $user, $current), 409);
      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, $user),
          '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, $user),
          '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_continue_target_level($state, $current, $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, $user),
          '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], $user),
        '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 (manager_pending_sent_invite($state, $user)) reply(manager_invite_pending_payload($state, $user, $season), 409);
      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);
      $repairedContinues = manager_auto_mark_played_round_continues($season, $now);
      if ($repairedContinues > 0) {
        $season['updated'] = $now;
        manager_log($state, 'auto_round_continue_repair', ['seasonId' => (string)($season['id'] ?? $id), 'user' => $user, 'count' => $repairedContinues]);
      }
      $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, $user),
          '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, $user),
            'managerTransferOffers' => public_manager_transfer_offers($state, $user),
            'simVotePending' => true,
            'roundWaiting' => $split['waiting'],
          ];
        }
      }
      if (manager_next_round_request_is_stale($season, $roundIndex, $data)) {
        $freshRequest = manager_fresh_next_round_request($season, $user, $roundIndex);
        if ($freshRequest) {
          manager_log($state, 'next_round_request_normalized', [
            'user' => $user,
            'requestedRoundIndex' => $data['roundIndex'] ?? null,
            'requestedFixtureIndex' => $data['fixtureIndex'] ?? null,
            'freshFixtureIndex' => $freshRequest['fixtureIndex'],
          ] + manager_round_log_context($season, $roundIndex, (int)$freshRequest['fixtureIndex']));
          $data = array_merge($data, $freshRequest);
        }
      }
      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,
          'requestedFixtureIndex' => $data['fixtureIndex'] ?? 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);
      manager_staff_auto_lineups_for_round($state, $season, $roundIndex, $now);
      manager_scout_auto_training_lineups_for_round($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)) {
        $finish = manager_finish_cpu_only_pending_rounds($state, $season, $user, $now);
        $currentRoundIndex = manager_pending_round_index($season);
        $split = $currentRoundIndex !== null ? manager_round_ready_waiting($season, $currentRoundIndex) : ['ready' => [], 'waiting' => []];
        $simulated = (int)($finish['simulated'] ?? 0);
        $season['updated'] = $now;
        manager_log($state, 'next_round_wait', ['user' => $user, 'ready' => $split['ready'], 'waiting' => $split['waiting'], 'simulatedCpu' => $simulated, 'cpuSteps' => (int)($finish['steps'] ?? 0), 'blocked' => (string)($finish['blocked'] ?? '')] + manager_round_log_context($season, $roundIndex));
        if ($simulated > 0 || (($finish['finished'] ?? false) === true)) {
          manager_log($state, 'sim_round_cpu_finish', ['user' => $user, 'simulated' => $simulated, 'status' => $season['status'] ?? '', 'steps' => (int)($finish['steps'] ?? 0), 'blocked' => (string)($finish['blocked'] ?? '')] + manager_round_log_context($season, $roundIndex));
          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, $season),
          'managerMarket' => public_manager_market($state, $season, $user),
          '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;
          manager_autosave_season($state, $season, null, $user);
          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, $user),
            'managerTransferOffers' => public_manager_transfer_offers($state, $user),
            'simVotePending' => true,
            'humanMatchReady' => $ready,
            'humanMatchWaiting' => $waiting,
          ];
        }
        unset($season['simVotes'][$voteKey]);
        unset($season['watchVotes'][$watchKey]);
        $playbackSpeed = manager_playback_speed_pct($data['speedPct'] ?? 100);
        simulate_manager_fixture_headless($state, $season, $roundIndex, $fixtureIndex);
        manager_mark_fixture_result_reveal_delay($season, $roundIndex, $fixtureIndex, implode(',', $fixtureHumans), $now);
        manager_set_fixture_playback_speed($season, $roundIndex, $fixtureIndex, $playbackSpeed, $user, $now);
        manager_require_round_continue($season, $roundIndex);
        $autoContinued = manager_auto_mark_played_round_continues($season, $now);
        $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, 'autoContinued' => $autoContinued, '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);
        $roundWaiting = array_values(array_filter($split['waiting'], fn($name) => $name !== $user));
        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, $user),
          'managerTransferOffers' => public_manager_transfer_offers($state, $user),
          'managerFixtureSimulated' => true,
          'roundWaiting' => $roundWaiting,
          'roundContinuePending' => !empty($roundWaiting),
        ];
      }
      $playbackSpeed = manager_playback_speed_pct($data['speedPct'] ?? 100);
      simulate_manager_fixture_headless($state, $season, $roundIndex, $fixtureIndex);
      manager_mark_fixture_result_reveal_delay($season, $roundIndex, $fixtureIndex, $user, $now);
      manager_set_fixture_playback_speed($season, $roundIndex, $fixtureIndex, $playbackSpeed, $user, $now);
      manager_require_round_continue($season, $roundIndex);
      $autoContinued = manager_auto_mark_played_round_continues($season, $now);
      $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, 'autoContinued' => $autoContinued, '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);
      $roundWaiting = array_values(array_filter($split['waiting'], fn($name) => $name !== $user));
      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, $user),
        'managerTransferOffers' => public_manager_transfer_offers($state, $user),
        'managerFixtureSimulated' => true,
        'roundWaiting' => $roundWaiting,
        'roundContinuePending' => !empty($roundWaiting),
      ];
    }

    if ($action === 'managerLivePlaybackSpeed') {
      $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);
      $roundIndex = isset($data['roundIndex']) && is_numeric($data['roundIndex']) ? (int)$data['roundIndex'] : -1;
      $fixtureIndex = isset($data['fixtureIndex']) && is_numeric($data['fixtureIndex']) ? (int)$data['fixtureIndex'] : -1;
      $speedPct = manager_playback_speed_pct($data['speedPct'] ?? 100);
      if ($roundIndex < 0 || $fixtureIndex < 0 || !manager_set_fixture_playback_speed($season, $roundIndex, $fixtureIndex, $speedPct, $user, $now)) {
        reply(['ok' => false, 'error' => 'bad_fixture'], 404);
      }
      $season['updated'] = $now;
      return [
        'speedPct' => $speedPct,
        'managerSeason' => public_manager_season($season, $state, $user),
      ];
    }

    if ($action === 'managerLivePlaybackComplete') {
      $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);
      $roundIndex = isset($data['roundIndex']) && is_numeric($data['roundIndex']) ? (int)$data['roundIndex'] : -1;
      $fixtureIndex = isset($data['fixtureIndex']) && is_numeric($data['fixtureIndex']) ? (int)$data['fixtureIndex'] : -1;
      if ($roundIndex < 0 || $fixtureIndex < 0) {
        $pendingPlayback = manager_user_pending_playback_fixture($season, $user);
        if ($pendingPlayback) {
          $roundIndex = (int)$pendingPlayback['roundIndex'];
          $fixtureIndex = (int)$pendingPlayback['fixtureIndex'];
        }
      }
      $fixture = ($roundIndex >= 0 && $fixtureIndex >= 0) ? ($season['rounds'][$roundIndex]['fixtures'][$fixtureIndex] ?? null) : null;
      if (!is_array($fixture) || ($fixture['status'] ?? '') !== 'played' || (($fixture['home'] ?? '') !== $user && ($fixture['away'] ?? '') !== $user)) {
        reply(['ok' => false, 'error' => 'bad_fixture'], 404);
      }
      manager_mark_fixture_playback_complete($season, $roundIndex, $fixtureIndex, $user, $now);
      if (manager_round_continue_required($season, $roundIndex)) manager_mark_round_continue($season, $user, $roundIndex, $now);
      $autoContinued = manager_auto_mark_played_round_continues($season, $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, 'live_playback_complete', ['user' => $user, 'ready' => $split['ready'], 'waiting' => $split['waiting'], 'autoContinued' => $autoContinued, 'simulatedCpu' => $simulatedCpu] + manager_round_log_context($season, $roundIndex, $fixtureIndex, $fixture));
      if ($split['waiting'] || $simulatedCpu > 0) 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, $season),
        'managerMarket' => public_manager_market($state, $season, $user),
        'managerTransferOffers' => public_manager_transfer_offers($state, $user),
        'managerRoundContinued' => manager_has_round_continue($season, $user, $roundIndex),
        'managerRoundAdvanced' => !$split['waiting'],
        'simVotePending' => !empty($split['waiting']),
        'roundWaiting' => $split['waiting'],
      ];
    }

    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 (manager_pending_sent_invite($state, $user)) reply(manager_invite_pending_payload($state, $user, $season), 409);
      if (($season['status'] ?? '') === 'finished') return ['managerSeason' => public_manager_season($season, $state, $user)];
      $repairedContinues = manager_auto_mark_played_round_continues($season, $now);
      if ($repairedContinues > 0) {
        $season['updated'] = $now;
        manager_log($state, 'auto_round_continue_repair', ['seasonId' => (string)($season['id'] ?? $id), 'user' => $user, 'count' => $repairedContinues]);
      }
      $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, $user),
          '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, $user),
          '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, $user),
          '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));
      if ($split['waiting'] || $simulatedCpu > 0) 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, $season),
        'managerMarket' => public_manager_market($state, $season, $user),
        '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 (manager_pending_sent_invite($state, $user)) reply(manager_invite_pending_payload($state, $user, $season), 409);
      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 = admin_user_can_manage($state, $user) && !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;
          manager_autosave_season($state, $season, null, $user);
          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);
        manager_staff_auto_lineups_for_round($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, $user),
        '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 (manager_pending_sent_invite($state, $user)) reply(manager_invite_pending_payload($state, $user, $season), 409);
      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);
      manager_staff_auto_lineups_for_round($state, $season, (int)$next['roundIndex'], $now);
      manager_scout_auto_training_lineups_for_round($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;
          manager_autosave_season($state, $season, null, $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,
            '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);
	      if ($offerTotal < $basePrice) {
	        return [
	          'status' => 'rejected',
	          'message' => ($team['name'] ?? 'The club') . ' rejected the low bid.',
	          'player' => $player,
	          'team' => $team,
	          'basePrice' => $basePrice,
	          'cashPrice' => $cashOffer,
	          'offerValue' => $offerValue,
	          'offerPlayerCount' => count($offerPlayers),
	          'offerTotal' => $offerTotal,
	        ];
	      }

	      $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;
	        $settledHumanMarketListing = false;
		        if (manager_market_listing_is_unsettled($player)) {
		          if (!manager_market_listing_is_active($state, $player, $season, $user)) reply(['ok' => false, 'error' => 'stale_listing'], 409);
		          return manager_create_market_transfer_offer($state, $season, $user, $player, $data, $now);
		        } elseif (manager_market_listing_seller($player) !== '') {
		          $settledHumanMarketListing = true;
		          manager_strip_human_market_listing_fields($player);
		        }
		        if (!$settledHumanMarketListing && manager_team_controlled_by_human($state, $season, (string)($player['sourceTeamId'] ?? ''))) reply(['ok' => false, 'error' => 'human_transfer_locked'], 409);
			        $listedPrice = (int)($player['price'] ?? player_value($player));
			        $marketRule = manager_market_division_rule($player, manager_user_division_level($state, $user), $listedPrice);
			        $price = (int)$marketRule['price'];
		        $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, 'marketBasePrice' => (int)$marketRule['listedPrice'], 'marketDivisionGap' => (int)$marketRule['gap'], 'marketPricePremiumPct' => (int)$marketRule['premiumPct'], 'cashPrice' => $cashOffer, 'offerValue' => $offerValue, 'offerTotal' => $offerTotal], 409);
		        $offerRatio = $price > 0 ? $offerTotal / $price : 1.0;
		        $rejectChance = manager_market_division_reject_chance((int)$marketRule['gap'], $offerRatio);
		        if ($rejectChance > 0 && mt_rand(1, 100) <= $rejectChance) {
		          reply(['ok' => false, 'error' => 'bid_rejected', 'message' => 'The selling club rejected the bid.', 'basePrice' => $price, 'marketBasePrice' => (int)$marketRule['listedPrice'], 'marketDivisionGap' => (int)$marketRule['gap'], 'marketPricePremiumPct' => (int)$marketRule['premiumPct'], 'cashPrice' => $cashOffer, 'offerValue' => $offerValue, 'offerTotal' => $offerTotal, 'rejectChance' => $rejectChance], 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_age_profile($player, (string)($player['originOwnerId'] ?? $player['sourceTeamId'] ?? 'market'), (string)($player['originTeamId'] ?? $player['sourceTeamId'] ?? ''), (string)($player['originTeamName'] ?? $player['sourceTeam'] ?? 'TRANSFER MARKET'), (string)($player['originDivisionName'] ?? $player['sourceDivision'] ?? ''));
	        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;
		        manager_strip_human_market_listing_fields($player);
		        unset($player['price']);
		        manager_prepare_bench_for_real_receive($state['users'][$user]);
		        $state['users'][$user]['squad']['bench'][] = $player;
		        manager_sync_emergency_loan_bench($state['users'][$user]);
		        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, $user),
	          'managerTransferOffers' => public_manager_transfer_offers($state, $user),
	          'boughtPlayer' => $boughtPlayer,
	          'price' => $cashOffer,
		          'cashPrice' => $cashOffer,
		          'basePrice' => $price,
		          'marketBasePrice' => (int)$marketRule['listedPrice'],
		          'marketDivisionGap' => (int)$marketRule['gap'],
		          'marketPricePremiumPct' => (int)$marketRule['premiumPct'],
		          '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;
	          if (manager_is_emergency_loan_player($player)) reply(['ok' => false, 'error' => 'loan_player_locked'], 409);
	          $season = current_manager_season($state, $user);
	          if ($season && !empty(manager_active_training($season, $user)[$playerId])) reply(['ok' => false, 'error' => 'player_training'], 409);
	          $listOnly = !empty($data['listOnly']);
	          if ($listOnly && !$season) reply(['ok' => false, 'error' => 'no_manager'], 409);
	          $humanMarketListing = $listOnly || ($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, $user),
	              '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]);
            }
	          }
	          manager_sync_emergency_loan_bench($state['users'][$user]);
	          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, $user),
	            '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'] ?? '');
      $swapSeason = current_manager_season($state, $user);
      if ($swapSeason) {
        $trainingIds = manager_active_training($swapSeason, $user);
        if (!empty($trainingIds[$firstId]) || !empty($trainingIds[$secondId])) reply(['ok' => false, 'error' => 'player_training'], 409);
      }
      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 === 'startPlayerTraining') {
      $user = require_user($state);
      if (!has_team($state['users'][$user])) reply(['ok' => false, 'error' => 'no_team'], 409);
      $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 (!in_array($user, $season['humanManagers'] ?? [], true)) reply(['ok' => false, 'error' => 'not_your_season'], 403);
      $playerId = (string)($data['playerId'] ?? '');
      $result = manager_start_player_training($state, $season, $user, $playerId, $now);
      if (empty($result['ok'])) reply(['ok' => false, 'error' => (string)($result['error'] ?? 'bad_player'), 'maxTraining' => (int)($result['max'] ?? MANAGER_MAX_TRAINING_PLAYERS)], 409);
      return [
        'training' => $result['training'] ?? null,
        'managerSeason' => public_manager_season($season, $state, $user),
        'managerFinance' => public_finance($state['users'][$user]),
        'managerSquad' => public_manager_context_squad($state, $user, $season),
      ];
    }

    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 = manager_unavailable_player_ids($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'];
      $state['users'][$user]['flagPattern'] = $team['flagPattern'] ?? 'plain';
      $state['users'][$user]['flagAccent'] = $team['flagAccent'] ?? ($team['trim'] ?? '#111111');
      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);
      }
    }

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

	    $managerInvites = $logged ? public_manager_invites($state, $user) : [];
	    $needsTeams = $logged && !has_team($state['users'][$user] ?? []);
	    $currentManagerSeason = $logged ? current_manager_season($state, $user) : null;
	    $response = [
	      'user' => $logged ? public_user($user, $state['users'][$user], $state) : null,
	      'match' => $match,
	      'managerInvites' => $managerInvites,
      'managerHasSeason' => $currentManagerSeason !== null,
      'managerSeasonStarted' => $currentManagerSeason !== null ? manager_season_has_started($currentManagerSeason) : false,
      'managerSaveUsers' => $logged ? manager_current_save_users($state, $user) : [],
      'managerCanContinue' => $logged ? manager_user_can_continue($state, $user) : false,
      'managerHasAnySave' => $logged ? manager_user_has_any_save($user) : false,
      'managerTurnAlerts' => $logged ? manager_turn_alerts($state, $user) : [],
      'managerPrivateChatAlerts' => $logged ? public_manager_private_chat_alerts($state, $user) : [],
      'loggedOut' => $sessionHadUser && !$logged,
      'appVersion' => app_version(),
	    ];
	    if ($needsTeams || $managerInvites) $response['teams'] = public_teams($state, $user);
    return $response;
  });

  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);
}
