# backend/app/core/brackets.py

import math
from ..constants import BracketTypes
from .structures import Match


class BracketGenerator:
    def __init__(self):
        self.matches_dict = {}
        self.match_counter = 1

    def generate(
        self, num_players: int, double_elimination: bool = False
    ) -> list[Match]:
        self.matches_dict = {}
        self.match_counter = 1

        size = 2 ** math.ceil(math.log2(num_players))
        seeds = self._generate_seeding_indices(num_players)

        # Create Winners Bracket
        bracket = self._create_tree_bracket(size)

        for i, match in enumerate(bracket[0]):
            match.teams = [seeds[i * 2], seeds[i * 2 + 1]]

        lb_rounds = []
        if double_elimination:
            lb_rounds = self._create_strict_lb(size, bracket)
            self._create_de_finals(bracket, lb_rounds)
        else:
            self._create_se_third_place(bracket)

        # Propagate ghosts (this marks matches as is_bye=True)
        self._resolve_byes(num_players)

        all_matches = sorted(self.matches_dict.values(), key=lambda x: x.id)
        # Filter out byes so they don't become DB rows
        return [m for m in all_matches if not m.is_bye]

    def _generate_seeding_indices(self, num_players: int) -> list[int]:
        p2 = 2 ** math.ceil(math.log2(num_players))
        seeds = [1]
        curr = 1
        while curr < p2:
            seeds = [x for val in seeds for x in (val, (curr * 2) + 1 - val)]
            curr *= 2
        return seeds

    def _create_tree_bracket(self, size: int) -> list[list[Match]]:
        rounds: list[list[Match]] = []
        current_size = size // 2
        r_num = 1

        while current_size >= 1:
            round_matches: list[Match] = []
            for _ in range(current_size):
                m = Match(self.match_counter, BracketTypes.WB, r_num)
                self.matches_dict[m.id] = m
                round_matches.append(m)
                self.match_counter += 1
            rounds.append(round_matches)
            current_size //= 2
            r_num += 1

        for r in range(len(rounds) - 1):
            for i, match in enumerate(rounds[r]):
                match.next_win = rounds[r + 1][i // 2]
                match.next_win_slot = i % 2

        return rounds

    def _create_strict_lb(
        self, size: int, wb_rounds: list[list[Match]]
    ) -> list[list[Match]]:
        lb_rounds: list[list[Match]] = []
        num_wb_rounds = len(wb_rounds)
        total_lb_rounds = (num_wb_rounds - 1) * 2

        current_lb_size = size // 4
        if current_lb_size < 1:
            current_lb_size = 1

        for r in range(1, total_lb_rounds + 1):
            if r > 2 and r % 2 != 0:
                current_lb_size //= 2

            round_matches = []
            for _ in range(current_lb_size):
                m = Match(self.match_counter, BracketTypes.LB, r)
                self.matches_dict[m.id] = m
                round_matches.append(m)
                self.match_counter += 1
            lb_rounds.append(round_matches)

        # Link LB rounds
        for r in range(len(lb_rounds) - 1):
            curr = lb_rounds[r]
            next_r = lb_rounds[r + 1]
            for i, match in enumerate(curr):
                if len(curr) == len(next_r):
                    # 1-to-1 Mapping case (e.g. 4 matches -> 4 matches)
                    match.next_win = next_r[i]
                    # FIX: Always use slot 1 (Slot 0 is taken by WB Drop-down)
                    match.next_win_slot = 1
                else:
                    # 2-to-1 Mapping case (e.g. 4 matches -> 2 matches)
                    match.next_win = next_r[i // 2]
                    match.next_win_slot = i % 2

        # Link WB Losers -> LB
        for r in range(num_wb_rounds):
            wb_round = wb_rounds[r]
            lb_target_idx = 0 if r == 0 else (r * 2) - 1

            if lb_target_idx >= len(lb_rounds):
                break

            lb_round = lb_rounds[lb_target_idx]
            for i, wb_match in enumerate(wb_round):
                if r == 0:
                    target = lb_round[i // 2]
                    wb_match.next_loss_slot = i % 2
                else:
                    # Drop-in round
                    target = lb_round[i] if i < len(lb_round) else lb_round[0]
                    # Drop-ins take Slot 0
                    wb_match.next_loss_slot = 0
                wb_match.next_loss = target

        return lb_rounds

    def _create_se_third_place(self, wb_rounds: list[list[Match]]) -> None:
        if len(wb_rounds) < 2:
            return
        semis = wb_rounds[-2]
        final_round_idx = wb_rounds[-1][0].round_number
        third_place_match = Match(
            self.match_counter, BracketTypes.FINALS, final_round_idx
        )
        self.matches_dict[third_place_match.id] = third_place_match
        self.match_counter += 1

        for i, sm in enumerate(semis):
            sm.next_loss = third_place_match
            sm.next_loss_slot = i % 2

    def _create_de_finals(
        self, wb_rounds: list[list[Match]], lb_rounds: list[list[Match]]
    ) -> None:
        wb_final = wb_rounds[-1][0]
        final_round_idx = wb_final.round_number + 1

        gf = Match(self.match_counter, BracketTypes.FINALS, final_round_idx)
        self.matches_dict[gf.id] = gf
        self.match_counter += 1

        wb_final.next_win = gf
        wb_final.next_win_slot = 0

        if lb_rounds:
            lb_rounds[-1][0].next_win = gf
            lb_rounds[-1][0].next_win_slot = 1

            reset = Match(self.match_counter, BracketTypes.FINALS, final_round_idx + 1)
            self.matches_dict[reset.id] = reset
            self.match_counter += 1
            gf.next_loss = reset
            gf.next_loss_slot = 0
            gf.next_win = reset
            gf.next_win_slot = 1

    def _resolve_byes(self, num_players: int) -> None:
        """
        Iterates through all matches to identify 'Ghost' players (seeds > num_players).
        If a match has a ghost, it is marked as a Bye, and the real player is
        automatically pushed to the next match.
        """
        all_matches: list[Match] = sorted(
            self.matches_dict.values(), key=lambda x: x.id
        )

        for m in all_matches:
            if m.is_bye:
                continue

            p1, p2 = m.teams

            p1_is_ghost = p1 is not None and p1 > num_players
            p2_is_ghost = p2 is not None and p2 > num_players

            if p1_is_ghost or p2_is_ghost:
                m.is_bye = True

                winner = None
                loser = None  # Ghost

                if p1_is_ghost and p2_is_ghost:
                    winner = p1
                    loser = p2
                elif p1_is_ghost:
                    winner = p2
                    loser = p1
                elif p2_is_ghost:
                    winner = p1
                    loser = p2

                # Propagate Winner (Advance to next round)
                if m.next_win:
                    m.next_win.teams[m.next_win_slot] = winner

                # Propagate Loser (Drop to LB)
                if m.next_loss:
                    m.next_loss.teams[m.next_loss_slot] = loser
