<?php
/**
 * PGN Live Stream - URL/FTP sources for automatic PGN fetch
 *
 * @package Chess_Podium
 */

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

final class ChessPodium_PgnLive
{
    public const CRON_HOOK = 'chess_podium_fetch_pgn_sources';

    public static function init(): void
    {
        add_action(self::CRON_HOOK, [self::class, 'fetch_all_sources']);
        add_action('init', [self::class, 'schedule_cron']);
        add_filter('cron_schedules', [self::class, 'add_cron_interval']);
    }

    public static function add_cron_interval(array $schedules): array
    {
        $schedules['chess_podium_pgn_interval'] = [
            'interval' => 2,
            'display' => __('Every 2 seconds (PGN Live)', 'chess-podium'),
        ];
        return $schedules;
    }

    public static function schedule_cron(): void
    {
        $next = wp_next_scheduled(self::CRON_HOOK);
        if ($next) {
            $interval = 2;
            $expected = time() + $interval;
            if ($next > $expected + 30) {
                wp_clear_scheduled_hook(self::CRON_HOOK);
                $next = false;
            }
        }
        if (!$next) {
            wp_schedule_event(time(), 'chess_podium_pgn_interval', self::CRON_HOOK);
        }
    }

    public static function fetch_all_sources(): void
    {
        $sources = self::get_all_sources();
        foreach ($sources as $src) {
            self::fetch_source($src);
        }
    }

    public static function fetch_sources_for_tournament(int $tournamentId): int
    {
        $sources = self::get_sources_for_tournament($tournamentId);
        $total = 0;
        foreach ($sources as $src) {
            $total += self::fetch_source($src);
        }
        return $total;
    }

    public static function fetch_source(object $source): int
    {
        $url = (string) ($source->source_url ?? '');
        if ($url === '') {
            return 0;
        }
        $response = wp_remote_get($url, ['timeout' => 60]);
        if (is_wp_error($response)) {
            return 0;
        }
        $code = wp_remote_retrieve_response_code($response);
        if ($code !== 200) {
            return 0;
        }
        $body = wp_remote_retrieve_body($response);
        if ($body === '') {
            return 0;
        }
        $tid = (int) $source->tournament_id;
        $round = (int) ($source->round_no ?? 1);
        $count = self::import_pgn_content($tid, $body, $round);
        if ($count > 0) {
            self::update_last_fetched((int) $source->id);
        }
        return $count;
    }

    private static function import_pgn_content(int $tournamentId, string $content, int $round): int
    {
        if (!class_exists('ChessPodiumPlugin')) {
            return 0;
        }
        $games = self::split_pgn_games($content);
        $count = 0;
        $pairings = self::get_pairings_for_round($tournamentId, $round);
        $nonByePairings = array_values(array_filter($pairings, static function ($p) {
            return (int) ($p->is_bye ?? 0) !== 1;
        }));
        foreach ($games as $game) {
            $pairing = self::find_pairing_for_pgn_game($game, $nonByePairings);
            if ($pairing !== null) {
                self::update_pairing_live_from_pgn((int) $pairing->id, $game, (int) $pairing->tournament_id, (int) $pairing->round_no);
                $count++;
            }
        }
        return $count;
    }

    /**
     * Match a PGN game to a pairing by White/Black from PGN headers.
     * Prefers FIDE ID when available (Lichess broadcasts include WhiteFideId/BlackFideId).
     * Falls back to name slug for format differences (e.g. "Santos Latasa, Jaime" vs "Jaime Santos Latasa").
     */
    private static function find_pairing_for_pgn_game(string $pgn, array $nonByePairings): ?object
    {
        $headers = self::parse_pgn_headers($pgn);
        $pgnWhite = trim((string) ($headers['white'] ?? ''));
        $pgnBlack = trim((string) ($headers['black'] ?? ''));
        $pgnWhiteFide = self::sanitize_fide_id((string) ($headers['whitefideid'] ?? ''));
        $pgnBlackFide = self::sanitize_fide_id((string) ($headers['blackfideid'] ?? ''));

        foreach ($nonByePairings as $p) {
            $dbWhiteFide = self::sanitize_fide_id((string) ($p->white_fide_id ?? ''));
            $dbBlackFide = self::sanitize_fide_id((string) ($p->black_fide_id ?? ''));

            if ($pgnWhiteFide !== '' && $pgnBlackFide !== '' && $dbWhiteFide !== '' && $dbBlackFide !== '') {
                if ($pgnWhiteFide === $dbWhiteFide && $pgnBlackFide === $dbBlackFide) {
                    return $p;
                }
                continue;
            }

            $pgnKey = self::name_to_slug($pgnWhite) . '||' . self::name_to_slug($pgnBlack);
            if ($pgnKey === '||') {
                continue;
            }
            $dbWhite = trim((string) ($p->white_name ?? ''));
            $dbBlack = trim((string) ($p->black_name ?? ''));
            $dbKey = self::name_to_slug($dbWhite) . '||' . self::name_to_slug($dbBlack);
            if ($pgnKey === $dbKey) {
                return $p;
            }
        }
        return null;
    }

    private static function sanitize_fide_id(string $raw): string
    {
        $s = preg_replace('/[^0-9]/', '', $raw);
        return $s !== '' ? $s : '';
    }

    /**
     * Normalize a player name to a slug for comparison. Removes punctuation,
     * collapses whitespace, sorts tokens, normalizes accents (e.g. Örn vs Orn).
     * "Santos Latasa, Jaime" and "Jaime Santos Latasa" both become "jaime|latasa|santos".
     */
    private static function name_to_slug(string $name): string
    {
        $s = preg_replace('/[^\p{L}\p{N}\s]/u', ' ', $name);
        $s = preg_replace('/\s+/', ' ', trim($s));
        if (class_exists('Normalizer')) {
            $s = Normalizer::normalize($s, Normalizer::FORM_KD);
            $s = preg_replace('/\p{M}/u', '', $s);
        }
        $tokens = array_filter(explode(' ', strtolower($s)));
        sort($tokens);
        return implode('|', $tokens);
    }

    private static function get_pairings_for_round(int $tournamentId, int $round): array
    {
        global $wpdb;
        if ($round <= 0) {
            return [];
        }
        $pairings = $wpdb->prefix . 'regina_pairings';
        $players = $wpdb->prefix . 'regina_players';
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT p.*, wp.name AS white_name, bp.name AS black_name, wp.fide_id AS white_fide_id, bp.fide_id AS black_fide_id
            FROM $pairings p
            LEFT JOIN $players wp ON p.white_player_id = wp.id
            LEFT JOIN $players bp ON p.black_player_id = bp.id
            WHERE p.tournament_id = %d AND p.round_no = %d
            ORDER BY (CASE WHEN p.is_bye = 1 OR p.result IN ('1F','0F') THEN 1 ELSE 0 END) ASC, p.id ASC",
            $tournamentId,
            $round
        ));
        return $rows ?: [];
    }

    private static function update_pairing_live_from_pgn(int $pairingId, string $pgn, int $tournamentId = 0, int $roundNo = 0): void
    {
        global $wpdb;
        $table = $wpdb->prefix . 'regina_pairings';
        $pgnsTable = $wpdb->prefix . 'regina_tournament_pgns';
        $pgnClean = self::strip_pgn_for_storage($pgn);
        $headers = self::parse_pgn_headers($pgn);
        $pgnResult = trim((string) ($headers['result'] ?? '*'));

        $validFinal = ['1-0', '0-1', '1/2-1/2'];
        $result = null;
        if ($pgnResult !== '' && $pgnResult !== '*') {
            $normalized = class_exists('ChessPodiumPlugin')
                ? ChessPodiumPlugin::normalize_result($pgnResult)
                : trim($pgnResult);
            if (in_array($normalized, $validFinal, true)) {
                $result = $normalized;
            }
        }

        if ($result !== null) {
            $wpdb->update(
                $table,
                [
                    'result' => $result,
                    'is_live' => 0,
                ],
                ['id' => $pairingId],
                ['%s', '%d'],
                ['%d']
            );
            if ($tournamentId > 0 && $roundNo > 0) {
                $existing = $wpdb->get_var($wpdb->prepare(
                    "SELECT id FROM $pgnsTable WHERE tournament_id = %d AND pairing_id = %d LIMIT 1",
                    $tournamentId,
                    $pairingId
                ));
                if (!$existing) {
                    $white = trim((string) ($headers['white'] ?? ''));
                    $black = trim((string) ($headers['black'] ?? ''));
                    self::insert_pgn($tournamentId, $roundNo, $pgnClean, $white ?: null, $black ?: null, $result, $pairingId);
                }
            }
        } else {
            $wpdb->update(
                $table,
                ['is_live' => 1, 'live_pgn' => $pgnClean],
                ['id' => $pairingId],
                ['%d', '%s'],
                ['%d']
            );
        }
    }

    /**
     * Minimal cleanup for PGN storage. Preserves [%clk], [%eval] and { } blocks
     * needed for live board sync. chess.js on the frontend handles parsing.
     */
    private static function strip_pgn_for_storage(string $pgn): string
    {
        return trim($pgn);
    }

    /**
     * Split multi-game PGN (e.g. Lichess broadcast round file). Public for round import.
     */
    public static function split_games(string $content): array
    {
        return self::split_pgn_games($content);
    }

    /**
     * Parse headers for import (same as internal parser).
     */
    public static function parse_headers_for_import(string $pgn): array
    {
        return self::parse_pgn_headers($pgn);
    }

    /**
     * Match a tournament player to PGN White/Black using FIDE ID first, then normalized name.
     */
    public static function find_player_for_broadcast_header(string $name, ?string $fideId, array $players): ?object
    {
        $fid = self::sanitize_fide_id((string) ($fideId ?? ''));
        if ($fid !== '') {
            foreach ($players as $p) {
                if (self::sanitize_fide_id((string) ($p->fide_id ?? '')) === $fid) {
                    return $p;
                }
            }
        }
        $slug = self::name_to_slug($name);
        if ($slug === '') {
            return null;
        }
        foreach ($players as $p) {
            if (self::name_to_slug((string) ($p->name ?? '')) === $slug) {
                return $p;
            }
        }
        return null;
    }

    private static function split_pgn_games(string $content): array
    {
        $games = [];
        $content = trim($content);
        $parts = preg_split('/\n\s*\n(?=\s*\[Event)/', $content);
        foreach ($parts as $p) {
            $p = trim($p);
            if ($p !== '' && preg_match('/\[Event/', $p)) {
                $games[] = $p;
            }
        }
        if (empty($games) && $content !== '') {
            $games[] = $content;
        }
        return $games;
    }

    private static function parse_pgn_headers(string $pgn): array
    {
        $out = [];
        if (preg_match('/\[White\s+"([^"]*)"\]/', $pgn, $m)) {
            $out['white'] = $m[1];
        }
        if (preg_match('/\[Black\s+"([^"]*)"\]/', $pgn, $m)) {
            $out['black'] = $m[1];
        }
        if (preg_match('/\[Result\s+"([^"]*)"\]/', $pgn, $m)) {
            $out['result'] = $m[1];
        }
        if (preg_match('/\[WhiteFideId\s+"([^"]*)"\]/i', $pgn, $m)) {
            $out['whitefideid'] = $m[1];
        }
        if (preg_match('/\[BlackFideId\s+"([^"]*)"\]/i', $pgn, $m)) {
            $out['blackfideid'] = $m[1];
        }
        return $out;
    }

    private static function insert_pgn(int $tid, int $round, string $content, ?string $white, ?string $black, ?string $result, ?int $pairingId = null): void
    {
        global $wpdb;
        $data = [
            'tournament_id' => $tid,
            'round_no' => $round,
            'white_name' => $white,
            'black_name' => $black,
            'result' => $result,
            'pgn_content' => $content,
            'created_at' => current_time('mysql'),
        ];
        $formats = ['%d', '%d', '%s', '%s', '%s', '%s', '%s'];
        if ($pairingId !== null) {
            $data['pairing_id'] = $pairingId;
            $formats[] = '%d';
        }
        $wpdb->insert($wpdb->prefix . 'regina_tournament_pgns', $data, $formats);
    }

    private static function update_last_fetched(int $sourceId): void
    {
        global $wpdb;
        $table = self::db_table();
        $wpdb->update($table, ['last_fetched' => current_time('mysql')], ['id' => $sourceId], ['%s'], ['%d']);
    }

    public static function get_all_sources(): array
    {
        global $wpdb;
        $table = self::db_table();
        return $wpdb->get_results("SELECT * FROM $table WHERE source_url != '' AND source_url IS NOT NULL ORDER BY id ASC");
    }

    public static function get_sources_for_tournament(int $tournamentId): array
    {
        global $wpdb;
        $table = self::db_table();
        return $wpdb->get_results($wpdb->prepare("SELECT * FROM $table WHERE tournament_id = %d ORDER BY id ASC", $tournamentId));
    }

    public static function add_source(int $tournamentId, string $url, int $round = 1, int $intervalSec = 60): int
    {
        global $wpdb;
        $wpdb->insert(self::db_table(), [
            'tournament_id' => $tournamentId,
            'source_type' => 'url',
            'source_url' => $url,
            'round_no' => $round,
            'fetch_interval_sec' => $intervalSec,
            'created_at' => current_time('mysql'),
        ], ['%d', '%s', '%s', '%d', '%d', '%s']);
        return (int) $wpdb->insert_id;
    }

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

    private static function db_table(): string
    {
        return $GLOBALS['wpdb']->prefix . 'regina_pgn_sources';
    }

    public static function create_table(): void
    {
        global $wpdb;
        $table = self::db_table();
        if ($wpdb->get_var("SHOW TABLES LIKE '$table'") === $table) {
            return;
        }
        $charset = $wpdb->get_charset_collate();
        $sql = "CREATE TABLE $table (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            tournament_id BIGINT UNSIGNED NOT NULL,
            source_type VARCHAR(20) NOT NULL DEFAULT 'url',
            source_url VARCHAR(500) NULL,
            round_no INT NOT NULL DEFAULT 1,
            pairing_id BIGINT UNSIGNED NULL,
            last_fetched DATETIME NULL,
            fetch_interval_sec INT NOT NULL DEFAULT 60,
            created_at DATETIME NOT NULL,
            PRIMARY KEY (id),
            KEY tournament_id (tournament_id)
        ) $charset";
        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta($sql);
    }
}
