"""
FIDE Swiss — упрощённый алгоритм на основе Folding с перебором offset.
Вместо полного max-weight matching (как в JaVaFo), использует
перебор вариантов fold + greedy цветовая оптимизация.

Улучшения:
- Учёт предыдущих bye (не даём bye повторно)
- Downfloaters паруются с верхом следующей группы очков
- Абсолютное цветовое предпочтение (|balance| >= 2) — жёсткое правило
- Сортировка по очкам + рейтингу для корректного fold
"""

from collections import defaultdict
from typing import Optional, List, Tuple


class Player:
    def __init__(self, sno: int, name: str, rating: int, fed: str = '',
                 points: float = 0.0, results: list = None):
        self.sno = sno
        self.name = name
        self.rating = rating
        self.fed = fed
        self.points = points
        self.results = results or []
        self.opponents = [r['opponent'] for r in self.results]
        self.colors = [r['color'] for r in self.results]
        self.tb = []

    @property
    def white_count(self): return sum(1 for c in self.colors if c == 'w')
    @property
    def black_count(self): return sum(1 for c in self.colors if c == 'b')
    @property
    def color_balance(self): return self.white_count - self.black_count
    @property
    def last_color(self): return self.colors[-1] if self.colors else None
    @property
    def had_bye(self):
        return any(r.get('opponent', 0) == 0 for r in self.results)

    def preferred_color(self) -> str:
        if not self.colors:
            return 'w'
        bal = self.color_balance
        if bal >= 2: return 'b'
        if bal <= -2: return 'w'
        if bal == 1: return 'b'
        if bal == -1: return 'w'
        return 'b' if self.last_color == 'w' else 'w'

    def color_force(self) -> int:
        bal = abs(self.color_balance)
        if bal >= 2: return 2
        if bal == 1: return 1
        return 0

    def has_played(self, opponent_sno: int) -> bool:
        return opponent_sno in self.opponents

    def __repr__(self):
        return f"#{self.sno} {self.name} ({self.points}pts, R{self.rating})"


def sort_key(player: Player) -> tuple:
    return (-player.points, -player.rating)


def compute_tiebreakers(players: list, rounds: int) -> None:
    pts_map = {p.sno: p.points for p in players}
    for p in players:
        p.tb = [round(sum(pts_map.get(o, 0) for o in p.opponents), 1)]


# ═══════════════════════════════════
# ЦВЕТА
# ═══════════════════════════════════

def _assign_colors(p1: Player, p2: Player) -> str:
    """Return 'w' if p1 gets white, 'b' if p1 gets black.

    FIDE colour allocation rules (C04 Annex E.5):
    1. Absolute preference (|bal| >= 2) MUST be satisfied
    2. Strong preference (|bal| == 1) SHOULD be satisfied
    3. When both have same absolute preference, higher rated gets it
    4. When no absolute conflict, alternate from last round
    """
    force1 = p1.color_force()
    force2 = p2.color_force()
    pref1 = p1.preferred_color()
    pref2 = p2.preferred_color()

    # Rule 1: absolute colour preference is mandatory
    if force1 == 2 and force2 < 2:
        return pref1
    if force2 == 2 and force1 < 2:
        return 'b' if pref2 == 'w' else 'w'

    # Both have absolute preference
    if force1 == 2 and force2 == 2:
        if pref1 != pref2:
            return pref1  # both satisfied
        # Both want same colour — higher rated gets preference
        if p1.rating >= p2.rating:
            return pref1
        else:
            return 'b' if pref1 == 'w' else 'w'

    # No absolute preferences — use strong preference, then rating
    if force1 > force2:
        return pref1
    if force2 > force1:
        return 'b' if pref2 == 'w' else 'w'

    # Equal force (0 or 1), maybe same or different preferences
    if pref1 != pref2:
        return pref1

    # Same preferences — higher rated decides
    if p1.rating >= p2.rating:
        return pref1
    return 'b' if pref1 == 'w' else 'w'


def _color_score(p1: Player, p2: Player, p1_color: str) -> int:
    """Score colour quality for the pair. Higher is better."""
    score = 0
    p1_has_pref = (p1_color == p1.preferred_color())
    p2_has_pref = (('b' if p1_color == 'w' else 'w') == p2.preferred_color())

    if p1_has_pref:
        score += 3 if p1.color_force() >= 2 else 2
    else:
        score -= 5 if p1.color_force() >= 2 else 0

    if p2_has_pref:
        score += 3 if p2.color_force() >= 2 else 2
    else:
        score -= 5 if p2.color_force() >= 2 else 0

    return score


# ═══════════════════════════════════
# ПАРИРОВАНИЕ BRACKET — ПЕРЕБОР OFFSET
# ═══════════════════════════════════

def _pair_bracket_fold_search(
    players: List[Player],
    all_paired: set,
) -> Tuple[List[Tuple[Player, Player]], List[Player]]:
    """Fold pairing with offset search and floater candidates.

    For a bracket of size N:
    1. If N odd — try each player as a downfloater
    2. For the remaining M (even) — try fold offsets 0..M/2-1
    3. Select combination with best colour score

    Sort is by (-points, -rating) so downfloaters (higher score)
    naturally end up in S1, pairing with top of S2.
    """
    available = [p for p in players if p.sno not in all_paired]
    n = len(available)

    if n < 2:
        return [], list(available)

    available.sort(key=lambda p: (-p.points, -p.rating))

    best_pairs = []
    best_floaters = list(available[-1:]) if n % 2 == 1 else []
    best_score = -9999

    # Floater candidates
    floater_candidates = [None]
    if n % 2 == 1:
        floater_candidates = range(n)

    for fi in floater_candidates:
        if fi is not None:
            floater = available[fi]
            rest = available[:fi] + available[fi + 1:]
        else:
            floater = None
            rest = available

        m = len(rest)

        # Try fold with different offsets
        for offset in range(m // 2):
            pairs = []
            ok = True
            used = set()

            for i in range(m // 2):
                a = rest[i]
                b = rest[m // 2 + ((i + offset) % (m // 2))]

                if a.has_played(b.sno):
                    ok = False
                    break
                if a.sno in used or b.sno in used:
                    ok = False
                    break

                color = _assign_colors(a, b)
                if color == 'w':
                    pairs.append((a, b))
                else:
                    pairs.append((b, a))
                used.add(a.sno)
                used.add(b.sno)

            if not ok or len(pairs) < m // 2:
                continue

            # Score colour quality
            color_score = sum(_color_score(wp, bp, 'w') for wp, bp in pairs)

            if color_score > best_score:
                best_score = color_score
                best_pairs = pairs
                best_floaters = [floater] if floater else []

    if not best_pairs and n % 2 == 1:
        # Fallback: float the last player
        floater = available[-1]
        rest = available[:-1]
        m = len(rest)
        best_pairs = []
        best_floaters = [floater]
        for i in range(m // 2):
            a, b = rest[i], rest[m // 2 + i]
            color = _assign_colors(a, b)
            if color == 'w':
                best_pairs.append((a, b))
            else:
                best_pairs.append((b, a))

    if not best_pairs:
        # Desperate fallback: force-pair even if already played (no other option)
        # This can happen when only 2 players in a score group have met before
        rest = list(available)
        m = len(rest)
        if m >= 2:
            best_pairs = []
            best_floaters = []
            if m % 2 == 1:
                best_floaters = [rest[-1]]
                rest = rest[:-1]
                m = len(rest)
            for i in range(m // 2):
                a, b = rest[i], rest[m // 2 + i]
                color = _assign_colors(a, b)
                if color == 'w':
                    best_pairs.append((a, b))
                else:
                    best_pairs.append((b, a))

    for wp, bp in best_pairs:
        all_paired.add(wp.sno)
        all_paired.add(bp.sno)

    return best_pairs, best_floaters


# ═══════════════════════════════════
# ОСНОВНОЙ АЛГОРИТМ
# ═══════════════════════════════════

def fide_swiss_pairing(players: list, current_round: int) -> list:
    """FIDE Swiss pairings with fold + offset search.

    Players sorted by (-points, -rating).
    Score brackets processed from highest to lowest.
    Downfloaters paired with the top of the lower bracket.
    Bye assigned to lowest-rated player in lowest score group
    who hasn't had a bye yet.
    """
    sorted_players = sorted(players, key=sort_key)

    # Bye — assign to eligible player in lowest score group
    if len(sorted_players) % 2 == 1:
        groups = defaultdict(list)
        for p in sorted_players:
            groups[p.points].append(p)
        # Lowest score group, by rating ascending, excluding previous bye receivers
        lowest_group = groups[min(groups.keys())]
        lowest_group.sort(key=lambda p: (p.had_bye, p.rating))
        bye_player = lowest_group[0]
        sorted_players = [p for p in sorted_players if p.sno != bye_player.sno]

    # Create score brackets
    brackets = []
    i = 0
    while i < len(sorted_players):
        score = sorted_players[i].points
        group = []
        while i < len(sorted_players) and sorted_players[i].points == score:
            group.append(sorted_players[i])
            i += 1
        brackets.append(group)

    all_pairs = []
    all_paired = set()
    downfloaters = []

    for group in brackets:
        # Available players in this bracket
        bracket_avail = [p for p in group if p.sno not in all_paired]

        # Downfloaters from above (have more points than this bracket)
        floaters = [df for df in downfloaters if df.sno not in all_paired]

        if len(bracket_avail) + len(floaters) < 2:
            downfloaters = bracket_avail + floaters
            continue

        # Priority: pair each downfloater with a bracket member
        # (downfloaters get paired with top-rated bracket members)
        bracket_avail.sort(key=lambda p: -p.rating)
        floaters.sort(key=lambda p: -p.rating)

        allocated = set()
        for floater in floaters:
            best_idx = None
            best_score = -999
            for j, bp in enumerate(bracket_avail):
                if bp.sno in allocated:
                    continue
                if floater.has_played(bp.sno):
                    continue
                color = _assign_colors(floater, bp)
                score = _color_score(floater, bp, color)
                if score > best_score:
                    best_score = score
                    best_idx = j
            if best_idx is not None:
                bp = bracket_avail[best_idx]
                color = _assign_colors(floater, bp)
                if color == 'w':
                    all_pairs.append((floater, bp))
                else:
                    all_pairs.append((bp, floater))
                all_paired.add(floater.sno)
                all_paired.add(bp.sno)
                allocated.add(bp.sno)
                downfloaters = [df for df in downfloaters if df.sno != floater.sno]

        # Remaining bracket members — pair among themselves
        remaining = [p for p in bracket_avail if p.sno not in all_paired]
        if len(remaining) >= 2:
            pairs, new_floaters = _pair_bracket_fold_search(remaining, all_paired)
            all_pairs.extend(pairs)
            downfloaters = new_floaters + [df for df in downfloaters if df.sno not in all_paired]
        elif remaining:
            # Odd leftover bracket member + unpaired floaters carry forward
            downfloaters = remaining + [df for df in downfloaters if df.sno not in all_paired]

    # Final pass: pair any remaining unpaired players (bottom of the bracket chain)
    unpaired = [df for df in downfloaters if df.sno not in all_paired]
    if unpaired:
        unpaired.sort(key=lambda p: (-p.points, -p.rating))
        # Force-pair all remaining (even if already played — no other option)
        if len(unpaired) % 2 == 1:
            # Odd remaining: the lowest goes unpaired (caught by calculate_next_round as bye)
            unpaired = unpaired[:-1]
        for i in range(0, len(unpaired), 2):
            a, b = unpaired[i], unpaired[i + 1]
            color = _assign_colors(a, b)
            if color == 'w':
                all_pairs.append((a, b))
            else:
                all_pairs.append((b, a))

    return all_pairs


def swiss_pairing(players: list, current_round: int) -> list:
    return fide_swiss_pairing(players, current_round)


def calculate_next_round(tournament_data: dict) -> dict:
    standings = tournament_data['standings']
    current_round = tournament_data['current_round']
    next_round = current_round + 1

    player_map = {}
    sno_to_player = {s.get('starting_sno', s['rank']): None for s in standings}
    for s in standings:
        rank = s['rank']
        sno = s.get('starting_sno', rank)
        p = Player(
            sno=sno,
            name=s['name'], rating=s.get('rating', 0),
            fed=s['fed'], points=s['points'], results=s['results'])
        p.rank = rank
        p.tb = s.get('tb', [])
        player_map[rank] = p
        sno_to_player[sno] = p

    # Try bbpPairings first (FIDE 2025 Dutch System engine)
    bbp_error = None
    try:
        from .trf_generator import generate_trf
        from .bbp_wrapper import call_bbp

        trf = generate_trf(
            tournament_data, next_round,
            name=tournament_data.get('name', 'Chess Tournament'),
            use_rank=False,
            initial_color_white=False,
        )
        bbp_pairs, _ = call_bbp(trf)

        if bbp_pairs:
            pairings = []
            for w_sno, b_sno in bbp_pairs:
                wp = sno_to_player.get(w_sno)
                if wp is None:
                    continue
                if b_sno == 0:
                    pairings.append((wp, wp, 'bye'))
                else:
                    bp = sno_to_player.get(b_sno)
                    if bp is None:
                        continue
                    pairings.append((wp, bp, 'w'))
            if pairings:
                return {
                    'round': next_round,
                    'pairings': pairings,
                    'players': player_map,
                    'source': 'bbp_pairings_fide_2025',
                }
    except Exception as e:
        bbp_error = str(e)

    if bbp_error:
        import sys
        print(f'⚠️  bbpPairings не сработал ({bbp_error}), использую упрощённый Swiss', file=sys.stderr)

    # Fallback: simplified Swiss algorithm
    raw = fide_swiss_pairing(list(player_map.values()), current_round)
    pairings = [(wp, bp, 'w') for wp, bp in raw]

    # Check for missing players (bye from odd count not returned in raw pairs)
    all_snos = set()
    for wp, bp in raw:
        all_snos.add(wp.sno)
        all_snos.add(bp.sno)
    for p in player_map.values():
        if p.sno not in all_snos:
            pairings.append((p, p, 'bye'))

    return {'round': next_round,
            'pairings': pairings,
            'players': player_map, 'source': 'swiss_algorithm_simplified'}
