<?php
/**
 * Grand Prix - Social tournaments ranking with points and social ELO
 *
 * @package Chess_Podium
 */

if (!defined('ABSPATH')) {
    exit;
}

final class ChessPodium_GrandPrix
{
    private const DEFAULT_POINTS = [1 => 10, 2 => 7, 3 => 5, 4 => 4, 5 => 3, 6 => 2, 7 => 1, 8 => 1];
    private const DEFAULT_ELO_START = 1000;
    private const DEFAULT_ELO_K = 32;

    public static function init(): void
    {
        add_action('admin_menu', [self::class, 'register_menu'], 20);
        add_action('admin_init', [self::class, 'handle_actions'], 5);
    }

    public static function create_tables(): void
    {
        global $wpdb;
        $gpTable = self::get_table();
        $gptTable = self::get_tournaments_table();
        if ($wpdb->get_var("SHOW TABLES LIKE '$gpTable'") !== $gpTable) { // phpcs:ignore
            require_once ABSPATH . 'wp-admin/includes/upgrade.php';
            $charset = $wpdb->get_charset_collate();
            $wpdb->query("CREATE TABLE $gpTable (
                id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
                name VARCHAR(191) NOT NULL,
                slug VARCHAR(191) NOT NULL DEFAULT '',
                description TEXT NULL,
                points_schema LONGTEXT NULL,
                elo_start INT NOT NULL DEFAULT 1000,
                elo_k INT NOT NULL DEFAULT 32,
                created_at DATETIME NOT NULL,
                PRIMARY KEY (id),
                KEY slug (slug)
            ) $charset"); // phpcs:ignore
        }
        if ($wpdb->get_var("SHOW TABLES LIKE '$gptTable'") !== $gptTable) { // phpcs:ignore
            require_once ABSPATH . 'wp-admin/includes/upgrade.php';
            $charset = $wpdb->get_charset_collate();
            $tournamentsTable = $wpdb->prefix . 'regina_tournaments';
            $wpdb->query("CREATE TABLE $gptTable (
                id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
                grand_prix_id BIGINT UNSIGNED NOT NULL,
                tournament_id BIGINT UNSIGNED NOT NULL,
                sort_order INT NOT NULL DEFAULT 0,
                created_at DATETIME NOT NULL,
                PRIMARY KEY (id),
                UNIQUE KEY gp_tournament (grand_prix_id, tournament_id),
                KEY grand_prix_id (grand_prix_id),
                KEY tournament_id (tournament_id)
            ) $charset"); // phpcs:ignore
        }
        self::maybe_upgrade_grand_prix_schema();
    }

    private static function maybe_upgrade_grand_prix_schema(): void
    {
        global $wpdb;
        $gpTable = self::get_table();
        if ($wpdb->get_var("SHOW TABLES LIKE '$gpTable'") !== $gpTable) { // phpcs:ignore
            return;
        }
        $col = $wpdb->get_var("SHOW COLUMNS FROM $gpTable LIKE 'player_identity_json'"); // phpcs:ignore
        if ($col === null) {
            $wpdb->query("ALTER TABLE $gpTable ADD COLUMN player_identity_json LONGTEXT NULL"); // phpcs:ignore
        }
    }

    public static function register_menu(): void
    {
        add_submenu_page(
            'chess-podium',
            __('Grand Prix', 'chess-podium'),
            __('Grand Prix', 'chess-podium'),
            'manage_options',
            'chess-podium-grand-prix',
            [self::class, 'render_admin_page']
        );
    }

    public static function get_table(): string
    {
        global $wpdb;
        return $wpdb->prefix . 'regina_grand_prix';
    }

    public static function get_tournaments_table(): string
    {
        global $wpdb;
        return $wpdb->prefix . 'regina_grand_prix_tournaments';
    }

    public static function get_all(): array
    {
        global $wpdb;
        $table = self::get_table();
        return $wpdb->get_results("SELECT * FROM $table ORDER BY created_at DESC"); // phpcs:ignore
    }

    public static function get(int $id): ?object
    {
        global $wpdb;
        $table = self::get_table();
        return $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE id = %d", $id));
    }

    /** @return array{merges: array<string, string>, profiles: array<string, array{name?: string, fide_id?: string, country?: string}>} */
    public static function get_identity_config(int $grandPrixId): array
    {
        $gp = self::get($grandPrixId);
        if (!$gp || empty($gp->player_identity_json)) {
            return ['merges' => [], 'profiles' => []];
        }
        $decoded = json_decode((string) $gp->player_identity_json, true);
        if (!is_array($decoded)) {
            return ['merges' => [], 'profiles' => []];
        }
        $merges = [];
        if (isset($decoded['merges']) && is_array($decoded['merges'])) {
            foreach ($decoded['merges'] as $from => $to) {
                $from = is_string($from) ? trim($from) : '';
                $to = is_string($to) ? trim($to) : '';
                if ($from !== '' && $to !== '' && $from !== $to && strlen($from) <= 500 && strlen($to) <= 500) {
                    $merges[$from] = $to;
                }
            }
        }
        $profiles = [];
        if (isset($decoded['profiles']) && is_array($decoded['profiles'])) {
            foreach ($decoded['profiles'] as $pk => $prof) {
                if (!is_string($pk) || trim($pk) === '' || strlen($pk) > 500 || !is_array($prof)) {
                    continue;
                }
                $profiles[trim($pk)] = [
                    'name' => isset($prof['name']) ? sanitize_text_field((string) $prof['name']) : '',
                    'fide_id' => isset($prof['fide_id']) ? preg_replace('/\D+/', '', (string) $prof['fide_id']) : '',
                    'country' => isset($prof['country']) ? strtoupper(substr(preg_replace('/[^a-zA-Z]/', '', (string) $prof['country']), 0, 2)) : '',
                ];
            }
        }
        return ['merges' => $merges, 'profiles' => $profiles];
    }

    /** @param array{merges?: array<string, string>, profiles?: array<string, array<string, string>>} $config */
    public static function save_identity_config(int $grandPrixId, array $config): bool
    {
        global $wpdb;
        $merges = [];
        if (isset($config['merges']) && is_array($config['merges'])) {
            foreach ($config['merges'] as $from => $to) {
                $from = is_string($from) ? trim($from) : '';
                $to = is_string($to) ? trim($to) : '';
                if ($from !== '' && $to !== '' && $from !== $to && strlen($from) <= 500 && strlen($to) <= 500) {
                    $merges[$from] = $to;
                }
            }
        }
        $profiles = [];
        if (isset($config['profiles']) && is_array($config['profiles'])) {
            foreach ($config['profiles'] as $pk => $prof) {
                if (!is_string($pk) || trim($pk) === '' || strlen($pk) > 500 || !is_array($prof)) {
                    continue;
                }
                $name = isset($prof['name']) ? sanitize_text_field((string) $prof['name']) : '';
                $fid = isset($prof['fide_id']) ? preg_replace('/\D+/', '', (string) $prof['fide_id']) : '';
                $country = isset($prof['country']) ? strtoupper(substr(preg_replace('/[^a-zA-Z]/', '', (string) $prof['country']), 0, 2)) : '';
                if ($name === '' && $fid === '' && $country === '') {
                    continue;
                }
                $profiles[trim($pk)] = array_filter([
                    'name' => $name,
                    'fide_id' => $fid,
                    'country' => $country,
                ], static function ($v) {
                    return $v !== null && $v !== '';
                });
            }
        }
        $json = wp_json_encode(['merges' => $merges, 'profiles' => $profiles], JSON_UNESCAPED_UNICODE);
        if ($json === false) {
            return false;
        }
        return (bool) $wpdb->update(
            self::get_table(),
            ['player_identity_json' => $json],
            ['id' => $grandPrixId],
            ['%s'],
            ['%d']
        );
    }

    private static function resolve_canonical_player_key(string $key, array $mergeMap): string
    {
        $visited = [];
        for ($i = 0; $i < 200; $i++) {
            if (!isset($mergeMap[$key])) {
                return $key;
            }
            $next = $mergeMap[$key];
            if ($next === $key) {
                return $key;
            }
            if (isset($visited[$key])) {
                return $key;
            }
            $visited[$key] = true;
            $key = $next;
        }
        return $key;
    }

    private static function resolved_player_key(string $name, string $fideId, array $mergeMap): string
    {
        $raw = self::player_key($name, $fideId);
        return self::resolve_canonical_player_key($raw, $mergeMap);
    }

    private static function apply_profile_overrides(array $playerRow, array $profiles): array
    {
        $pk = $playerRow['player_key'] ?? '';
        if ($pk === '' || !isset($profiles[$pk]) || !is_array($profiles[$pk])) {
            return $playerRow;
        }
        $p = $profiles[$pk];
        if (!empty($p['name'])) {
            $playerRow['name'] = $p['name'];
        }
        if (isset($p['fide_id']) && $p['fide_id'] !== '') {
            $playerRow['fide_id'] = $p['fide_id'];
        }
        if (!empty($p['country'])) {
            $playerRow['country'] = $p['country'];
        }
        return $playerRow;
    }

    public static function get_tournaments(int $grandPrixId): array
    {
        global $wpdb;
        $table = self::get_tournaments_table();
        $tournamentsTable = $wpdb->prefix . 'regina_tournaments';
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT gpt.*, t.name as tournament_name, t.current_round, t.rounds_total
             FROM $table gpt
             JOIN $tournamentsTable t ON t.id = gpt.tournament_id
             WHERE gpt.grand_prix_id = %d
             ORDER BY gpt.sort_order ASC, gpt.created_at ASC",
            $grandPrixId
        ));
        return $rows ? (array) $rows : [];
    }

    /** Tournaments that have at least one round played (have results). */
    public static function get_tournaments_with_results(int $grandPrixId): array
    {
        $all = self::get_tournaments($grandPrixId);
        return array_filter($all, static function ($t) {
            return (int) ($t->current_round ?? 0) >= 1;
        });
    }

    public static function get_standings(int $grandPrixId): array
    {
        $gp = self::get($grandPrixId);
        if (!$gp) {
            return [];
        }
        $identity = self::get_identity_config($grandPrixId);
        $mergeMap = $identity['merges'];
        $profiles = $identity['profiles'];
        $pointsSchema = self::parse_points_schema($gp->points_schema ?? '');
        $tournaments = self::get_tournaments_with_results($grandPrixId);
        $playerData = [];

        foreach ($tournaments as $gt) {
            $tid = (int) $gt->tournament_id;
            $standings = ChessPodiumPlugin::compute_standings($tid);
            foreach ($standings as $pos => $s) {
                $key = self::resolved_player_key($s['name'] ?? '', (string) ($s['fide_id'] ?? ''), $mergeMap);
                $country = trim((string) ($s['country'] ?? ''));
                if (!isset($playerData[$key])) {
                    $playerData[$key] = [
                        'player_key' => $key,
                        'name' => $s['name'] ?? '',
                        'fide_id' => $s['fide_id'] ?? '',
                        'country' => $country,
                        'gp_points' => 0.0,
                        'tournaments' => 0,
                        'games' => [],
                    ];
                } elseif ($country !== '' && ($playerData[$key]['country'] ?? '') === '') {
                    $playerData[$key]['country'] = $country;
                }
                $position = $pos + 1;
                $pts = $pointsSchema[$position] ?? ($pointsSchema['participate'] ?? 0);
                $playerData[$key]['gp_points'] += (float) $pts;
                $playerData[$key]['tournaments']++;
                $playerData[$key]['results'][] = [
                    'tournament_id' => $tid,
                    'tournament_name' => $gt->tournament_name ?? '',
                    'position' => $position,
                    'points' => (float) $pts,
                    'tournament_points' => (float) ($s['points'] ?? 0),
                ];
            }
        }

        $players = array_values($playerData);
        foreach ($players as $i => $row) {
            $players[$i] = self::apply_profile_overrides($row, $profiles);
        }
        usort($players, static function ($a, $b) {
            return $b['gp_points'] <=> $a['gp_points'];
        });

        return $players;
    }

    /**
     * Compute social ELO for all players in the GP.
     * Games are processed in chronological order (by tournament round).
     */
    public static function get_social_elo(int $grandPrixId): array
    {
        $gp = self::get($grandPrixId);
        if (!$gp) {
            return [];
        }
        $identity = self::get_identity_config($grandPrixId);
        $mergeMap = $identity['merges'];
        $profiles = $identity['profiles'];
        $eloStart = (int) ($gp->elo_start ?? self::DEFAULT_ELO_START);
        $eloK = (int) ($gp->elo_k ?? self::DEFAULT_ELO_K);
        $tournaments = self::get_tournaments_with_results($grandPrixId);

        $ratings = [];
        $games = [];

        foreach ($tournaments as $gt) {
            $tid = (int) $gt->tournament_id;
            $pairings = ChessPodiumPlugin::get_pairings($tid);
            $players = ChessPodiumPlugin::get_players($tid);
            $playerNames = [];
            foreach ($players as $p) {
                $playerNames[(int) $p->id] = ['name' => $p->name, 'fide_id' => $p->fide_id ?? ''];
            }
            foreach ($pairings as $m) {
                if ((int) $m->is_bye === 1) {
                    continue;
                }
                $wId = (int) $m->white_player_id;
                $bId = (int) $m->black_player_id;
                if ($bId <= 0) {
                    continue;
                }
                $wInfo = $playerNames[$wId] ?? null;
                $bInfo = $playerNames[$bId] ?? null;
                if (!$wInfo || !$bInfo) {
                    continue;
                }
                $res = ChessPodiumPlugin::normalize_result((string) ($m->result ?? ''));
                $scoreW = 0.5;
                if (in_array($res, ['1-0', '1F'], true)) {
                    $scoreW = 1.0;
                } elseif (in_array($res, ['0-1', '0F'], true)) {
                    $scoreW = 0.0;
                }
                $games[] = [
                    'tournament_id' => $tid,
                    'round' => (int) $m->round_no,
                    'white' => $wInfo,
                    'black' => $bInfo,
                    'score_white' => $scoreW,
                ];
            }
        }

        foreach ($games as $g) {
            $wKey = self::resolved_player_key($g['white']['name'], (string) $g['white']['fide_id'], $mergeMap);
            $bKey = self::resolved_player_key($g['black']['name'], (string) $g['black']['fide_id'], $mergeMap);
            if (!isset($ratings[$wKey])) {
                $ratings[$wKey] = ['name' => $g['white']['name'], 'fide_id' => $g['white']['fide_id'], 'elo' => $eloStart];
            }
            if (!isset($ratings[$bKey])) {
                $ratings[$bKey] = ['name' => $g['black']['name'], 'fide_id' => $g['black']['fide_id'], 'elo' => $eloStart];
            }
            $rw = $ratings[$wKey]['elo'];
            $rb = $ratings[$bKey]['elo'];
            $ew = 1 / (1 + pow(10, ($rb - $rw) / 400));
            $eb = 1 - $ew;
            $ratings[$wKey]['elo'] = round($rw + $eloK * ($g['score_white'] - $ew));
            $ratings[$bKey]['elo'] = round($rb + $eloK * ((1 - $g['score_white']) - $eb));
        }

        $out = [];
        foreach ($ratings as $canonicalKey => $row) {
            $row['player_key'] = $canonicalKey;
            $out[] = self::apply_profile_overrides($row, $profiles);
        }
        usort($out, static function ($a, $b) {
            return $b['elo'] <=> $a['elo'];
        });
        return $out;
    }

    /**
     * Combined standings: GP points + social ELO.
     */
    public static function get_combined_standings(int $grandPrixId): array
    {
        $standings = self::get_standings($grandPrixId);
        $elos = self::get_social_elo($grandPrixId);
        $eloMap = [];
        foreach ($elos as $e) {
            $key = isset($e['player_key']) && is_string($e['player_key']) && $e['player_key'] !== ''
                ? $e['player_key']
                : self::player_key($e['name'] ?? '', (string) ($e['fide_id'] ?? ''));
            $eloMap[$key] = (int) $e['elo'];
        }
        foreach ($standings as &$s) {
            $s['social_elo'] = $eloMap[$s['player_key']] ?? null;
        }
        // Attach unified player profile mapping (site-wide) when available.
        if (class_exists('ChessPodium_PlayerDirectory')) {
            foreach ($standings as &$s) {
                $name = (string) ($s['name'] ?? '');
                $fid = (string) ($s['fide_id'] ?? '');
                $cty = (string) ($s['country'] ?? '');
                $pk = (string) ($s['player_key'] ?? '');
                $unifiedId = $pk !== '' ? ChessPodium_PlayerDirectory::get_unified_id_for_gp_key($grandPrixId, $pk) : 0;
                $unified = $unifiedId > 0 ? ChessPodium_PlayerDirectory::get_by_id($unifiedId) : null;
                if (!$unified) {
                    $unified = ChessPodium_PlayerDirectory::resolve_or_create($name, $fid, $cty);
                    if ($unified && $pk !== '') {
                        ChessPodium_PlayerDirectory::map_gp_key_to_player($grandPrixId, $pk, (int) $unified->id);
                    }
                }
                if ($unified) {
                    $s['unified_player_id'] = (int) $unified->id;
                    $s['unified_player_slug'] = (string) ($unified->slug ?? '');
                }
            }
        }
        return $standings;
    }

    /** @return list<string> */
    private static function get_combined_standings_player_keys(int $grandPrixId): array
    {
        $rows = self::get_combined_standings($grandPrixId);
        $keys = [];
        foreach ($rows as $r) {
            if (!empty($r['player_key']) && is_string($r['player_key'])) {
                $keys[] = $r['player_key'];
            }
        }
        return array_values(array_unique($keys));
    }

    private static function player_key(string $name, string $fideId): string
    {
        $name = trim($name);
        $fideId = trim($fideId);
        if ($fideId !== '') {
            return 'fide:' . $fideId;
        }
        return 'name:' . mb_strtolower($name);
    }

    private static function parse_points_schema(string $json): array
    {
        if ($json === '') {
            return self::DEFAULT_POINTS;
        }
        $decoded = json_decode($json, true);
        if (!is_array($decoded)) {
            return self::DEFAULT_POINTS;
        }
        $out = [];
        foreach ($decoded as $k => $v) {
            if (is_numeric($k)) {
                $out[(int) $k] = (float) $v;
            } elseif ($k === 'participate') {
                $out['participate'] = (float) $v;
            }
        }
        return $out;
    }

    private static function points_schema_to_json(array $schema): string
    {
        return wp_json_encode($schema);
    }

    public static function create(string $name, string $description, string $pointsSchema, int $eloStart, int $eloK): int
    {
        global $wpdb;
        $table = self::get_table();
        $slug = sanitize_title($name) ?: 'grand-prix-' . time();
        $wpdb->insert($table, [
            'name' => $name,
            'slug' => $slug,
            'description' => $description,
            'points_schema' => $pointsSchema,
            'elo_start' => $eloStart,
            'elo_k' => $eloK,
            'created_at' => current_time('mysql'),
        ], ['%s', '%s', '%s', '%s', '%d', '%d', '%s']);
        return (int) $wpdb->insert_id;
    }

    public static function update(int $id, string $name, string $description, string $pointsSchema, int $eloStart, int $eloK): bool
    {
        global $wpdb;
        $table = self::get_table();
        return (bool) $wpdb->update($table, [
            'name' => $name,
            'description' => $description,
            'points_schema' => $pointsSchema,
            'elo_start' => $eloStart,
            'elo_k' => $eloK,
        ], ['id' => $id], ['%s', '%s', '%s', '%d', '%d'], ['%d']);
    }

    public static function delete(int $id): bool
    {
        global $wpdb;
        $wpdb->delete(self::get_tournaments_table(), ['grand_prix_id' => $id], ['%d']);
        return (bool) $wpdb->delete(self::get_table(), ['id' => $id], ['%d']);
    }

    public static function add_tournament(int $grandPrixId, int $tournamentId): bool
    {
        global $wpdb;
        $table = self::get_tournaments_table();
        $exists = $wpdb->get_var($wpdb->prepare(
            "SELECT id FROM $table WHERE grand_prix_id = %d AND tournament_id = %d",
            $grandPrixId,
            $tournamentId
        ));
        if ($exists) {
            return true;
        }
        $maxOrder = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COALESCE(MAX(sort_order), 0) FROM $table WHERE grand_prix_id = %d",
            $grandPrixId
        ));
        return (bool) $wpdb->insert($table, [
            'grand_prix_id' => $grandPrixId,
            'tournament_id' => $tournamentId,
            'sort_order' => $maxOrder + 1,
            'created_at' => current_time('mysql'),
        ], ['%d', '%d', '%d', '%s']);
    }

    public static function remove_tournament(int $grandPrixId, int $tournamentId): bool
    {
        global $wpdb;
        return (bool) $wpdb->delete(self::get_tournaments_table(), [
            'grand_prix_id' => $grandPrixId,
            'tournament_id' => $tournamentId,
        ], ['%d', '%d']);
    }

    /** Get Grand Prix IDs that include this tournament. */
    public static function get_grand_prix_ids_for_tournament(int $tournamentId): array
    {
        global $wpdb;
        $table = self::get_tournaments_table();
        $rows = $wpdb->get_col($wpdb->prepare(
            "SELECT grand_prix_id FROM $table WHERE tournament_id = %d",
            $tournamentId
        ));
        return $rows ? array_map('intval', (array) $rows) : [];
    }

    /** Set which Grand Prix this tournament belongs to. Replaces existing associations. */
    public static function set_tournament_grand_prix(int $tournamentId, array $grandPrixIds): void
    {
        global $wpdb;
        $table = self::get_tournaments_table();
        $wpdb->delete($table, ['tournament_id' => $tournamentId], ['%d']);
        $grandPrixIds = array_unique(array_filter(array_map('intval', $grandPrixIds)));
        foreach ($grandPrixIds as $gpId) {
            if ($gpId > 0) {
                self::add_tournament($gpId, $tournamentId);
            }
        }
    }

    public static function handle_actions(): void
    {
        if (!is_admin() || !current_user_can('manage_options')) {
            return;
        }
        if (empty($_POST['regina_action'])) {
            return;
        }
        check_admin_referer('regina_torneo_action', 'regina_nonce');
        $action = sanitize_text_field(wp_unslash($_POST['regina_action']));

        if ($action === 'grand_prix_create') {
            $name = sanitize_text_field(wp_unslash($_POST['gp_name'] ?? ''));
            if ($name !== '') {
                $desc = sanitize_textarea_field(wp_unslash($_POST['gp_description'] ?? ''));
                $schema = self::sanitize_points_schema_input($_POST['gp_points'] ?? []);
                $eloStart = max(800, min(1500, (int) ($_POST['gp_elo_start'] ?? self::DEFAULT_ELO_START)));
                $eloK = max(10, min(64, (int) ($_POST['gp_elo_k'] ?? self::DEFAULT_ELO_K)));
                $id = self::create($name, $desc, $schema, $eloStart, $eloK);
                wp_safe_redirect(admin_url('admin.php?page=chess-podium-grand-prix&gp_id=' . $id . '&cp_msg=gp_created'));
                exit;
            }
        }

        if ($action === 'grand_prix_update') {
            $id = (int) ($_POST['gp_id'] ?? 0);
            if ($id > 0) {
                $name = sanitize_text_field(wp_unslash($_POST['gp_name'] ?? ''));
                $desc = sanitize_textarea_field(wp_unslash($_POST['gp_description'] ?? ''));
                $schema = self::sanitize_points_schema_input($_POST['gp_points'] ?? []);
                $eloStart = max(800, min(1500, (int) ($_POST['gp_elo_start'] ?? self::DEFAULT_ELO_START)));
                $eloK = max(10, min(64, (int) ($_POST['gp_elo_k'] ?? self::DEFAULT_ELO_K)));
                self::update($id, $name, $desc, $schema, $eloStart, $eloK);
                wp_safe_redirect(admin_url('admin.php?page=chess-podium-grand-prix&gp_id=' . $id . '&cp_msg=gp_updated'));
                exit;
            }
        }

        if ($action === 'grand_prix_delete') {
            $id = (int) ($_POST['gp_id'] ?? 0);
            if ($id > 0) {
                self::delete($id);
                wp_safe_redirect(admin_url('admin.php?page=chess-podium-grand-prix&cp_msg=gp_deleted'));
                exit;
            }
        }

        if ($action === 'grand_prix_add_tournament') {
            $gpId = (int) ($_POST['gp_id'] ?? 0);
            $tid = (int) ($_POST['tournament_id'] ?? 0);
            if ($gpId > 0 && $tid > 0) {
                self::add_tournament($gpId, $tid);
                wp_safe_redirect(admin_url('admin.php?page=chess-podium-grand-prix&gp_id=' . $gpId . '&cp_msg=gp_tournament_added'));
                exit;
            }
        }

        if ($action === 'grand_prix_remove_tournament') {
            $gpId = (int) ($_POST['gp_id'] ?? 0);
            $tid = (int) ($_POST['tournament_id'] ?? 0);
            if ($gpId > 0 && $tid > 0) {
                self::remove_tournament($gpId, $tid);
                wp_safe_redirect(admin_url('admin.php?page=chess-podium-grand-prix&gp_id=' . $gpId . '&cp_msg=gp_tournament_removed'));
                exit;
            }
        }

        if ($action === 'grand_prix_save_player_profiles') {
            $gpId = (int) ($_POST['gp_id'] ?? 0);
            if ($gpId > 0 && self::get($gpId)) {
                $keys = isset($_POST['gp_profile_key']) && is_array($_POST['gp_profile_key'])
                    ? array_map(static function ($v) {
                        return substr(sanitize_text_field(wp_unslash((string) $v)), 0, 500);
                    }, (array) $_POST['gp_profile_key'])
                    : [];
                $names = isset($_POST['gp_profile_name']) && is_array($_POST['gp_profile_name'])
                    ? array_map(static function ($v) {
                        return sanitize_text_field(wp_unslash((string) $v));
                    }, (array) $_POST['gp_profile_name'])
                    : [];
                $fides = isset($_POST['gp_profile_fide']) && is_array($_POST['gp_profile_fide'])
                    ? array_map(static function ($v) {
                        return preg_replace('/\D+/', '', wp_unslash((string) $v));
                    }, (array) $_POST['gp_profile_fide'])
                    : [];
                $countries = isset($_POST['gp_profile_country']) && is_array($_POST['gp_profile_country'])
                    ? array_map(static function ($v) {
                        return strtoupper(substr(preg_replace('/[^a-zA-Z]/', '', wp_unslash((string) $v)), 0, 2));
                    }, (array) $_POST['gp_profile_country'])
                    : [];
                $base = self::get_identity_config($gpId);
                $profiles = $base['profiles'];
                foreach ($keys as $i => $pk) {
                    if ($pk === '') {
                        continue;
                    }
                    $name = $names[$i] ?? '';
                    $fid = $fides[$i] ?? '';
                    $cty = $countries[$i] ?? '';
                    if ($name === '' && $fid === '' && $cty === '') {
                        unset($profiles[$pk]);
                        continue;
                    }
                    $profiles[$pk] = array_filter([
                        'name' => $name,
                        'fide_id' => $fid,
                        'country' => $cty,
                    ], static function ($v) {
                        return $v !== null && $v !== '';
                    });
                }
                $base['profiles'] = $profiles;
                self::save_identity_config($gpId, $base);
                wp_safe_redirect(admin_url('admin.php?page=chess-podium-grand-prix&gp_id=' . $gpId . '&cp_msg=gp_players_saved'));
                exit;
            }
        }

        if ($action === 'grand_prix_merge_players') {
            $gpId = (int) ($_POST['gp_id'] ?? 0);
            $keep = substr(sanitize_text_field(wp_unslash((string) ($_POST['gp_merge_keep'] ?? ''))), 0, 500);
            $duplicate = substr(sanitize_text_field(wp_unslash((string) ($_POST['gp_merge_duplicate'] ?? ''))), 0, 500);
            if ($gpId > 0 && $keep !== '' && $duplicate !== '' && self::get($gpId)) {
                $validKeys = self::get_combined_standings_player_keys($gpId);
                if (!in_array($keep, $validKeys, true) || !in_array($duplicate, $validKeys, true)) {
                    wp_safe_redirect(admin_url('admin.php?page=chess-podium-grand-prix&gp_id=' . $gpId . '&cp_msg=gp_merge_invalid'));
                    exit;
                }
                $base = self::get_identity_config($gpId);
                $merges = $base['merges'];
                $rk = self::resolve_canonical_player_key($keep, $merges);
                $rm = self::resolve_canonical_player_key($duplicate, $merges);
                if ($rk === $rm) {
                    wp_safe_redirect(admin_url('admin.php?page=chess-podium-grand-prix&gp_id=' . $gpId . '&cp_msg=gp_merge_same'));
                    exit;
                }
                $merges[$rm] = $rk;
                $profiles = $base['profiles'];
                if (isset($profiles[$rm])) {
                    if (!isset($profiles[$rk])) {
                        $profiles[$rk] = $profiles[$rm];
                    } else {
                        foreach (['name', 'fide_id', 'country'] as $f) {
                            $cur = $profiles[$rk][$f] ?? '';
                            $alt = $profiles[$rm][$f] ?? '';
                            if (($cur === '' || $cur === null) && $alt !== '' && $alt !== null) {
                                $profiles[$rk][$f] = $alt;
                            }
                        }
                    }
                    unset($profiles[$rm]);
                }
                $base['merges'] = $merges;
                $base['profiles'] = $profiles;
                self::save_identity_config($gpId, $base);
                wp_safe_redirect(admin_url('admin.php?page=chess-podium-grand-prix&gp_id=' . $gpId . '&cp_msg=gp_players_merged'));
                exit;
            }
        }

        if ($action === 'update_tournament_grand_prix') {
            $tid = (int) ($_POST['tournament_id'] ?? 0);
            if ($tid > 0) {
                $gpIds = isset($_POST['grand_prix_ids']) && is_array($_POST['grand_prix_ids'])
                    ? array_map('intval', (array) $_POST['grand_prix_ids'])
                    : [];
                self::set_tournament_grand_prix($tid, $gpIds);
                wp_safe_redirect(admin_url('admin.php?page=chess-podium&tournament_id=' . $tid . '&cp_tab=settings&cp_msg=tournament_gp_saved'));
                exit;
            }
        }
    }

    private static function sanitize_points_schema_input($input): string
    {
        if (!is_array($input)) {
            return wp_json_encode(self::DEFAULT_POINTS);
        }
        $out = [];
        foreach ($input as $pos => $val) {
            if (is_numeric($pos) && $pos >= 1) {
                $out[(int) $pos] = max(0, (float) $val);
            }
        }
        if (isset($input['participate']) && is_numeric($input['participate'])) {
            $out['participate'] = max(0, (float) $input['participate']);
        }
        return wp_json_encode($out ?: self::DEFAULT_POINTS);
    }

    public static function render_admin_page(): void
    {
        $gpId = isset($_GET['gp_id']) ? (int) $_GET['gp_id'] : 0;
        $msg = isset($_GET['cp_msg']) ? sanitize_key(wp_unslash($_GET['cp_msg'])) : '';
        $messages = [
            'gp_created' => __('Grand Prix created.', 'chess-podium'),
            'gp_updated' => __('Grand Prix updated.', 'chess-podium'),
            'gp_deleted' => __('Grand Prix deleted.', 'chess-podium'),
            'gp_tournament_added' => __('Tournament added to Grand Prix.', 'chess-podium'),
            'gp_tournament_removed' => __('Tournament removed from Grand Prix.', 'chess-podium'),
            'gp_players_saved' => __('Grand Prix player display saved.', 'chess-podium'),
            'gp_players_merged' => __('Players merged in this Grand Prix. Standings and social ELO now use a single identity.', 'chess-podium'),
            'gp_merge_same' => __('Those players are already merged or identical.', 'chess-podium'),
            'gp_merge_invalid' => __('Could not merge: pick two players from the list.', 'chess-podium'),
        ];
        if ($msg && isset($messages[$msg])) {
            $noticeType = in_array($msg, ['gp_merge_same', 'gp_merge_invalid'], true) ? 'error' : 'success';
            echo '<div class="notice notice-' . esc_attr($noticeType) . ' is-dismissible"><p>' . esc_html($messages[$msg]) . '</p></div>';
        }

        if ($gpId > 0) {
            self::render_edit_page($gpId);
        } else {
            self::render_list_page();
        }
    }

    private static function render_list_page(): void
    {
        $gps = self::get_all();
        ?>
        <div class="wrap">
            <h1><?php esc_html_e('Grand Prix', 'chess-podium'); ?></h1>
            <p><?php esc_html_e('Create Grand Prix rankings for social tournaments. Add points from each tournament and compute a social ELO based on performance.', 'chess-podium'); ?></p>

            <form method="post" style="max-width:600px; margin:2rem 0;">
                <?php wp_nonce_field('regina_torneo_action', 'regina_nonce'); ?>
                <input type="hidden" name="regina_action" value="grand_prix_create">
                <h2><?php esc_html_e('Create Grand Prix', 'chess-podium'); ?></h2>
                <table class="form-table">
                    <tr>
                        <th><label for="gp_name"><?php esc_html_e('Name', 'chess-podium'); ?></label></th>
                        <td><input id="gp_name" name="gp_name" type="text" class="regular-text" required placeholder="<?php esc_attr_e('e.g. Social Grand Prix 2025', 'chess-podium'); ?>"></td>
                    </tr>
                    <tr>
                        <th><label for="gp_description"><?php esc_html_e('Description', 'chess-podium'); ?></label></th>
                        <td><textarea id="gp_description" name="gp_description" rows="2" class="large-text" placeholder="<?php esc_attr_e('Optional description', 'chess-podium'); ?>"></textarea></td>
                    </tr>
                    <tr>
                        <th><label><?php esc_html_e('Points by position', 'chess-podium'); ?></label></th>
                        <td>
                            <p class="description"><?php esc_html_e('Points awarded for each position in a tournament. Leave empty for default.', 'chess-podium'); ?></p>
                            <div style="display:flex; gap:1rem; flex-wrap:wrap; margin-top:0.5rem;">
                                <?php for ($i = 1; $i <= 10; $i++): ?>
                                    <label>#<?php echo (int) $i; ?> <input type="number" name="gp_points[<?php echo (int) $i; ?>]" value="<?php echo $i <= 8 ? (int) (self::DEFAULT_POINTS[$i] ?? 0) : 0; ?>" min="0" step="0.5" style="width:60px;"></label>
                                <?php endfor; ?>
                            </div>
                            <label style="margin-top:0.5rem; display:block;"><?php esc_html_e('Participation:', 'chess-podium'); ?> <input type="number" name="gp_points[participate]" value="0" min="0" step="0.5" style="width:60px;"> <?php esc_html_e('pts per tournament played', 'chess-podium'); ?></label>
                        </td>
                    </tr>
                    <tr>
                        <th><label for="gp_elo_start"><?php esc_html_e('Social ELO start', 'chess-podium'); ?></label></th>
                        <td><input id="gp_elo_start" name="gp_elo_start" type="number" value="<?php echo (int) self::DEFAULT_ELO_START; ?>" min="800" max="1500" style="width:80px;"> <?php esc_html_e('(initial rating for new players)', 'chess-podium'); ?></td>
                    </tr>
                    <tr>
                        <th><label for="gp_elo_k"><?php esc_html_e('Social ELO K-factor', 'chess-podium'); ?></label></th>
                        <td><input id="gp_elo_k" name="gp_elo_k" type="number" value="<?php echo (int) self::DEFAULT_ELO_K; ?>" min="10" max="64" style="width:80px;"> <?php esc_html_e('(sensitivity of rating changes)', 'chess-podium'); ?></td>
                    </tr>
                </table>
                <p><button type="submit" class="button button-primary"><?php esc_html_e('Create Grand Prix', 'chess-podium'); ?></button></p>
            </form>

            <?php if (!empty($gps)): ?>
            <h2><?php esc_html_e('Existing Grand Prix', 'chess-podium'); ?></h2>
            <table class="wp-list-table widefat fixed striped">
                <thead>
                    <tr>
                        <th><?php esc_html_e('Name', 'chess-podium'); ?></th>
                        <th><?php esc_html_e('Tournaments', 'chess-podium'); ?></th>
                        <th><?php esc_html_e('Actions', 'chess-podium'); ?></th>
                    </tr>
                </thead>
                <tbody>
                    <?php foreach ($gps as $gp): ?>
                        <?php $tours = self::get_tournaments((int) $gp->id); ?>
                        <tr>
                            <td><strong><?php echo esc_html($gp->name); ?></strong></td>
                            <td><?php echo count($tours); ?></td>
                            <td>
                                <a href="<?php echo esc_url(admin_url('admin.php?page=chess-podium-grand-prix&gp_id=' . (int) $gp->id)); ?>" class="button"><?php esc_html_e('Edit', 'chess-podium'); ?></a>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                </tbody>
            </table>
            <?php endif; ?>
        </div>
        <?php
    }

    private static function render_edit_page(int $gpId): void
    {
        $gp = self::get($gpId);
        if (!$gp) {
            echo '<p>' . esc_html__('Grand Prix not found.', 'chess-podium') . '</p>';
            echo '<p><a href="' . esc_url(admin_url('admin.php?page=chess-podium-grand-prix')) . '">' . esc_html__('Back to list', 'chess-podium') . '</a></p>';
            return;
        }
        $tournaments = self::get_tournaments($gpId);
        $allTournaments = ChessPodiumPlugin::get_tournaments();
        $includedIds = array_column($tournaments, 'tournament_id');
        $pointsSchema = self::parse_points_schema($gp->points_schema ?? '');
        $standings = self::get_combined_standings($gpId);
        ?>
        <div class="wrap">
            <h1><?php echo esc_html($gp->name); ?></h1>
            <p><a href="<?php echo esc_url(admin_url('admin.php?page=chess-podium-grand-prix')); ?>">&larr; <?php esc_html_e('Back to Grand Prix list', 'chess-podium'); ?></a></p>

            <form method="post" style="max-width:600px; margin:2rem 0;">
                <?php wp_nonce_field('regina_torneo_action', 'regina_nonce'); ?>
                <input type="hidden" name="regina_action" value="grand_prix_update">
                <input type="hidden" name="gp_id" value="<?php echo (int) $gpId; ?>">
                <h2><?php esc_html_e('Settings', 'chess-podium'); ?></h2>
                <table class="form-table">
                    <tr>
                        <th><label for="gp_name"><?php esc_html_e('Name', 'chess-podium'); ?></label></th>
                        <td><input id="gp_name" name="gp_name" type="text" class="regular-text" value="<?php echo esc_attr($gp->name); ?>" required></td>
                    </tr>
                    <tr>
                        <th><label for="gp_description"><?php esc_html_e('Description', 'chess-podium'); ?></label></th>
                        <td><textarea id="gp_description" name="gp_description" rows="2" class="large-text"><?php echo esc_textarea($gp->description ?? ''); ?></textarea></td>
                    </tr>
                    <tr>
                        <th><label><?php esc_html_e('Points by position', 'chess-podium'); ?></label></th>
                        <td>
                            <div style="display:flex; gap:1rem; flex-wrap:wrap; margin-top:0.5rem;">
                                <?php for ($i = 1; $i <= 10; $i++): ?>
                                    <label>#<?php echo (int) $i; ?> <input type="number" name="gp_points[<?php echo (int) $i; ?>]" value="<?php echo $i <= 10 ? (float) ($pointsSchema[$i] ?? 0) : 0; ?>" min="0" step="0.5" style="width:60px;"></label>
                                <?php endfor; ?>
                            </div>
                            <label style="margin-top:0.5rem; display:block;"><?php esc_html_e('Participation:', 'chess-podium'); ?> <input type="number" name="gp_points[participate]" value="<?php echo (float) ($pointsSchema['participate'] ?? 0); ?>" min="0" step="0.5" style="width:60px;"></label>
                        </td>
                    </tr>
                    <tr>
                        <th><label for="gp_elo_start"><?php esc_html_e('Social ELO start', 'chess-podium'); ?></label></th>
                        <td><input id="gp_elo_start" name="gp_elo_start" type="number" value="<?php echo (int) ($gp->elo_start ?? self::DEFAULT_ELO_START); ?>" min="800" max="1500" style="width:80px;"></td>
                    </tr>
                    <tr>
                        <th><label for="gp_elo_k"><?php esc_html_e('Social ELO K-factor', 'chess-podium'); ?></label></th>
                        <td><input id="gp_elo_k" name="gp_elo_k" type="number" value="<?php echo (int) ($gp->elo_k ?? self::DEFAULT_ELO_K); ?>" min="10" max="64" style="width:80px;"></td>
                    </tr>
                </table>
                <p><button type="submit" class="button button-primary"><?php esc_html_e('Save settings', 'chess-podium'); ?></button></p>
            </form>

            <h2><?php esc_html_e('Tournaments in this Grand Prix', 'chess-podium'); ?></h2>
            <p><?php esc_html_e('Add tournaments that have already been played. You can include past tournaments retroactively.', 'chess-podium'); ?></p>

            <?php if (!empty($tournaments)): ?>
            <table class="wp-list-table widefat fixed striped" style="max-width:700px;">
                <thead>
                    <tr>
                        <th><?php esc_html_e('Tournament', 'chess-podium'); ?></th>
                        <th><?php esc_html_e('Rounds', 'chess-podium'); ?></th>
                        <th></th>
                    </tr>
                </thead>
                <tbody>
                    <?php foreach ($tournaments as $t): ?>
                        <tr>
                            <td><?php echo esc_html($t->tournament_name); ?></td>
                            <td><?php echo (int) $t->current_round; ?> / <?php echo (int) $t->rounds_total; ?></td>
                            <td>
                                <form method="post" style="display:inline;">
                                    <?php wp_nonce_field('regina_torneo_action', 'regina_nonce'); ?>
                                    <input type="hidden" name="regina_action" value="grand_prix_remove_tournament">
                                    <input type="hidden" name="gp_id" value="<?php echo (int) $gpId; ?>">
                                    <input type="hidden" name="tournament_id" value="<?php echo (int) $t->tournament_id; ?>">
                                    <button type="submit" class="button button-link-delete"><?php esc_html_e('Remove', 'chess-podium'); ?></button>
                                </form>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                </tbody>
            </table>
            <?php endif; ?>

            <form method="post" style="margin-top:1rem;">
                <?php wp_nonce_field('regina_torneo_action', 'regina_nonce'); ?>
                <input type="hidden" name="regina_action" value="grand_prix_add_tournament">
                <input type="hidden" name="gp_id" value="<?php echo (int) $gpId; ?>">
                <select name="tournament_id">
                    <option value=""><?php esc_html_e('— Add tournament —', 'chess-podium'); ?></option>
                    <?php foreach ($allTournaments as $t): ?>
                        <?php if (in_array((int) $t->id, $includedIds, true)) continue; ?>
                        <?php if ((int) $t->current_round < 1) continue; ?>
                        <option value="<?php echo (int) $t->id; ?>"><?php echo esc_html($t->name); ?> (<?php echo (int) $t->current_round; ?> rounds)</option>
                    <?php endforeach; ?>
                </select>
                <button type="submit" class="button"><?php esc_html_e('Add', 'chess-podium'); ?></button>
            </form>

            <h2><?php esc_html_e('Players in this Grand Prix', 'chess-podium'); ?></h2>
            <p class="description" style="max-width:820px;"><?php esc_html_e('If the same person appears under different names or with/without a FIDE ID, merge the duplicate into the player you want to keep. You can edit display name, FIDE ID, or country for the Grand Prix standings and shortcode only — individual tournament rosters are not changed.', 'chess-podium'); ?></p>

            <?php if (!empty($standings)): ?>
            <form method="post" style="margin:1rem 0 1.5rem;padding:12px;background:#f6f7f7;border:1px solid #c3c4c7;border-radius:4px;max-width:820px;">
                <?php wp_nonce_field('regina_torneo_action', 'regina_nonce'); ?>
                <input type="hidden" name="regina_action" value="grand_prix_merge_players">
                <input type="hidden" name="gp_id" value="<?php echo (int) $gpId; ?>">
                <strong><?php esc_html_e('Merge duplicate player', 'chess-podium'); ?></strong>
                <p class="description" style="margin:0.35rem 0 0.75rem;"><?php esc_html_e('Choose the duplicate entry first, then the player to keep. GP points and social ELO are combined.', 'chess-podium'); ?></p>
                <div style="display:flex;flex-wrap:wrap;gap:12px;align-items:center;">
                    <label>
                        <?php esc_html_e('Duplicate (merge away)', 'chess-podium'); ?>
                        <select name="gp_merge_duplicate" class="regular-text" style="min-width:220px;margin-left:6px;" required>
                            <option value=""><?php esc_html_e('— Select —', 'chess-podium'); ?></option>
                            <?php foreach ($standings as $s): ?>
                                <option value="<?php echo esc_attr($s['player_key']); ?>"><?php echo esc_html($s['name']); ?> (<?php echo esc_html(number_format((float) $s['gp_points'], 1)); ?>)</option>
                            <?php endforeach; ?>
                        </select>
                    </label>
                    <span aria-hidden="true">→</span>
                    <label>
                        <?php esc_html_e('Keep (canonical player)', 'chess-podium'); ?>
                        <select name="gp_merge_keep" class="regular-text" style="min-width:220px;margin-left:6px;" required>
                            <option value=""><?php esc_html_e('— Select —', 'chess-podium'); ?></option>
                            <?php foreach ($standings as $s): ?>
                                <option value="<?php echo esc_attr($s['player_key']); ?>"><?php echo esc_html($s['name']); ?> (<?php echo esc_html(number_format((float) $s['gp_points'], 1)); ?>)</option>
                            <?php endforeach; ?>
                        </select>
                    </label>
                    <button type="submit" class="button button-secondary" onclick="return confirm('<?php echo esc_js(__('Merge these two players in this Grand Prix?', 'chess-podium')); ?>');"><?php esc_html_e('Merge players', 'chess-podium'); ?></button>
                </div>
            </form>

            <form method="post" style="max-width:920px;">
                <?php wp_nonce_field('regina_torneo_action', 'regina_nonce'); ?>
                <input type="hidden" name="regina_action" value="grand_prix_save_player_profiles">
                <input type="hidden" name="gp_id" value="<?php echo (int) $gpId; ?>">
            <h2><?php esc_html_e('Standings', 'chess-podium'); ?></h2>
            <table class="wp-list-table widefat fixed striped">
                <thead>
                    <tr>
                        <th>#</th>
                        <th><?php esc_html_e('Display name', 'chess-podium'); ?></th>
                        <th><?php esc_html_e('FIDE ID', 'chess-podium'); ?></th>
                        <th><?php esc_html_e('Country', 'chess-podium'); ?></th>
                        <th><?php esc_html_e('GP Points', 'chess-podium'); ?></th>
                        <th><?php esc_html_e('Tournaments', 'chess-podium'); ?></th>
                        <th><?php esc_html_e('Social ELO', 'chess-podium'); ?></th>
                    </tr>
                </thead>
                <tbody>
                    <?php foreach ($standings as $i => $s): ?>
                        <tr>
                            <td><?php echo esc_html((string) ($i + 1)); ?></td>
                            <td>
                                <input type="hidden" name="gp_profile_key[]" value="<?php echo esc_attr($s['player_key']); ?>">
                                <?php echo wp_kses_post(ChessPodium_CountryHelper::render_flag($s['country'] ?? '', $s['country'] ?? '')); ?>
                                <input type="text" name="gp_profile_name[]" value="<?php echo esc_attr($s['name']); ?>" class="regular-text" style="max-width:200px;">
                            </td>
                            <td><input type="text" name="gp_profile_fide[]" value="<?php echo esc_attr((string) ($s['fide_id'] ?? '')); ?>" maxlength="20" style="width:7rem;" inputmode="numeric"></td>
                            <td><input type="text" name="gp_profile_country[]" value="<?php echo esc_attr((string) ($s['country'] ?? '')); ?>" maxlength="2" style="width:3rem;text-transform:uppercase;"></td>
                            <td><strong><?php echo esc_html(number_format((float) $s['gp_points'], 1)); ?></strong></td>
                            <td><?php echo esc_html((string) (int) $s['tournaments']); ?></td>
                            <td><?php echo $s['social_elo'] !== null ? esc_html((string) (int) $s['social_elo']) : '—'; ?></td>
                        </tr>
                    <?php endforeach; ?>
                </tbody>
            </table>
                <p><button type="submit" class="button button-primary"><?php esc_html_e('Save player display', 'chess-podium'); ?></button></p>
            </form>
            <?php else: ?>
            <h2><?php esc_html_e('Standings', 'chess-podium'); ?></h2>
                <p><?php esc_html_e('No standings yet. Add tournaments to this Grand Prix.', 'chess-podium'); ?></p>
            <?php endif; ?>

            <form method="post" style="margin-top:2rem;" onsubmit="return confirm('<?php echo esc_js(__('Delete this Grand Prix?', 'chess-podium')); ?>');">
                <?php wp_nonce_field('regina_torneo_action', 'regina_nonce'); ?>
                <input type="hidden" name="regina_action" value="grand_prix_delete">
                <input type="hidden" name="gp_id" value="<?php echo (int) $gpId; ?>">
                <button type="submit" class="button button-link-delete"><?php esc_html_e('Delete Grand Prix', 'chess-podium'); ?></button>
            </form>
        </div>
        <?php
    }

    /**
     * Shortcode: [chess_podium_grand_prix id="1"]
     */
    public static function render_shortcode($atts): string
    {
        $atts = shortcode_atts(['id' => 0], $atts, 'chess_podium_grand_prix');
        $gpId = (int) $atts['id'];
        if ($gpId <= 0) {
            return '';
        }
        $gp = self::get($gpId);
        if (!$gp) {
            return '';
        }
        $standings = self::get_combined_standings($gpId);
        $tournamentsWithResults = self::get_tournaments_with_results($gpId);
        ob_start();
        ?>
        <div class="cp-grand-prix">
            <h2 class="cp-grand-prix-title"><?php echo esc_html($gp->name); ?></h2>
            <?php if (!empty($gp->description)): ?>
                <p class="cp-grand-prix-desc"><?php echo esc_html($gp->description); ?></p>
            <?php endif; ?>

            <h3 class="cp-grand-prix-section"><?php esc_html_e('GP standings', 'chess-podium'); ?></h3>
            <?php if (empty($standings)): ?>
                <p><?php esc_html_e('No standings yet.', 'chess-podium'); ?></p>
            <?php else: ?>
            <table class="cp-grand-prix-table">
                <thead>
                    <tr>
                        <th>#</th>
                        <th><?php esc_html_e('Player', 'chess-podium'); ?></th>
                        <th><?php esc_html_e('Points', 'chess-podium'); ?></th>
                        <th><?php esc_html_e('Tournaments', 'chess-podium'); ?></th>
                        <th><?php esc_html_e('Social ELO', 'chess-podium'); ?></th>
                    </tr>
                </thead>
                <tbody>
                    <?php foreach ($standings as $i => $s): ?>
                        <tr>
                            <td><?php echo esc_html((string) ($i + 1)); ?></td>
                            <td>
                                <?php
                                $label = self::render_player_with_flag($s['name'] ?? '', $s['country'] ?? '');
                                $slug = isset($s['unified_player_slug']) ? (string) $s['unified_player_slug'] : '';
                                $url = ($slug !== '' && class_exists('ChessPodiumPlugin')) ? ChessPodiumPlugin::get_unified_player_profile_url($slug) : '';
                                echo $url !== '' ? '<a href="' . esc_url($url) . '">' . wp_kses_post($label) . '</a>' : wp_kses_post($label);
                                ?>
                            </td>
                            <td><strong><?php echo esc_html(number_format((float) $s['gp_points'], 1)); ?></strong></td>
                            <td><?php echo esc_html((string) (int) $s['tournaments']); ?></td>
                            <td><?php echo $s['social_elo'] !== null ? esc_html((string) (int) $s['social_elo']) : '—'; ?></td>
                        </tr>
                    <?php endforeach; ?>
                </tbody>
            </table>
            <?php endif; ?>

            <?php if (!empty($tournamentsWithResults)): ?>
            <h3 class="cp-grand-prix-section"><?php esc_html_e('Tournament standings', 'chess-podium'); ?></h3>
            <?php foreach ($tournamentsWithResults as $gt): ?>
                <?php
                $tid = (int) $gt->tournament_id;
                $tStandings = ChessPodiumPlugin::compute_standings($tid);
                ?>
            <h4 class="cp-grand-prix-tournament-name"><?php echo esc_html($gt->tournament_name ?? ''); ?></h4>
            <?php if (empty($tStandings)): ?>
                <p><?php esc_html_e('No standings.', 'chess-podium'); ?></p>
            <?php else: ?>
            <table class="cp-grand-prix-table cp-grand-prix-tournament-table">
                <thead>
                    <tr>
                        <th>#</th>
                        <th><?php esc_html_e('Player', 'chess-podium'); ?></th>
                        <th><?php esc_html_e('Pts', 'chess-podium'); ?></th>
                        <th><?php echo esc_html(__('Buchholz', 'chess-podium')); ?></th>
                        <th><?php echo esc_html(__('SB', 'chess-podium')); ?></th>
                    </tr>
                </thead>
                <tbody>
                    <?php foreach ($tStandings as $pos => $ts): ?>
                        <tr>
                            <td><?php echo esc_html((string) ($pos + 1)); ?></td>
                            <td><?php echo wp_kses_post(self::render_player_with_flag($ts['name'] ?? '', $ts['country'] ?? '', $ts['title'] ?? null)); ?></td>
                            <td><?php echo esc_html(number_format((float) ($ts['points'] ?? 0), 1)); ?></td>
                            <td><?php echo esc_html(number_format((float) ($ts['buchholz'] ?? 0), 2)); ?></td>
                            <td><?php echo esc_html(number_format((float) ($ts['sb'] ?? 0), 2)); ?></td>
                        </tr>
                    <?php endforeach; ?>
                </tbody>
            </table>
            <?php endif; ?>
            <?php endforeach; ?>
            <?php endif; ?>
        </div>
        <?php
        return ob_get_clean();
    }

    private static function render_player_with_flag(string $name, string $country, ?string $title = null): string
    {
        $flag = trim($country) !== ''
            ? ChessPodium_CountryHelper::render_flag_img($country, $country) . ' '
            : '';
        $titleStr = $title ? esc_html(trim($title)) . ' ' : '';
        return $flag . $titleStr . esc_html($name);
    }
}
