//
//  MatchScheduler.swift
//  PadelClub
//
//  Created by Razmig Sarkissian on 08/04/2024.
//

import Foundation
import LeStorage
import SwiftUI

@Observable
final class MatchScheduler: BaseMatchScheduler, SideStorable {
//    
//    init(tournament: String,
//         timeDifferenceLimit: Int = 5,
//         loserBracketRotationDifference: Int = 0,
//         upperBracketRotationDifference: Int = 1,
//         accountUpperBracketBreakTime: Bool = true,
//         accountLoserBracketBreakTime: Bool = false,
//         randomizeCourts: Bool = true,
//         rotationDifferenceIsImportant: Bool = false,
//         shouldHandleUpperRoundSlice: Bool = false,
//         shouldEndRoundBeforeStartingNext: Bool = true,
//<<<<<<< HEAD
//         groupStageChunkCount: Int? = nil, overrideCourtsUnavailability: Bool = false, shouldTryToFillUpCourtsAvailable: Bool = false) {
//        super.init()
//=======
//         groupStageChunkCount: Int? = nil,
//         overrideCourtsUnavailability: Bool = false,
//         shouldTryToFillUpCourtsAvailable: Bool = true,
//         courtsAvailable: Set<Int> = Set<Int>(),
//         simultaneousStart: Bool = true) {
//>>>>>>> main
//        self.tournament = tournament
//        self.timeDifferenceLimit = timeDifferenceLimit
//        self.loserBracketRotationDifference = loserBracketRotationDifference
//        self.upperBracketRotationDifference = upperBracketRotationDifference
//        self.accountUpperBracketBreakTime = accountUpperBracketBreakTime
//        self.accountLoserBracketBreakTime = accountLoserBracketBreakTime
//        self.randomizeCourts = randomizeCourts
//        self.rotationDifferenceIsImportant = rotationDifferenceIsImportant
//        self.shouldHandleUpperRoundSlice = shouldHandleUpperRoundSlice
//        self.shouldEndRoundBeforeStartingNext = shouldEndRoundBeforeStartingNext
//        self.groupStageChunkCount = groupStageChunkCount
//        self.overrideCourtsUnavailability = overrideCourtsUnavailability
//        self.shouldTryToFillUpCourtsAvailable = shouldTryToFillUpCourtsAvailable
//        self.courtsAvailable = courtsAvailable
//        self.simultaneousStart = simultaneousStart
//    }
    
    var courtsUnavailability: [DateInterval]? {
        guard let event = tournamentObject()?.eventObject() else { return nil }
        return event.courtsUnavailability + (overrideCourtsUnavailability ? [] : event.tournamentsCourtsUsed(exluding: tournament))
    }
    
    var additionalEstimationDuration : Int {
        return tournamentObject()?.additionalEstimationDuration ?? 0
    }
    
    var tournamentStore: TournamentStore? {
        return TournamentLibrary.shared.store(tournamentId: self.tournament)
//        TournamentStore.instance(tournamentId: self.tournament)
    }
    
    func tournamentObject() -> Tournament? {
        return Store.main.findById(tournament)
    }
    
    @discardableResult
    func updateGroupStageSchedule(tournament: Tournament, specificGroupStage: GroupStage? = nil, atStep step: Int = 0, startDate: Date? = nil) -> Date {
        let computedGroupStageChunkCount = groupStageChunkCount ?? tournament.getGroupStageChunkValue()
        var groupStages: [GroupStage] = tournament.groupStages(atStep: step)
        if let specificGroupStage {
            groupStages = [specificGroupStage]
        }

        let matches = groupStages.flatMap { $0._matches() }
        matches.forEach({
            $0.removeCourt()
            $0.startDate = nil
            $0.confirmed = false
        })
        
        var lastDate : Date = startDate ?? tournament.startDate
        let times = Set(groupStages.compactMap { $0.startDate }).sorted()

        if let first = times.first {
            if first.isEarlierThan(tournament.startDate) {
                tournament.startDate = first
                do {
                    try DataStore.shared.tournaments.addOrUpdate(instance: tournament)
                } catch {
                    Logger.error(error)
                }
            }
        }
        
        times.forEach({ time in            
            if lastDate.isEarlierThan(time) {
                lastDate = time
            }
            let groups = groupStages.filter({ $0.startDate == time })
            let dispatch = groupStageDispatcher(groupStages: groups, startingDate: lastDate)

            dispatch.timedMatches.forEach { matchSchedule in
                if let match = matches.first(where: { $0.id == matchSchedule.matchID }) {
                    let estimatedDuration = match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)
                    let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(estimatedDuration) * 60
                    if let startDate = match.groupStageObject?.startDate {
                        let matchStartDate = startDate.addingTimeInterval(timeIntervalToAdd)
                        match.startDate = matchStartDate
                        lastDate = matchStartDate.addingTimeInterval(Double(estimatedDuration) * 60)
                    }
                    match.setCourt(matchSchedule.courtIndex)
                }
            }
        })

        groupStages.filter({ $0.startDate == nil || times.contains($0.startDate!) == false }).chunked(into: computedGroupStageChunkCount).forEach { groups in
            groups.forEach({ $0.startDate = lastDate })
            do {
                try self.tournamentStore?.groupStages.addOrUpdate(contentOfs: groups)
            } catch {
                Logger.error(error)
            }

            let dispatch = groupStageDispatcher(groupStages: groups, startingDate: lastDate)

            dispatch.timedMatches.forEach { matchSchedule in
                if let match = matches.first(where: { $0.id == matchSchedule.matchID }) {
                    let estimatedDuration = match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)
                    let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(estimatedDuration) * 60
                    if let startDate = match.groupStageObject?.startDate {
                        let matchStartDate = startDate.addingTimeInterval(timeIntervalToAdd)
                        match.startDate = matchStartDate
                        lastDate = matchStartDate.addingTimeInterval(Double(estimatedDuration) * 60)
                    }
                    match.setCourt(matchSchedule.courtIndex)
                }
            }
        }
        do {
            try self.tournamentStore?.matches.addOrUpdate(contentOfs: matches)
        } catch {
            Logger.error(error)
        }
        return lastDate
    }
    
    func groupStageDispatcher(groupStages: [GroupStage], startingDate: Date) -> GroupStageMatchDispatcher {
        
        let _groupStages = groupStages
        
        // Get the maximum count of matches in any group
        let maxMatchesCount = _groupStages.map { $0._matches().count }.max() ?? 0
        var flattenedMatches = [Match]()
        if simultaneousStart {
            // Flatten matches in a round-robin order by cycling through each group
            flattenedMatches = (0..<maxMatchesCount).flatMap { index in
                _groupStages.compactMap { group in
                    // Safely access matches, return nil if index is out of bounds
                    let playedMatches = group.playedMatches()
                    return playedMatches.indices.contains(index) ? playedMatches[index] : nil
                }
            }
        } else {
            flattenedMatches = _groupStages.flatMap({ $0.playedMatches() })
        }
        
        var slots = [GroupStageTimeMatch]()
        var availableMatches = flattenedMatches
        var rotationIndex = 0
        var teamsPerRotation = [Int: [String]]()  // Tracks teams assigned to each rotation
        var freeCourtPerRotation = [Int: [Int]]()  // Tracks free courts per rotation
        var groupLastRotation = [Int: Int]()  // Tracks the last rotation each group was involved in
        let courtsUnavailability = courtsUnavailability
        
        while slots.count < flattenedMatches.count {
            print("Starting rotation \(rotationIndex) with \(availableMatches.count) matches left")
            teamsPerRotation[rotationIndex] = []
            freeCourtPerRotation[rotationIndex] = []
            
            let previousRotationBracketIndexes = slots.filter { $0.rotationIndex == rotationIndex - 1 }
                .map { ($0.groupIndex, 1) }
            
            let counts = Dictionary(previousRotationBracketIndexes, uniquingKeysWith: +)
            var rotationMatches = Array(availableMatches.filter({ match in
                // Check if all teams from the match are not already scheduled in the current rotation
                let teamsAvailable = teamsPerRotation[rotationIndex]!.allSatisfy({ !match.containsTeamIndex($0) })
                if !teamsAvailable {
                    print("Match \(match.roundAndMatchTitle()) has teams already scheduled in rotation \(rotationIndex)")
                }
                return teamsAvailable
            }))
            
            if rotationIndex > 0, simultaneousStart == false {
                rotationMatches = rotationMatches.sorted(by: {
                    if counts[$0.groupStageObject!.index] ?? 0 == counts[$1.groupStageObject!.index] ?? 0 {
                        return $0.groupStageObject!.index < $1.groupStageObject!.index
                    } else {
                        return counts[$0.groupStageObject!.index] ?? 0 < counts[$1.groupStageObject!.index] ?? 0
                    }
                })
            }
            
            courtsAvailable.forEach { courtIndex in
                print("Checking availability for court \(courtIndex) in rotation \(rotationIndex)")
                if let first = rotationMatches.first(where: { match in
                    let estimatedDuration = match.matchFormat.getEstimatedDuration(additionalEstimationDuration)
                    let timeIntervalToAdd = Double(rotationIndex) * Double(estimatedDuration) * 60
                    let rotationStartDate: Date = startingDate.addingTimeInterval(timeIntervalToAdd)
                    
                    let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: match.matchFormat.getEstimatedDuration(additionalEstimationDuration), courtsUnavailability: courtsUnavailability)

                    if courtsUnavailable.contains(courtIndex) {
                        print("Court \(courtIndex) is unavailable at \(rotationStartDate)")
                        return false
                    }
                    
                    let teamsAvailable = teamsPerRotation[rotationIndex]!.allSatisfy({ !match.containsTeamIndex($0) })
                    if !teamsAvailable {
                        print("Teams from match \(match.roundAndMatchTitle()) are already scheduled in this rotation")
                        return false
                    }
                    
                    print("Match \(match.roundAndMatchTitle()) is available for court \(courtIndex) at \(rotationStartDate)")
                    return true
                }) {
                    let timeMatch = GroupStageTimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, groupIndex: first.groupStageObject!.index)
                    
                    print("Scheduled match: \(first.roundAndMatchTitle()) on court \(courtIndex) at rotation \(rotationIndex)")
                    
                    slots.append(timeMatch)
                    teamsPerRotation[rotationIndex]!.append(contentsOf: first.matchUp())
                    rotationMatches.removeAll(where: { $0.id == first.id })
                    availableMatches.removeAll(where: { $0.id == first.id })
                    
                    if let index = first.groupStageObject?.index {
                        groupLastRotation[index] = rotationIndex
                    }
                } else {
                    print("No available matches for court \(courtIndex) in rotation \(rotationIndex), adding to free court list")
                    freeCourtPerRotation[rotationIndex]!.append(courtIndex)
                }
            }
            
            rotationIndex += 1
        }
        
        print("All matches scheduled. Total rotations: \(rotationIndex)")
        
        // Organize slots and ensure courts are randomized or sorted
        var organizedSlots = [GroupStageTimeMatch]()
        for i in 0..<rotationIndex {
            let courtsSorted: [Int] = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted()
            let courts: [Int] = randomizeCourts ? courtsSorted.shuffled() : courtsSorted
            var matches = slots.filter({ $0.rotationIndex == i }).sorted(using: .keyPath(\.groupIndex), .keyPath(\.courtIndex))
            
            for j in 0..<matches.count {
                matches[j].courtIndex = courts[j]
                organizedSlots.append(matches[j])
            }
        }
        
        return GroupStageMatchDispatcher(
            timedMatches: organizedSlots,
            freeCourtPerRotation: freeCourtPerRotation,
            rotationCount: rotationIndex,
            groupLastRotation: groupLastRotation
        )
    }

    
    func rotationDifference(loserBracket: Bool) -> Int {
        if loserBracket {
            return loserBracketRotationDifference
        } else {
            return upperBracketRotationDifference
        }
    }
    
    func roundMatchCanBePlayed(_ match: Match, roundObject: Round, slots: [TimeMatch], rotationIndex: Int, targetedStartDate: Date, minimumTargetedEndDate: inout Date) -> Bool {
        print("Evaluating match: \(match.roundAndMatchTitle()) in round: \(roundObject.roundTitle()) with index: \(match.index)")

        if let roundStartDate = roundObject.startDate, targetedStartDate < roundStartDate {
            print("Cannot start at \(targetedStartDate), earlier than round start date \(roundStartDate)")
            if targetedStartDate == minimumTargetedEndDate {
                print("Updating minimumTargetedEndDate to roundStartDate: \(roundStartDate)")
                minimumTargetedEndDate = roundStartDate
            } else {
                print("Setting minimumTargetedEndDate to the earlier of \(roundStartDate) and \(minimumTargetedEndDate)")
                minimumTargetedEndDate = min(roundStartDate, minimumTargetedEndDate)
            }
            print("Returning false: Match cannot start earlier than the round start date.")
            return false
        }
        
        let previousMatches = roundObject.precedentMatches(ofMatch: match)
        if previousMatches.isEmpty {
            print("No ancestors matches for this match, returning true. (eg beginning of tournament 1st bracket")
            return true
        }

        let previousMatchSlots = slots.filter { previousMatches.map { $0.id }.contains($0.matchID) }
        
        if previousMatchSlots.isEmpty {
            if previousMatches.filter({ !$0.disabled }).allSatisfy({ $0.startDate != nil }) {
                print("All previous matches have start dates, returning true.")
                return true
            }
            print("Some previous matches are pending, returning false.")
            return false
        }
        
        if previousMatches.filter({ !$0.disabled }).count > previousMatchSlots.count {
            if previousMatches.filter({ !$0.disabled }).anySatisfy({ $0.startDate != nil }) {
                print("Some previous matches started, returning true.")
                return true
            }
            print("Not enough previous matches have started, returning false.")
            return false
        }

        var includeBreakTime = false
        if accountLoserBracketBreakTime && roundObject.isLoserBracket() {
            includeBreakTime = true
            print("Including break time for loser bracket.")
        }
        
        if accountUpperBracketBreakTime && !roundObject.isLoserBracket() {
            includeBreakTime = true
            print("Including break time for upper bracket.")
        }

        let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy {
            $0.rotationIndex + rotationDifference(loserBracket: roundObject.isLoserBracket()) < rotationIndex
        }
        
        if previousMatchIsInPreviousRotation {
            print("All previous matches are from earlier rotations, returning true.")
        } else {
            print("Some previous matches are from the current rotation.")
        }

        guard let minimumPossibleEndDate = previousMatchSlots.map({
            $0.estimatedEndDate(includeBreakTime: includeBreakTime)
        }).max() else {
            print("No valid previous match end date, returning \(previousMatchIsInPreviousRotation).")
            return previousMatchIsInPreviousRotation
        }
        
        if targetedStartDate >= minimumPossibleEndDate {
            if rotationDifferenceIsImportant {
                print("Targeted start date is after the minimum possible end date and rotation difference is important, returning \(previousMatchIsInPreviousRotation).")
                return previousMatchIsInPreviousRotation
            } else {
                print("Targeted start date is after the minimum possible end date, returning true.")
                return true
            }
        } else {
            if targetedStartDate == minimumTargetedEndDate {
                print("Updating minimumTargetedEndDate to minimumPossibleEndDate: \(minimumPossibleEndDate)")
                minimumTargetedEndDate = minimumPossibleEndDate
            } else {
                print("Setting minimumTargetedEndDate to the earlier of \(minimumPossibleEndDate) and \(minimumTargetedEndDate)")
                minimumTargetedEndDate = min(minimumPossibleEndDate, minimumTargetedEndDate)
            }
            print("Targeted start date \(targetedStartDate) is before the minimum possible end date, returning false. \(minimumTargetedEndDate)")
            return false
        }
    }

    
    func getNextStartDate(fromPreviousRotationSlots slots: [TimeMatch], includeBreakTime: Bool) -> Date? {
        slots.map { $0.estimatedEndDate(includeBreakTime: includeBreakTime) }.min()
    }
    
    func getNextEarliestAvailableDate(from slots: [TimeMatch]) -> [(Int, Date)] {
        let byCourt = Dictionary(grouping: slots, by: { $0.courtIndex })
        return (byCourt.keys.flatMap { courtIndex in
            let matchesByCourt = byCourt[courtIndex]?.sorted(by: \.startDate)
            let lastMatch = matchesByCourt?.last
            var results = [(Int, Date)]()
            if let courtFreeDate = lastMatch?.estimatedEndDate(includeBreakTime: false) {
                results.append((courtIndex, courtFreeDate))
            }
            return results
        }
        )
    }
    
    func getAvailableCourts(from matches: [Match]) -> [(Int, Date)] {
        let validMatches = matches.filter({ $0.courtIndex != nil && $0.startDate != nil })
        let byCourt = Dictionary(grouping: validMatches, by: { $0.courtIndex! })
        return (byCourt.keys.flatMap { court in
            let matchesByCourt = byCourt[court]?.sorted(by: \.startDate!)
            let lastMatch = matchesByCourt?.last
            var results = [(Int, Date)]()
            if let courtFreeDate = lastMatch?.estimatedEndDate(additionalEstimationDuration) {
                results.append((court, courtFreeDate))
            }
            return results
        }
        )
    }
    
    func roundDispatcher(flattenedMatches: [Match], dispatcherStartDate: Date, initialCourts: [Int]?) -> MatchDispatcher {
        var slots = [TimeMatch]()
        var _startDate: Date?
        var rotationIndex = 0
        var availableMatchs = flattenedMatches.filter({ $0.startDate == nil })
        let courtsUnavailability = courtsUnavailability
        var issueFound: Bool = false

        // Log start of the function
        print("Starting roundDispatcher with \(availableMatchs.count) matches and \(courtsAvailable) courts available")
        
        flattenedMatches.filter { $0.startDate != nil }.sorted(by: \.startDate!).forEach { match in
            if _startDate == nil {
                _startDate = match.startDate
            } else if match.startDate! > _startDate! {
                _startDate = match.startDate
                rotationIndex += 1
            }
            
            let timeMatch = TimeMatch(matchID: match.id, rotationIndex: rotationIndex, courtIndex: match.courtIndex ?? 0, startDate: match.startDate!, durationLeft: match.matchFormat.getEstimatedDuration(additionalEstimationDuration), minimumBreakTime: match.matchFormat.breakTime.breakTime)
            slots.append(timeMatch)
        }

        if !slots.isEmpty {
            rotationIndex += 1
        }
        
        var freeCourtPerRotation = [Int: [Int]]()
        var courts = initialCourts ?? Array(courtsAvailable)
        var shouldStartAtDispatcherDate = rotationIndex > 0
        var suitableDate: Date?
        
        while !availableMatchs.isEmpty && !issueFound && rotationIndex < 50 {
            freeCourtPerRotation[rotationIndex] = []
            let previousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 1 })
            
            var rotationStartDate: Date
            if previousRotationSlots.isEmpty && rotationIndex > 0 {
                let computedSuitableDate = slots.sorted(by: \.computedEndDateForSorting).last?.computedEndDateForSorting
                print("Previous rotation was empty, find a suitable rotationStartDate \(suitableDate)")
                rotationStartDate = suitableDate ?? computedSuitableDate ?? dispatcherStartDate
            } else {
                rotationStartDate = getNextStartDate(fromPreviousRotationSlots: previousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate
            }

            if shouldStartAtDispatcherDate {
                rotationStartDate = dispatcherStartDate
                shouldStartAtDispatcherDate = false
            } else {
                courts = rotationIndex == 0 ? courts : Array(courtsAvailable)
            }
            courts.sort()

            // Log courts availability and start date
            print("Courts available at rotation \(rotationIndex): \(courts)")
            print("Rotation start date: \(rotationStartDate)")

            // Check for court availability and break time conflicts
            if rotationIndex > 0, let freeCourtPreviousRotation = freeCourtPerRotation[rotationIndex - 1], !freeCourtPreviousRotation.isEmpty {
                print("Handling break time conflicts or waiting for free courts")
                let previousPreviousRotationSlots = slots.filter { $0.rotationIndex == rotationIndex - 2 && freeCourtPreviousRotation.contains($0.courtIndex) }
                var previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: accountUpperBracketBreakTime)
                var previousEndDateNoBreak = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false)
                
                if let courtsUnavailability, previousEndDate != nil {
                    previousEndDate = getFirstFreeCourt(startDate: previousEndDate!, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability).earliestFreeDate
                }
                
                if let courtsUnavailability, previousEndDateNoBreak != nil {
                    previousEndDateNoBreak = getFirstFreeCourt(startDate: previousEndDateNoBreak!, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability).earliestFreeDate
                }

                let noBreakAlreadyTested = previousRotationSlots.anySatisfy { $0.startDate == previousEndDateNoBreak }
                
                if let previousEndDate, let previousEndDateNoBreak {
                    let differenceWithBreak = rotationStartDate.timeIntervalSince(previousEndDate)
                    let differenceWithoutBreak = rotationStartDate.timeIntervalSince(previousEndDateNoBreak)
                    print("Difference with break: \(differenceWithBreak), without break: \(differenceWithoutBreak)")
                    
                    let timeDifferenceLimitInSeconds = Double(timeDifferenceLimit * 60)
                    var difference = differenceWithBreak
                    
                    if differenceWithBreak <= 0, accountUpperBracketBreakTime == false {
                        difference = differenceWithoutBreak
                    } else if differenceWithBreak > timeDifferenceLimitInSeconds && differenceWithoutBreak > timeDifferenceLimitInSeconds {
                        difference = noBreakAlreadyTested ? differenceWithBreak : max(differenceWithBreak, differenceWithoutBreak)
                    }

                    print("Final difference to evaluate: \(difference)")

                    if (difference > timeDifferenceLimitInSeconds && rotationStartDate.addingTimeInterval(-difference) != previousEndDate) || difference < 0 {
                        print("""
                        Adjusting rotation start:
                          - Initial rotationStartDate: \(rotationStartDate)
                          - Adjusted by difference: \(difference)
                          - Adjusted rotationStartDate: \(rotationStartDate.addingTimeInterval(-difference))
                          - PreviousEndDate: \(previousEndDate)
                        """)

                        courts.removeAll(where: { freeCourtPreviousRotation.contains($0) })
                        freeCourtPerRotation[rotationIndex] = courts
                        courts = freeCourtPreviousRotation
                        rotationStartDate = rotationStartDate.addingTimeInterval(-difference)
                    }
                }
            } else if let firstMatch = availableMatchs.first {
                let duration = firstMatch.matchFormat.getEstimatedDuration(additionalEstimationDuration)
                let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: duration, courtsUnavailability: courtsUnavailability)
                
                if Array(Set(courtsAvailable).subtracting(Set(courtsUnavailable))).isEmpty {
                    print("Issue: All courts unavailable in this rotation")
                    if let courtsUnavailability {
                        let computedStartDateAndCourts = getFirstFreeCourt(startDate: rotationStartDate, duration: duration, courts: courts, courtsUnavailability: courtsUnavailability)
                        rotationStartDate = computedStartDateAndCourts.earliestFreeDate
                        courts = computedStartDateAndCourts.availableCourts
                    } else {
                        issueFound = true
                    }
                } else {
                    courts = Array(Set(courtsAvailable).subtracting(Set(courtsUnavailable)))
                }
            }

            // Dispatch courts and schedule matches
            suitableDate = dispatchCourts(courts: courts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: rotationStartDate, freeCourtPerRotation: &freeCourtPerRotation, courtsUnavailability: courtsUnavailability)
            rotationIndex += 1
        }

        // Organize matches in slots
        var organizedSlots = [TimeMatch]()
        for i in 0..<rotationIndex {
            let courtsSorted = slots.filter { $0.rotationIndex == i }.map { $0.courtIndex }.sorted()
            let courts = randomizeCourts ? courtsSorted.shuffled() : courtsSorted
            var matches = slots.filter { $0.rotationIndex == i }.sorted(using: .keyPath(\.courtIndex))
            
            for j in 0..<matches.count {
                matches[j].courtIndex = courts[j]
                organizedSlots.append(matches[j])
            }
        }

        print("Finished roundDispatcher with \(organizedSlots.count) scheduled matches")

        return MatchDispatcher(timedMatches: organizedSlots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, issueFound: issueFound)
    }

    func dispatchCourts(courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]], courtsUnavailability: [DateInterval]?) -> Date {
        var matchPerRound = [String: Int]()
        var minimumTargetedEndDate = rotationStartDate

        // Log dispatch attempt
        print("Dispatching courts for rotation \(rotationIndex) with start date \(rotationStartDate) and available courts \(courts.sorted())")

        for (courtPosition, courtIndex) in courts.sorted().enumerated() {
            if let firstMatch = availableMatchs.first(where: { match in
                print("Trying to find a match for court \(courtIndex) in rotation \(rotationIndex)")
                
                let roundObject = match.roundObject!
                let duration = match.matchFormat.getEstimatedDuration(additionalEstimationDuration)
                
                let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: duration, courtsUnavailability: courtsUnavailability)
                
                if courtsUnavailable.contains(courtIndex) {
                    print("Returning false: Court \(courtIndex) unavailable due to schedule conflicts during \(rotationStartDate).")
                    return false
                }
                
                let canBePlayed = roundMatchCanBePlayed(match, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate)
                
                if !canBePlayed {
                    print("Returning false: Match \(match.roundAndMatchTitle()) can't be played due to constraints.")
                    return false
                }
                
                let currentRotationSameRoundMatches = matchPerRound[roundObject.id] ?? 0
                let roundMatchesCount = roundObject.playedMatches().count
                
                if shouldHandleUpperRoundSlice {
                    if roundObject.parent == nil, roundObject.index > 1, currentRotationSameRoundMatches == 0, courts.count - matchPerRound.count < 2 {
                        return false
                    }
                    else if roundObject.parent == nil, roundObject.index == 0, matchPerRound.isEmpty == false {
                        return false
                    }
                    else if roundObject.parent == nil, roundObject.index == 1, currentRotationSameRoundMatches >= 0, courtsAvailable.count > 0 {
                        return true
                    }
                    else if roundObject.parent == nil && roundMatchesCount > courts.count && currentRotationSameRoundMatches >= min(roundMatchesCount / 2, courts.count) {
                        print("Returning false: Too many matches already played in the current rotation for round \(roundObject.roundTitle()).")
                        return false
                    }
                }
                
                let indexInRound = match.indexInRound()
                
                
                if shouldTryToFillUpCourtsAvailable == false {
                    if roundObject.parent == nil && roundObject.index > 1 && indexInRound == 0, let nextMatch = match.next() {
                        
                        var nextMinimumTargetedEndDate = minimumTargetedEndDate
                        if courtPosition < courts.count - 1 && canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &nextMinimumTargetedEndDate) {
                            print("Returning true: Both current \(match.index) and next match  \(nextMatch.index) can be played in rotation \(rotationIndex).")
                            return true
                        } else {
                            print("Returning false: Either current match or next match cannot be played in rotation \(rotationIndex).")
                            return false
                        }
                    }
                }

                print("Returning true: Match \(match.roundAndMatchTitle()) can be played on court \(courtIndex).")
                return canBePlayed
            }) {
                print("Found match: \(firstMatch.roundAndMatchTitle()) for court \(courtIndex) at \(rotationStartDate)")

                matchPerRound[firstMatch.roundObject!.id, default: 0] += 1

                let timeMatch = TimeMatch(
                    matchID: firstMatch.id,
                    rotationIndex: rotationIndex,
                    courtIndex: courtIndex,
                    startDate: rotationStartDate,
                    durationLeft: firstMatch.matchFormat.getEstimatedDuration(additionalEstimationDuration),
                    minimumBreakTime: firstMatch.matchFormat.breakTime.breakTime
                )
                
                slots.append(timeMatch)
                availableMatchs.removeAll(where: { $0.id == firstMatch.id })
            } else {
                print("No suitable match found for court \(courtIndex) in rotation \(rotationIndex). Adding court to freeCourtPerRotation.")
                freeCourtPerRotation[rotationIndex]?.append(courtIndex)
            }

        }

        if freeCourtPerRotation[rotationIndex]?.count == courtsAvailable.count {
            print("All courts in rotation \(rotationIndex) are free, minimumTargetedEndDate : \(minimumTargetedEndDate)")
        }
        
        if let courtsUnavailability {
            let computedStartDateAndCourts = getFirstFreeCourt(startDate: minimumTargetedEndDate, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability)
            return computedStartDateAndCourts.earliestFreeDate
        }

        return minimumTargetedEndDate
    }

    @discardableResult func updateBracketSchedule(tournament: Tournament, fromRoundId roundId: String?, fromMatchId matchId: String?, startDate: Date) -> Bool {

        let upperRounds: [Round] = tournament.rounds()
        let allMatches: [Match] = tournament.allMatches().filter({ $0.hasEnded() == false && $0.hasStarted() == false })

        var rounds = [Round]()
        
        if let groupStageLoserBracketRound = tournament.groupStageLoserBracket() {
            rounds.append(groupStageLoserBracketRound)
        }
        
        if shouldEndRoundBeforeStartingNext {
            rounds.append(contentsOf: upperRounds.flatMap {
                [$0] + $0.loserRoundsAndChildren()
            })
        } else {
            rounds.append(contentsOf: upperRounds.map {
                $0
            } + upperRounds.flatMap {
                $0.loserRoundsAndChildren()
            })
        }
        
        let flattenedMatches = rounds.flatMap { round in
            round._matches().filter({ $0.disabled == false && $0.hasEnded() == false && $0.hasStarted() == false }).sorted(by: \.index)
        }
                
        flattenedMatches.forEach({
            if (roundId == nil && matchId == nil) || $0.startDate?.isEarlierThan(startDate) == false {
                $0.startDate = nil
                $0.removeCourt()
                $0.confirmed = false
            }
        })
        
//        if let roundId {
//            if let round : Round = Store.main.findById(roundId) {
//                let matches = round._matches().filter({ $0.disabled == false }).sorted(by: \.index)
//                round.resetFromRoundAllMatchesStartDate()
//                flattenedMatches = matches + flattenedMatches
//            }
//            
//        } else if let matchId {
//            if let match : Match = Store.main.findById(matchId) {
//                if let round = match.roundObject {
//                    round.resetFromRoundAllMatchesStartDate(from: match)
//                }
//                flattenedMatches = [match] + flattenedMatches
//            }
//        }
        
        if let roundId, let matchId {
            //todo
            if let index = flattenedMatches.firstIndex(where: { $0.round == roundId && $0.id == matchId }) {
                flattenedMatches[index...].forEach {
                    $0.startDate = nil
                    $0.removeCourt()
                    $0.confirmed = false
                }
            }
        } else if let roundId {
            //todo
            if let index = flattenedMatches.firstIndex(where: { $0.round == roundId }) {
                flattenedMatches[index...].forEach {
                    $0.startDate = nil
                    $0.removeCourt()
                    $0.confirmed = false
                }
            }
        }
        
        
        let matches: [Match] = allMatches.filter { $0.startDate?.isEarlierThan(startDate) == true && $0.startDate?.dayInt == startDate.dayInt }
        let usedCourts = getAvailableCourts(from: matches)
        let initialCourts: [Int] = usedCourts.filter { (court, availableDate) in
            availableDate <= startDate
        }.sorted(by: \.1).compactMap { $0.0 }

        let courts : [Int]? = initialCourts.isEmpty ? nil : initialCourts
        
        print("initial available courts at beginning: \(courts ?? [])")

        let roundDispatch = self.roundDispatcher(flattenedMatches: flattenedMatches, dispatcherStartDate: startDate, initialCourts: courts)
        
        roundDispatch.timedMatches.forEach { matchSchedule in
            if let match = flattenedMatches.first(where: { $0.id == matchSchedule.matchID }) {
                match.startDate = matchSchedule.startDate
                match.setCourt(matchSchedule.courtIndex)
            }
        }
        
        do {
            try self.tournamentStore?.matches.addOrUpdate(contentOfs: allMatches)
        } catch {
            Logger.error(error)
        }
        
        return roundDispatch.issueFound
    }
    
    
    func courtsUnavailable(startDate: Date, duration: Int, courtsUnavailability: [DateInterval]?) -> [Int] {
        let endDate = startDate.addingTimeInterval(Double(duration) * 60)
        guard let courtsUnavailability else { return [] }
        let groupedBy = Dictionary(grouping: courtsUnavailability, by: { $0.courtIndex })
        let courts = groupedBy.keys
        return courts.filter {
            courtUnavailable(courtIndex: $0, from: startDate, to: endDate, source: courtsUnavailability)
        }
    }
    
    func courtUnavailable(courtIndex: Int, from startDate: Date, to endDate: Date, source: [DateInterval]) -> Bool {
        let courtLockedSchedule = source.filter({ $0.courtIndex == courtIndex })
        return courtLockedSchedule.anySatisfy({ dateInterval in
            let range = startDate..<endDate
            return dateInterval.range.overlaps(range)
        })
    }
    
    func getFirstFreeCourt(startDate: Date, duration: Int, courts: [Int], courtsUnavailability: [DateInterval]) -> (earliestFreeDate: Date, availableCourts: [Int]) {
        var earliestEndDate: Date?
        var availableCourtsAtEarliest: [Int] = []

        // Iterate through each court and find the earliest time it becomes free
        for courtIndex in courts {
            let unavailabilityForCourt = courtsUnavailability.filter { $0.courtIndex == courtIndex }
            var isAvailable = true
            
            for interval in unavailabilityForCourt {
                if interval.startDate <= startDate && interval.endDate > startDate {
                    isAvailable = false
                    if let currentEarliest = earliestEndDate {
                        earliestEndDate = min(currentEarliest, interval.endDate)
                    } else {
                        earliestEndDate = interval.endDate
                    }
                }
            }
            
            // If the court is available at the start date, add it to the list of available courts
            if isAvailable {
                availableCourtsAtEarliest.append(courtIndex)
            }
        }

        // If there are no unavailable courts, return the original start date and all courts
        if let earliestEndDate = earliestEndDate {
            // Find which courts will be available at the earliest free date
            let courtsAvailableAtEarliest = courts.filter { courtIndex in
                let unavailabilityForCourt = courtsUnavailability.filter { $0.courtIndex == courtIndex }
                return unavailabilityForCourt.allSatisfy { $0.endDate <= earliestEndDate }
            }
            return (earliestFreeDate: earliestEndDate, availableCourts: courtsAvailableAtEarliest)
        } else {
            // If no courts were unavailable, all courts are available at the start date
            return (earliestFreeDate: startDate.addingTimeInterval(Double(duration) * 60), availableCourts: courts)
        }
    }
    
    func updateSchedule(tournament: Tournament) -> Bool {
        if tournament.courtCount < courtsAvailable.count {
            courtsAvailable = Set(tournament.courtsAvailable())
        }
        var lastDate = tournament.startDate
        if tournament.groupStageCount > 0 {
            lastDate = updateGroupStageSchedule(tournament: tournament)
        }
        if tournament.groupStages(atStep: 1).isEmpty == false {
            lastDate = updateGroupStageSchedule(tournament: tournament, atStep: 1, startDate: lastDate)
        }
        return updateBracketSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: lastDate)
    }
}

struct GroupStageTimeMatch {
    let matchID: String
    let rotationIndex: Int
    var courtIndex: Int
    let groupIndex: Int
}

struct TimeMatch {
    let matchID: String
    let rotationIndex: Int
    var courtIndex: Int
    var startDate: Date
    var durationLeft: Int //in minutes
    var minimumBreakTime: Int //in minutes
    
    func estimatedEndDate(includeBreakTime: Bool) -> Date {
        let minutesToAdd = Double(durationLeft + (includeBreakTime ? minimumBreakTime : 0))
        return startDate.addingTimeInterval(minutesToAdd * 60.0)
    }
    
    var computedEndDateForSorting: Date {
        estimatedEndDate(includeBreakTime: false)
    }
}

struct GroupStageMatchDispatcher {
    let timedMatches: [GroupStageTimeMatch]
    let freeCourtPerRotation: [Int: [Int]]
    let rotationCount: Int
    let groupLastRotation: [Int: Int]
}

struct MatchDispatcher {
    let timedMatches: [TimeMatch]
    let freeCourtPerRotation: [Int: [Int]]
    let rotationCount: Int
    let issueFound: Bool
}

extension Match {
    func teamIds() -> [String] {
        return teams().map { $0.id }
    }
    
    func containsTeamId(_ id: String) -> Bool {
        return teamIds().contains(id)
    }
    
    func containsTeamIndex(_ id: String) -> Bool {
        matchUp().contains(id)
    }
    
    func matchUp() -> [String] {
        guard let groupStageObject else {
            return []
        }
        
        return groupStageObject._matchUp(for: index).map { groupStageObject.id + "_\($0)" }
    }
}
