# backend/app/logic.py
import heapq
from collections import defaultdict
from datetime import datetime, timedelta
from uuid import uuid4

from sqlalchemy.orm import Session
from sqlalchemy.orm.attributes import flag_modified

from . import models
from .constants import BracketTypes, MatchStatus, TournamentTypes
from .core.brackets import BracketGenerator
from .core.structures import Match


def generate_bracket(db: Session, t: models.Tournament):
    teams = t.teams
    if not teams:
        return

    gen = BracketGenerator()
    is_double = t.type == TournamentTypes.DOUBLE
    abstract_matches = gen.generate(len(teams), double_elimination=is_double)
    id_map = {m.id: str(uuid4()) for m in abstract_matches}

    def resolve_target(match_node: Match, is_winner_path):
        if is_winner_path:
            curr = match_node.next_win
            slot = getattr(match_node, "next_win_slot", None)
        else:
            curr = match_node.next_loss
            slot = getattr(match_node, "next_loss_slot", None)

        while curr and getattr(curr, "is_bye", False):
            slot = getattr(curr, "next_win_slot", None)
            curr = curr.next_win

        return curr, slot

    db_matches = []
    friendly_counter = 1

    for m in abstract_matches:
        real_win, win_slot = resolve_target(m, True)
        real_loss, loss_slot = resolve_target(m, False)

        initial_status = MatchStatus.SCHEDULED
        p1_id = None
        p2_id = None

        p1_seed = m.teams[0]
        p2_seed = m.teams[1]

        if p1_seed and p1_seed <= len(teams):
            p1_id = teams[p1_seed - 1].id
        if p2_seed and p2_seed <= len(teams):
            p2_id = teams[p2_seed - 1].id

        if p1_id and p2_id:
            initial_status = MatchStatus.PENDING

        new_match = models.Match(
            id=id_map[m.id],
            tournament_id=t.id,
            match_number=friendly_counter,
            bracket_type=m.bracket_type,
            round_number=m.round_number,
            status=initial_status,
            p1_team_id=p1_id,
            p2_team_id=p2_id,
            winner_next_match_id=id_map[real_win.id] if real_win else None,
            winner_next_match_slot=win_slot if real_win else None,
            loser_next_match_id=id_map[real_loss.id] if real_loss else None,
            loser_next_match_slot=loss_slot if real_loss else None,
        )
        db_matches.append(new_match)
        friendly_counter += 1

    db.add_all(db_matches)
    db.flush()


def update_schedule_times(db: Session, t: models.Tournament):
    if not t.timestamp:
        return

    # 1. Grab the entire day of tournaments
    target_date = t.timestamp.date()
    start_of_day = datetime.combine(target_date, datetime.min.time())
    end_of_day = start_of_day + timedelta(days=1)

    tournaments = (
        db.query(models.Tournament)
        .filter(
            models.Tournament.timestamp >= start_of_day,
            models.Tournament.timestamp < end_of_day,
        )
        .with_for_update()
        .all()
    )

    if not tournaments:
        return

    all_matches: list[models.Match] = []
    match_map: dict[str, models.Match] = {}
    prereqs = defaultdict(list)
    depth_cache = {}

    # 2. Gather all matches and prereqs globally
    for t_item in tournaments:
        for m in t_item.matches:
            all_matches.append(m)
            match_map[m.id] = m
            if m.winner_next_match_id:
                prereqs[m.winner_next_match_id].append(m.id)
            if m.loser_next_match_id:
                prereqs[m.loser_next_match_id].append(m.id)

    # 3. Calculate depths
    def get_depth(m_id):
        if m_id not in match_map:
            return 0
        if m_id in depth_cache:
            return depth_cache[m_id]
        m = match_map[m_id]
        child_depths = [0]
        if m.winner_next_match_id:
            child_depths.append(get_depth(m.winner_next_match_id))
        if m.loser_next_match_id:
            child_depths.append(get_depth(m.loser_next_match_id))
        depth = 1 + max(child_depths)
        depth_cache[m_id] = depth
        return depth

    for m in all_matches:
        get_depth(m.id)

    # 4. Initialize timers
    all_courts: set[models.Court] = set()
    court_sharing_count = defaultdict(int)

    for t_item in tournaments:
        for c in t_item.courts:
            all_courts.add(c)
            court_sharing_count[c.id] += 1

    court_timers = {c.id: start_of_day for c in all_courts}
    planned_finish_times = {}
    unscheduled: list[models.Match] = []

    for m in all_matches:
        if m.status == MatchStatus.FINISHED and m.start_time and m.court_id:
            fin = m.start_time + timedelta(minutes=m.tournament.duration)
            planned_finish_times[m.id] = fin
            if court_timers.get(m.court_id, start_of_day) < fin:
                court_timers[m.court_id] = fin
        else:
            unscheduled.append(m)

    tournament_match_counts = {t_item.id: 0 for t_item in tournaments}

    # 5. Critical Path Time-Stepping Scheduler
    loop_limit = max(5000, len(unscheduled) * 50)

    while unscheduled and loop_limit > 0:
        loop_limit -= 1

        best_court_id = min(
            court_timers.keys(), key=lambda k: (court_timers[k], court_sharing_count[k])
        )
        current_time = court_timers[best_court_id]

        ready: list[models.Match] = []
        for m in unscheduled:
            if best_court_id not in [c.id for c in m.tournament.courts]:
                continue
            is_ready = True
            max_prereq_time = (
                m.tournament.timestamp.replace(tzinfo=None)
                if m.tournament.timestamp.tzinfo
                else m.tournament.timestamp
            )

            for p_id in prereqs[m.id]:
                if p_id not in planned_finish_times:
                    is_ready = False
                    break
                max_prereq_time = max(max_prereq_time, planned_finish_times[p_id])

            if is_ready and max_prereq_time <= current_time:
                ready.append(m)

        if ready:
            ready.sort(
                key=lambda x: (
                    len(x.tournament.courts),
                    tournament_match_counts[x.tournament_id],
                    -depth_cache[x.id],
                    x.round_number,
                    x.match_number,
                )
            )
            cand = ready[0]

            cand.court_id = best_court_id
            cand.start_time = current_time

            fin = current_time + timedelta(minutes=cand.tournament.duration)
            planned_finish_times[cand.id] = fin
            court_timers[best_court_id] = fin
            tournament_match_counts[cand.tournament_id] += 1
            unscheduled.remove(cand)
        else:
            next_wake = None
            for m in unscheduled:
                if best_court_id not in [c.id for c in m.tournament.courts]:
                    continue
                is_ready = True
                max_prereq_time = (
                    m.tournament.timestamp.replace(tzinfo=None)
                    if m.tournament.timestamp.tzinfo
                    else m.tournament.timestamp
                )
                for p_id in prereqs[m.id]:
                    if p_id not in planned_finish_times:
                        is_ready = False
                        break
                    max_prereq_time = max(max_prereq_time, planned_finish_times[p_id])

                if is_ready and max_prereq_time > current_time:
                    if next_wake is None or max_prereq_time < next_wake:
                        next_wake = max_prereq_time

            if next_wake:
                court_timers[best_court_id] = next_wake
            else:
                court_timers[best_court_id] += timedelta(minutes=5)

    # 6. Ultra-Fast Smart Referee Assignment
    all_matches.sort(key=lambda x: (x.start_time or start_of_day, x.court_id or 0))
    duty_counts = defaultdict(int)

    team_first_match = {}
    for m in all_matches:
        if m.start_time:
            if m.p1_team_id and m.p1_team_id not in team_first_match:
                team_first_match[m.p1_team_id] = m.start_time
            if m.p2_team_id and m.p2_team_id not in team_first_match:
                team_first_match[m.p2_team_id] = m.start_time

    active_refs = []
    match_outcome_next_start = {}

    for m in all_matches:
        if m.winner_next_match_id:
            nm = match_map.get(m.winner_next_match_id)
            if nm and nm.start_time:
                match_outcome_next_start[("W", m.id)] = nm.start_time
        if m.loser_next_match_id:
            nm = match_map.get(m.loser_next_match_id)
            if nm and nm.start_time:
                match_outcome_next_start[("L", m.id)] = nm.start_time

    def is_ref_busy(identifier, start, end):
        for ref_id, r_start, r_end in active_refs:
            if ref_id == identifier and start < r_end and end > r_start:
                return True
        return False

    for m in all_matches:
        if not m.start_time:
            continue
        m_start = m.start_time
        m_end = m_start + timedelta(minutes=m.tournament.duration)

        if m.bracket_type == BracketTypes.FINALS:
            prev_final = next(
                (
                    p
                    for p in all_matches
                    if p.bracket_type == BracketTypes.FINALS
                    and p.winner_next_match_id == m.id
                ),
                None,
            )
            if prev_final:
                m.ref_team_id = prev_final.ref_team_id
                m.ref_label = prev_final.ref_label
                identifier = (
                    f"TEAM_{m.ref_team_id}" if m.ref_team_id else prev_final.ref_label
                )
                active_refs.append((identifier, m_start, m_end))
                continue

        best_outcome = None
        best_score = float("inf")

        for prev_m in all_matches:
            if prev_m.tournament_id != m.tournament_id:
                continue
            if not prev_m.start_time or prev_m.start_time >= m_start:
                continue
            prev_end = prev_m.start_time + timedelta(minutes=prev_m.tournament.duration)
            if prev_end > m_start:
                continue

            for outcome in ["W", "L"]:
                out_key = (outcome, prev_m.id)
                next_start = match_outcome_next_start.get(out_key)
                if next_start and next_start < m_end:
                    continue
                identifier = f"{outcome}:{prev_m.id}"
                if is_ref_busy(identifier, m_start, m_end):
                    continue

                wait_mins = (m_start - prev_end).total_seconds() / 60.0
                score = (duty_counts[identifier] * 120) + wait_mins
                if outcome == "W":
                    score += 60
                if prev_m.court_id == m.court_id:
                    score -= 30

                if score < best_score:
                    best_score = score
                    best_outcome = identifier

        if best_outcome:
            m.ref_label = best_outcome
            m.ref_team_id = None
            duty_counts[best_outcome] += 1
            active_refs.append((best_outcome, m_start, m_end))
        else:
            assigned_bye_team = False
            sorted_teams = sorted(
                m.tournament.teams, key=lambda t: duty_counts[f"TEAM_{t.id}"]
            )

            for team in sorted_teams:
                t_id = team.id
                t_first_start = team_first_match.get(t_id)
                identifier = f"TEAM_{t_id}"
                if t_first_start and t_first_start >= m_end:
                    if not is_ref_busy(identifier, m_start, m_end):
                        m.ref_team_id = t_id
                        m.ref_label = None
                        duty_counts[identifier] += 1
                        active_refs.append((identifier, m_start, m_end))
                        assigned_bye_team = True
                        break

            if not assigned_bye_team:
                m.ref_label = "Staff / Volunteers"
                m.ref_team_id = None

    db.commit()


def advance_winner(db: Session, match: models.Match, winner_id: int):
    if not winner_id:
        return

    loser_id = match.p1_team_id if match.p1_team_id != winner_id else match.p2_team_id

    for m in match.tournament.matches:
        if m.ref_label == f"L:{match.id}":
            m.ref_team_id = loser_id
            db.add(m)
        elif m.ref_label == f"W:{match.id}":
            m.ref_team_id = winner_id
            db.add(m)

    if (
        match.bracket_type == BracketTypes.FINALS
        and match.winner_next_match
        and match.winner_next_match.bracket_type == BracketTypes.FINALS
    ):
        if winner_id == match.p1_team_id:
            reset_match = match.winner_next_match
            if reset_match.winner_team_id:
                undo_advancement(db, reset_match)

            reset_match.winner_team_id = None
            reset_match.sets = []
            flag_modified(reset_match, "sets")

            reset_match.p1_team_id = None
            reset_match.p2_team_id = None
            reset_match.status = MatchStatus.SCHEDULED

            db.add(reset_match)
            db.commit()
            return

    def update_next_match(
        next_match: models.Match, team_id: int, target_slot: int | None
    ):
        if not next_match or target_slot is None:
            return
        current_team = (
            next_match.p1_team_id if target_slot == 0 else next_match.p2_team_id
        )

        if current_team and current_team != team_id:
            if next_match.winner_team_id:
                undo_advancement(db, next_match)
            next_match.winner_team_id = None
            next_match.sets = []
            flag_modified(next_match, "sets")
            next_match.status = MatchStatus.SCHEDULED

        if target_slot == 0:
            next_match.p1_team_id = team_id
        elif target_slot == 1:
            next_match.p2_team_id = team_id

        if (
            next_match.p1_team_id
            and next_match.p2_team_id
            and next_match.status == MatchStatus.SCHEDULED
        ):
            next_match.status = MatchStatus.PENDING

        db.add(next_match)

    update_next_match(match.winner_next_match, winner_id, match.winner_next_match_slot)
    if match.loser_next_match and loser_id:
        update_next_match(match.loser_next_match, loser_id, match.loser_next_match_slot)

    db.commit()


def undo_advancement(db: Session, match: models.Match):
    if not match.winner_team_id:
        return

    for m in match.tournament.matches:
        if m.ref_label in [f"L:{match.id}", f"W:{match.id}"]:
            m.ref_team_id = None
            db.add(m)

    winner_id = match.winner_team_id
    loser_id = match.p1_team_id if match.p1_team_id != winner_id else match.p2_team_id

    def clear_from_next(next_match: models.Match, target_slot: int | None):
        if not next_match or target_slot is None:
            return

        if next_match.winner_team_id:
            undo_advancement(db, next_match)

        next_match.winner_team_id = None
        next_match.sets = []
        flag_modified(next_match, "sets")

        if target_slot == 0:
            next_match.p1_team_id = None
        elif target_slot == 1:
            next_match.p2_team_id = None

        next_match.status = MatchStatus.SCHEDULED

        db.add(next_match)

    clear_from_next(match.winner_next_match, match.winner_next_match_slot)
    if match.loser_next_match and loser_id:
        clear_from_next(match.loser_next_match, match.loser_next_match_slot)

    db.commit()
