<?php

/**
 * Scheduled maintenance tick.
 *
 * Nothing in the platform previously acted on `auto_start`, so a tournament with
 * a start time simply never started. This endpoint is the missing scheduler:
 * call it every minute (CapRover cron, an external pinger, or a container-side
 * loop) and it will:
 *
 *   - start tournaments whose start time has passed and that have enough players
 *   - advance rounds whose games have all finished
 *   - forfeit no-shows so a round cannot stall forever
 *   - close out matches whose clock has run out while nobody was watching
 *
 * Every step is idempotent, so running it twice in the same minute is harmless.
 */

header('Content-Type: application/json');

require_once __DIR__ . '/../includes/supabase.php';
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/chess.php';
require_once __DIR__ . '/../includes/tournament-engine.php';

// Authenticated by a shared secret rather than a user token, so a scheduler can
// call it with no session. When no secret is configured the endpoint refuses to
// run rather than defaulting to open.
$secret = $_GET['secret'] ?? ($_SERVER['HTTP_X_CRON_SECRET'] ?? '');
if (CRON_SECRET === '' || !hash_equals(CRON_SECRET, (string)$secret)) {
    http_response_code(403);
    echo json_encode(['error' => 'forbidden']);
    exit;
}

$db = supabaseService();
$report = ['started' => [], 'advanced' => [], 'forfeited' => [], 'flagged' => [], 'errors' => []];

// How long a player has to appear for their game before losing it by no-show.
$NO_SHOW_SECONDS = (int)(getenv('TOURNAMENT_NO_SHOW_SECONDS') ?: 600);

// ---------------------------------------------------------------------------
// 1. Start tournaments that are due
// ---------------------------------------------------------------------------
$due = $db->get('el3ab_tournaments', [
    'status' => 'in.(' . TOURNAMENT_STATUS_REGISTRATION . ',' . TOURNAMENT_STATUS_DRAFT . ')',
    'auto_start' => 'is.true',
    'select' => 'id,name,starts_at,min_players',
    'limit' => 50,
]);
if (is_array($due) && !isset($due['error'])) {
    foreach ($due as $t) {
        $startsAt = $t['starts_at'] ?? null;
        if (!$startsAt || strtotime($startsAt) > time()) continue;

        $res = tournamentStart($db, $t['id']);
        if ($res['ok']) {
            $report['started'][] = ['id' => $t['id'], 'name' => $t['name']];
        } else {
            $report['errors'][] = ['id' => $t['id'], 'stage' => 'start', 'error' => $res['error']];
        }
    }
}

// ---------------------------------------------------------------------------
// 2. Running tournaments: flag dead clocks, forfeit no-shows, advance rounds
// ---------------------------------------------------------------------------
$running = $db->get('el3ab_tournaments', [
    'status' => 'eq.' . TOURNAMENT_STATUS_IN_PROGRESS,
    'select' => '*',
    'limit' => 50,
]);
if (is_array($running) && !isset($running['error'])) {
    foreach ($running as $t) {
        $roundNumber = (int)($t['current_round'] ?? 0);
        if ($roundNumber < 1) continue;

        $round = tournamentRound($db, $t['id'], $roundNumber);
        if (!$round || $round['status'] !== ROUND_STATUS_IN_PROGRESS) continue;

        $roundStarted = strtotime($round['started_at'] ?? 'now');

        foreach ($round['pairings'] as $p) {
            if (!empty($p['is_bye'])) continue;
            if (($p['result'] ?? null) !== null) continue;

            $matchId = tournamentMatchIdFor($t['id'], $roundNumber, (int)($p['board'] ?? 1));
            $rows = $db->get('matches', [
                'id' => 'eq.' . $matchId,
                'select' => 'id,game_key,white_player_id,black_player_id,status,current_fen,move_count,'
                          . 'white_time_remaining_ms,black_time_remaining_ms,started_at,updated_at,'
                          . 'tournament_id,tournament_round,metadata',
                'limit' => 1,
            ]);
            $match = (is_array($rows) && !empty($rows) && !isset($rows['error'])) ? $rows[0] : null;

            // Nobody opened the board at all: forfeit once the grace period is up.
            if (!$match) {
                if (time() - $roundStarted > $NO_SHOW_SECONDS) {
                    // Neither player showed. Forfeit the pairing to nobody so the
                    // round can close; both players score zero.
                    if (tournamentForfeitPairing($db, $t['id'], $roundNumber, $p['pairing_id'] ?? '', $p['white_id'])) {
                        $report['forfeited'][] = ['tournament' => $t['id'], 'board' => $p['board'] ?? null, 'reason' => 'no_show'];
                    }
                }
                continue;
            }

            if ($match['status'] !== 'in_progress') continue;

            // Somebody's clock ran out while both clients were gone.
            $sideToMove = chessSideToMove($match['current_fen'] ?? null);
            if ($sideToMove === null) continue;
            $metadata = is_array($match['metadata'] ?? null) ? $match['metadata'] : (json_decode((string)($match['metadata'] ?? '{}'), true) ?: []);
            $anchor = $metadata['last_move_at'] ?? $match['started_at'] ?? null;
            if ((int)($match['move_count'] ?? 0) < 1 || !$anchor) {
                // Never started: no-show forfeit for whoever was due to move.
                if (time() - $roundStarted > $NO_SHOW_SECONDS) {
                    $absent = $sideToMove === 'w' ? $match['white_player_id'] : $match['black_player_id'];
                    if (tournamentForfeitPairing($db, $t['id'], $roundNumber, $p['pairing_id'] ?? '', $absent)) {
                        $report['forfeited'][] = ['tournament' => $t['id'], 'board' => $p['board'] ?? null, 'reason' => 'no_first_move'];
                    }
                }
                continue;
            }

            $elapsedMs = max(0, (time() - strtotime($anchor)) * 1000);
            $col = $sideToMove === 'w' ? 'white_time_remaining_ms' : 'black_time_remaining_ms';
            if ((int)($match[$col] ?? 0) - $elapsedMs > 0) continue;

            $derived = chessDeriveResult('timeout', $match['current_fen'] ?? null, $sideToMove);
            if (cronFinaliseMatch($db, $match, $derived, $col)) {
                $report['flagged'][] = ['match' => $match['id'], 'result' => $derived['result']];
            }
        }

        $adv = tournamentMaybeAdvance($db, $t['id']);
        if (!empty($adv['advanced']) || !empty($adv['completed'])) {
            $report['advanced'][] = ['id' => $t['id'], 'result' => $adv];
        }
    }
}

echo json_encode($report, JSON_UNESCAPED_UNICODE);

/**
 * Close a match from the scheduler.
 *
 * A trimmed copy of game.php's finaliseMatch(): the same conditional write and
 * the same follow-up work, without needing a request-scoped user.
 */
function cronFinaliseMatch($db, array $match, array $derived, string $zeroClockColumn): bool {
    if (!in_array($derived['result'], MATCH_RESULT_ENUM, true)) return false;

    $metadata = is_array($match['metadata'] ?? null) ? $match['metadata'] : (json_decode((string)($match['metadata'] ?? '{}'), true) ?: []);
    $metadata['end_reason'] = $derived['reason'];
    $metadata['winner'] = $derived['winner'];
    $metadata['ended_by'] = 'system';

    $written = $db->update('matches', [
        'status' => 'completed',
        'result' => $derived['result'],
        'completed_at' => gmdate('c'),
        'updated_at' => gmdate('c'),
        'metadata' => $metadata,
        $zeroClockColumn => 0,
    ], ['id' => 'eq.' . $match['id'], 'status' => 'neq.completed']);

    if (isset($written['error']) || empty($written)) return false;

    $winnerId = chessWinnerId($derived['winner'], $match['white_player_id'], $match['black_player_id']);
    supabaseRpc('complete_match', [
        'p_game_key' => $match['game_key'] ?? 'chess',
        'p_match_id' => $match['id'],
        'p_winners'  => $winnerId ? [$winnerId] : [],
        'p_reason'   => $derived['reason'],
    ]);

    if (!empty($match['tournament_id'])) {
        tournamentRecordMatchResult($db, $match, $derived);
    }
    return true;
}
