<?php
/**
 * FIDE Title Norm Detection
 *
 * Calculates whether a player has achieved an IM or GM norm in a tournament
 * per FIDE Title Regulations (effective 1 January 2024).
 *
 * @package Chess_Podium
 */

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

final class ChessPodium_FideNorm
{
    /** Minimum games for a norm (FIDE 1.4.1) */
    private const MIN_GAMES = 9;

    /** GM: min performance 2600, min avg opponent 2380, floor 2200, need 1/3 GMs */
    private const GM_RP_MIN = 2600;
    private const GM_RA_MIN = 2380;
    private const GM_FLOOR = 2200;
    private const GM_TITLES = ['GM'];

    /** IM: min performance 2450, min avg opponent 2230, floor 2050, need 1/3 IMs or GMs */
    private const IM_RP_MIN = 2450;
    private const IM_RA_MIN = 2230;
    private const IM_FLOOR = 2050;
    private const IM_TITLES = ['IM', 'GM'];

    /** Title-holders that count for 50% requirement (FIDE 1.4.5 point 6, excluding CM/WCM) */
    private const TITLED = ['GM', 'IM', 'FM', 'WGM', 'WIM', 'WFM'];

    /** Unrated opponent rating (FIDE 1.4.6) */
    private const UNRATED_RATING = 1400;

    /** Results that count for norms (exclude forfeits 1F, 0F per FIDE 1.4.2) */
    private const NORM_RESULTS = ['1-0', '0-1', '1/2-1/2'];

    /**
     * Check if a player achieves GM or IM norm in the given games.
     *
     * @param array $games Array of ['opponent_id' => int, 'opponent_rating' => int, 'opponent_title' => string, 'opponent_country' => string, 'player_pts' => float]
     * @param string $playerCountry Player's federation code
     * @param float $pointsSystemScale 1.0 for standard, 3.0 for 3-1-0
     * @return array ['gm_norm' => bool, 'im_norm' => bool]
     */
    public static function check_norms(array $games, string $playerCountry, float $pointsSystemScale = 1.0): array
    {
        $result = ['gm_norm' => false, 'im_norm' => false];
        if (count($games) < self::MIN_GAMES) {
            return $result;
        }

        $totalPts = 0.0;
        $maxPtsPerGame = $pointsSystemScale;
        foreach ($games as $g) {
            $totalPts += (float) ($g['player_pts'] ?? 0);
        }
        $p = $totalPts / (count($games) * $maxPtsPerGame);
        if ($p < 0.35) {
            return $result;
        }

        $oppRatings = [];
        $oppTitles = [];
        $oppCountries = [];
        foreach ($games as $g) {
            $r = (int) ($g['opponent_rating'] ?? 0);
            $oppRatings[] = $r > 0 ? $r : self::UNRATED_RATING;
            $oppTitles[] = isset($g['opponent_title']) && $g['opponent_title'] !== '' ? (string) $g['opponent_title'] : '';
            $oppCountries[] = isset($g['opponent_country']) && $g['opponent_country'] !== '' ? (string) $g['opponent_country'] : '';
        }

        $playerCountry = strtoupper(trim($playerCountry));
        $foreignFeds = array_unique(array_filter($oppCountries, static fn ($c) => strtoupper(trim($c)) !== $playerCountry && $c !== ''));
        if (count($foreignFeds) < 2) {
            return $result;
        }

        $gmCount = count(array_filter($oppTitles, static fn ($t) => in_array($t, self::GM_TITLES, true)));
        $imOrGmCount = count(array_filter($oppTitles, static fn ($t) => in_array($t, self::IM_TITLES, true)));
        $titledCount = count(array_filter($oppTitles, static fn ($t) => in_array($t, self::TITLED, true)));
        $minTitled = (int) ceil(count($oppTitles) * 0.5);
        if ($titledCount < $minTitled) {
            return $result;
        }

        $raGm = self::avg_rating_with_floor($oppRatings, self::GM_FLOOR);
        $raIm = self::avg_rating_with_floor($oppRatings, self::IM_FLOOR);
        $dp = self::performance_dp($p);
        $rpGm = (int) round($raGm + $dp);
        $rpIm = (int) round($raIm + $dp);

        if ($raGm >= self::GM_RA_MIN && $rpGm >= self::GM_RP_MIN && $gmCount >= 3) {
            $result['gm_norm'] = true;
        }
        if ($raIm >= self::IM_RA_MIN && $rpIm >= self::IM_RP_MIN && $imOrGmCount >= 3) {
            $result['im_norm'] = true;
        }

        return $result;
    }

    /**
     * FIDE performance rating: dp from score p (0–1).
     * Rp = Ra + dp. Uses 400*log10(p/(1-p)), capped at ±800.
     */
    private static function performance_dp(float $p): float
    {
        if ($p <= 0 || $p >= 1) {
            return $p >= 1 ? 800.0 : -800.0;
        }
        $dp = 400.0 * log10($p / (1.0 - $p));
        return max(-800.0, min(800.0, $dp));
    }

    /**
     * Average opponent rating with floor. Max 1 opponent can be raised to floor (FIDE 1.4.6).
     */
    private static function avg_rating_with_floor(array $ratings, int $floor): float
    {
        if (count($ratings) === 0) {
            return 0.0;
        }
        $below = [];
        $sum = 0.0;
        foreach ($ratings as $r) {
            if ($r < $floor) {
                $below[] = $r;
            } else {
                $sum += $r;
            }
        }
        if (count($below) === 0) {
            return $sum / count($ratings);
        }
        sort($below);
        $raised = array_pop($below);
        $sum += $floor;
        foreach ($below as $b) {
            $sum += $b;
        }
        return $sum / count($ratings);
    }

    /**
     * Build norm games for a player from pairings and standings data.
     *
     * @param int $playerId
     * @param array $pairings From get_pairings
     * @param array $playersById [id => {rating, title, country}]
     * @param array $resultsVsOpponent [playerId => [oppId => pts]]
     * @param string $pointsSystem 'standard' or '310'
     * @return array Games for norm check
     */
    public static function build_norm_games(int $playerId, array $pairings, array $playersById, array $resultsVsOpponent, string $pointsSystem = 'standard'): array
    {
        $games = [];
        $scale = ($pointsSystem === '310') ? 3.0 : 1.0;

        foreach ($pairings as $match) {
            $res = ChessPodiumPlugin::normalize_result((string) ($match->result ?? ''));
            if (!in_array($res, self::NORM_RESULTS, true)) {
                continue;
            }
            $isBye = (int) ($match->is_bye ?? 0) === 1;
            if ($isBye) {
                continue;
            }

            $w = (int) $match->white_player_id;
            $b = (int) $match->black_player_id;
            $oppId = $playerId === $w ? $b : $w;
            $isWhite = $playerId === $w;

            $pts = $resultsVsOpponent[$playerId][$oppId] ?? 0.0;
            $opp = $playersById[$oppId] ?? null;
            if (!$opp) {
                continue;
            }

            $games[] = [
                'opponent_id' => $oppId,
                'opponent_rating' => (int) ($opp->rating ?? 0),
                'opponent_title' => isset($opp->title) && $opp->title !== '' ? (string) $opp->title : '',
                'opponent_country' => isset($opp->country) && $opp->country !== '' ? (string) $opp->country : '',
                'player_pts' => $pts,
            ];
        }

        return $games;
    }

    /**
     * Get norm label for display (e.g. "GM norm", "IM norm").
     */
    public static function get_norm_label(string $norm): string
    {
        if ($norm === 'gm_norm') {
            return __('GM norm', 'chess-podium');
        }
        if ($norm === 'im_norm') {
            return __('IM norm', 'chess-podium');
        }
        return '';
    }

    /**
     * Return only norms that are meaningful for the player's current title.
     *
     * Example:
     * - GM title -> hide GM/IM norms
     * - IM title -> hide IM norm, keep GM norm if present
     *
     * @param array<int, string> $norms
     * @param string|null $playerTitle
     * @return array<int, string>
     */
    public static function filter_displayable_norms(array $norms, ?string $playerTitle): array
    {
        $filtered = [];
        foreach ($norms as $norm) {
            if (self::is_norm_relevant_for_title((string) $norm, $playerTitle)) {
                $filtered[] = (string) $norm;
            }
        }
        return $filtered;
    }

    /**
     * True when the norm is still useful to display for the given title.
     */
    public static function is_norm_relevant_for_title(string $norm, ?string $playerTitle): bool
    {
        $rank = self::title_rank($playerTitle);
        if ($norm === 'gm_norm') {
            return $rank < 3; // Hide for GM and higher.
        }
        if ($norm === 'im_norm') {
            return $rank < 2; // Hide for IM and GM.
        }
        return true;
    }

    /**
     * Title hierarchy used for norm-visibility checks.
     */
    private static function title_rank(?string $title): int
    {
        $t = strtoupper(trim((string) $title));
        if ($t === 'GM') {
            return 3;
        }
        if ($t === 'IM') {
            return 2;
        }
        if ($t !== '') {
            return 1;
        }
        return 0;
    }
}
