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

import Foundation
import LeStorage
import SwiftUI

@Observable
class MatchScheduler : ModelObject, Storable {
    
    static func resourceName() -> String { return "match-scheduler" }
    static func tokenExemptedMethods() -> [HTTPMethod] { return [] }

    private(set) var id: String = Store.randomId()
    var tournament: String
    var timeDifferenceLimit: Int
    var loserBracketRotationDifference: Int
    var upperBracketRotationDifference: Int
    var accountUpperBracketBreakTime: Bool
    var accountLoserBracketBreakTime: Bool
    var randomizeCourts: Bool
    var rotationDifferenceIsImportant: Bool
    var shouldHandleUpperRoundSlice: Bool
    var shouldEndRoundBeforeStartingNext: Bool
    var groupStageChunkCount: Int?
    var overrideCourtsUnavailability: Bool = false
    
    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 = true,
         shouldEndRoundBeforeStartingNext: Bool = true,
         groupStageChunkCount: Int? = nil, overrideCourtsUnavailability: Bool = false) {
        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
    }
    
    enum CodingKeys: String, CodingKey {
        case _id = "id"
        case _tournament = "tournament"
        case _timeDifferenceLimit = "timeDifferenceLimit"
        case _loserBracketRotationDifference = "loserBracketRotationDifference"
        case _upperBracketRotationDifference = "upperBracketRotationDifference"
        case _accountUpperBracketBreakTime = "accountUpperBracketBreakTime"
        case _accountLoserBracketBreakTime = "accountLoserBracketBreakTime"
        case _randomizeCourts = "randomizeCourts"
        case _rotationDifferenceIsImportant = "rotationDifferenceIsImportant"
        case _shouldHandleUpperRoundSlice = "shouldHandleUpperRoundSlice"
        case _shouldEndRoundBeforeStartingNext = "shouldEndRoundBeforeStartingNext"
        case _groupStageChunkCount = "groupStageChunkCount"
        case _overrideCourtsUnavailability = "overrideCourtsUnavailability"
    }

    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
    }
    
    func tournamentObject() -> Tournament? {
        return Store.main.findById(tournament)
    }
    
    @discardableResult
    func updateGroupStageSchedule(tournament: Tournament) -> Date {
        let computedGroupStageChunkCount = groupStageChunkCount ?? 1
        let groupStages: [GroupStage] = tournament.groupStages()
        let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount

        let matches = groupStages.flatMap { $0._matches() }
        matches.forEach({
            $0.removeCourt()
            $0.startDate = nil
            $0.confirmed = false
        })
        
        var lastDate : Date = 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
            lastDate = time
            let groups = groupStages.filter({ $0.startDate == time })
            let dispatch = groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, 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 DataStore.shared.groupStages.addOrUpdate(contentOfs: groups)
            } catch {
                Logger.error(error)
            }

            let dispatch = groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, 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 DataStore.shared.matches.addOrUpdate(contentOfs: matches)
        } catch {
            Logger.error(error)
        }
        return lastDate
    }
    
    func groupStageDispatcher(numberOfCourtsAvailablePerRotation: Int, 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
        
        // Use zip and flatMap to flatten matches in the desired order
        let flattenedMatches = (0..<maxMatchesCount).flatMap { index in
            _groupStages.compactMap { group in
                // Use optional subscript to safely access matches
                let playedMatches = group.playedMatches()
                return playedMatches.indices.contains(index) ? playedMatches[index] : nil
            }
        }
        
        var slots = [GroupStageTimeMatch]()
        var availableMatchs = flattenedMatches
        var rotationIndex = 0
        var teamsPerRotation = [Int: [String]]()
        var freeCourtPerRotation = [Int: [Int]]()
        var groupLastRotation = [Int: Int]()
        let courtsUnavailability = courtsUnavailability
        
        while slots.count < flattenedMatches.count {
            teamsPerRotation[rotationIndex] = []
            freeCourtPerRotation[rotationIndex] = []
            let previousRotationBracketIndexes = slots.filter { $0.rotationIndex == rotationIndex - 1 }.map { ($0.groupIndex, 1) }
            let counts = Dictionary(previousRotationBracketIndexes, uniquingKeysWith: +)
            var rotationMatches = Array(availableMatchs.filter({ match in
                teamsPerRotation[rotationIndex]!.allSatisfy({ match.containsTeamId($0) == false }) == true
            }).prefix(numberOfCourtsAvailablePerRotation))
            
            if rotationIndex > 0 {
                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
                    }
                })
            }
            
            (0..<numberOfCourtsAvailablePerRotation).forEach { courtIndex in
                //print(mt.map { ($0.bracket!.index.intValue, counts[$0.bracket!.index.intValue]) })
                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 courtIndex >= numberOfCourtsAvailablePerRotation - courtsUnavailable {
                        return false
                    } else {
                        return teamsPerRotation[rotationIndex]!.allSatisfy({ match.containsTeamId($0) == false }) == true
                    }
                }) {
                    let timeMatch = GroupStageTimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, groupIndex: first.groupStageObject!.index)
                    slots.append(timeMatch)
                    teamsPerRotation[rotationIndex]!.append(contentsOf: first.teamIds())
                    rotationMatches.removeAll(where: { $0.id == first.id })
                    availableMatchs.removeAll(where: { $0.id == first.id })
                    if let index = first.groupStageObject?.index {
                        groupLastRotation[index] = rotationIndex
                    }
                } else {
                    freeCourtPerRotation[rotationIndex]!.append(courtIndex)
                }
            }
            
            rotationIndex += 1
        }
        
        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(roundObject.roundTitle(), match.matchTitle())
        
        if let roundStartDate = roundObject.startDate, targetedStartDate < roundStartDate {
            print("can't start \(targetedStartDate) earlier than \(roundStartDate)")
            if targetedStartDate == minimumTargetedEndDate {
                minimumTargetedEndDate = roundStartDate
            } else {
                minimumTargetedEndDate = min(roundStartDate, minimumTargetedEndDate)
            }
            return false
        }
        
        let previousMatches = roundObject.precedentMatches(ofMatch: match)
        if previousMatches.isEmpty { return true }

        let previousMatchSlots = slots.filter({ slot in
            previousMatches.map { $0.id }.contains(slot.matchID)
        })
        
        if previousMatchSlots.isEmpty {
            if previousMatches.filter({ $0.disabled == false }).allSatisfy({ $0.startDate != nil }) {
                return true
            }
            return false
        }
        
        if previousMatches.filter({ $0.disabled == false }).count > previousMatchSlots.count {
            if previousMatches.filter({ $0.disabled == false }).anySatisfy({ $0.startDate != nil }) {
                return true
            }
            return false
        }
        
        var includeBreakTime = false
        
        if accountLoserBracketBreakTime && roundObject.isLoserBracket() {
            includeBreakTime = true
        }

        if accountUpperBracketBreakTime && roundObject.isLoserBracket() == false {
            includeBreakTime = true
        }

        let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy({ $0.rotationIndex + rotationDifference(loserBracket: roundObject.isLoserBracket()) < rotationIndex })
                
        guard let minimumPossibleEndDate = previousMatchSlots.map({ $0.estimatedEndDate(includeBreakTime: includeBreakTime) }).max() else {
            return previousMatchIsInPreviousRotation
        }
        
        if targetedStartDate >= minimumPossibleEndDate {
            if rotationDifferenceIsImportant {
                return previousMatchIsInPreviousRotation
            } else {
                return true
            }
        } else {
            if targetedStartDate == minimumTargetedEndDate {
                minimumTargetedEndDate = minimumPossibleEndDate
            } else {
                minimumTargetedEndDate = min(minimumPossibleEndDate, 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(numberOfCourtsAvailablePerRotation: Int, 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
        
        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 == false {
            rotationIndex += 1
        }
        
        var freeCourtPerRotation = [Int: [Int]]()

        let availableCourt = numberOfCourtsAvailablePerRotation

        var courts = initialCourts ?? (0..<availableCourt).map { $0 }

        var shouldStartAtDispatcherDate = rotationIndex > 0

        while availableMatchs.count > 0 && issueFound == false {
            freeCourtPerRotation[rotationIndex] = []
            let previousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 1 })
            var rotationStartDate: Date = getNextStartDate(fromPreviousRotationSlots: previousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate
            
            if shouldStartAtDispatcherDate {
                rotationStartDate = dispatcherStartDate
                shouldStartAtDispatcherDate = false
            } else {
                courts = rotationIndex == 0 ? courts : (0..<availableCourt).map { $0 }
            }
            courts.sort()
            print("courts available at rotation \(rotationIndex)", courts)
            print("rotationStartDate", rotationStartDate)

            if rotationIndex > 0, let freeCourtPreviousRotation = freeCourtPerRotation[rotationIndex - 1], freeCourtPreviousRotation.count > 0 {
                print("scenario where we are waiting for a breaktime to be over without any match to play in between or a free court was available and we need to recheck breaktime left on it")
                let previousPreviousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 2 && freeCourtPreviousRotation.contains($0.courtIndex) })
                let previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: accountUpperBracketBreakTime)
                let previousEndDateNoBreak = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false)
                
                let noBreakAlreadyTested = previousRotationSlots.anySatisfy({ $0.startDate == previousEndDateNoBreak })
                if let previousEndDate, let previousEndDateNoBreak {
                    let differenceWithBreak = rotationStartDate.timeIntervalSince(previousEndDate)
                    let differenceWithoutBreak = rotationStartDate.timeIntervalSince(previousEndDateNoBreak)
                    print("difference w break", differenceWithBreak)
                    print("difference w/o break", differenceWithoutBreak)
                    let timeDifferenceLimitInSeconds = Double(timeDifferenceLimit * 60)
                    var difference = differenceWithBreak
                    if differenceWithBreak <= 0 {
                        difference = differenceWithoutBreak
                    } else if differenceWithBreak > timeDifferenceLimitInSeconds && differenceWithoutBreak > timeDifferenceLimitInSeconds {
                        difference = noBreakAlreadyTested ? differenceWithBreak : max(differenceWithBreak, differenceWithoutBreak)
                    }

                    if difference > timeDifferenceLimitInSeconds && rotationStartDate.addingTimeInterval(-difference) != previousEndDate {
                        courts.removeAll(where: { index in freeCourtPreviousRotation.contains(index)
                        })
                        freeCourtPerRotation[rotationIndex] = courts
                        courts = freeCourtPreviousRotation
                        rotationStartDate = rotationStartDate.addingTimeInterval(-difference)
                    }
                }
            } else if let first = availableMatchs.first {
                let duration = first.matchFormat.getEstimatedDuration(additionalEstimationDuration)
                let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: duration, courtsUnavailability: courtsUnavailability)
                if courtsUnavailable == numberOfCourtsAvailablePerRotation {
                    print("issue")
                    issueFound = true
                }
            }

            dispatchCourts(availableCourts: numberOfCourtsAvailablePerRotation, courts: courts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: rotationStartDate, freeCourtPerRotation: &freeCourtPerRotation, courtsUnavailability: courtsUnavailability)
            rotationIndex += 1
        }
        
        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])
            }
        }
        
        
        return MatchDispatcher(timedMatches: slots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, issueFound: issueFound)
    }
    
    func dispatchCourts(availableCourts: Int, courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]], courtsUnavailability: [DateInterval]?) {
        var matchPerRound = [String: Int]()
        var minimumTargetedEndDate: Date = rotationStartDate
        print("dispatchCourts", courts.sorted(), rotationStartDate, rotationIndex)
        for (courtPosition, courtIndex) in courts.sorted().enumerated() {
            if let first = availableMatchs.first(where: { match in
                print("trying to find a match for \(courtIndex) in \(rotationIndex)")
                let roundObject = match.roundObject!
                let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: match.matchFormat.getEstimatedDuration(additionalEstimationDuration), courtsUnavailability: courtsUnavailability)
                print("courtsUnavailable \(courtsUnavailable)")
                if courtPosition >= availableCourts - courtsUnavailable {
                    return false
                }
                
                let canBePlayed = roundMatchCanBePlayed(match, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate)
                
                let currentRotationSameRoundMatches = matchPerRound[roundObject.id] ?? 0
                
                let roundMatchesCount = roundObject.playedMatches().count

                if shouldHandleUpperRoundSlice {
                    print("shouldHandleUpperRoundSlice \(roundMatchesCount)")
                    if roundObject.parent == nil && roundMatchesCount > courts.count {
                        print("roundMatchesCount \(roundMatchesCount) > \(courts.count)")
                        if currentRotationSameRoundMatches >= min(roundMatchesCount / 2, courts.count) {
                            print("return false, \(currentRotationSameRoundMatches) >= \(min(roundMatchesCount / 2, courts.count))")
                            return false
                        }
                    }
                }
                
                //if all is ok, we do a final check to see if the first
                let indexInRound = match.indexInRound()

                print("Upper Round, index > 0, first Match of round \(indexInRound) and more than one court available; looking for next match (same round) \(indexInRound + 1)")
                if roundObject.parent == nil && roundObject.index > 0, indexInRound == 0, let nextMatch = match.next() {
                    guard courtPosition < courts.count - 1, courts.count > 1 else {
                        print("next match and this match can not be played at the same time, returning false")
                        return false
                    }
                    if canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) {
                        
                        print("next match and this match can be played, returning true")
                        
                        return true
                    }
                }
                

                //not adding a last match of a 4-match round (final not included obviously)
                print("\(currentRotationSameRoundMatches) modulo \(currentRotationSameRoundMatches%2) same round match is even, index of round is not 0 and upper bracket. If it's not the last court available \(courtIndex) == \(courts.count - 1)")
                if roundMatchesCount <= 4 && currentRotationSameRoundMatches%2 == 0 && roundObject.index != 0 && roundObject.parent == nil && ((courts.count > 1 && courtPosition >= courts.count - 1) || courts.count == 1 && availableCourts > 1) {
                    print("we return false")
                    return false
                }

                
                return canBePlayed
            }) {
                print(first.roundObject!.roundTitle(), first.matchTitle(), courtIndex, rotationStartDate)
                
                if first.roundObject!.parent == nil {
                    if let roundIndex = matchPerRound[first.roundObject!.id] {
                        matchPerRound[first.roundObject!.id] = roundIndex + 1
                    } else {
                        matchPerRound[first.roundObject!.id] = 1
                    }
                }
                let timeMatch = TimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, startDate: rotationStartDate, durationLeft: first.matchFormat.getEstimatedDuration(additionalEstimationDuration), minimumBreakTime: first.matchFormat.breakTime.breakTime)
                slots.append(timeMatch)
                availableMatchs.removeAll(where: { $0.id == first.id })
            } else {
                freeCourtPerRotation[rotationIndex]!.append(courtIndex)
            }
        }
        
        if freeCourtPerRotation[rotationIndex]!.count == availableCourts {
            print("no match found to be put in this rotation, check if we can put anything to another date")
            freeCourtPerRotation[rotationIndex] = []
            let courtsUsed = getNextEarliestAvailableDate(from: slots)
            var freeCourts: [Int] = []
            if courtsUsed.isEmpty {
                freeCourts = (0..<availableCourts).map { $0 }
            } else {
                freeCourts = courtsUsed.filter { (courtIndex, availableDate) in
                    availableDate <= minimumTargetedEndDate
                }.sorted(by: \.1).map { $0.0 }
            }
            

            if let first = availableMatchs.first {
                let duration = first.matchFormat.getEstimatedDuration(additionalEstimationDuration)
                let courtsUnavailable = courtsUnavailable(startDate: minimumTargetedEndDate, duration: duration, courtsUnavailability: courtsUnavailability)
                if courtsUnavailable < availableCourts {
                    dispatchCourts(availableCourts: availableCourts, courts: freeCourts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: minimumTargetedEndDate, freeCourtPerRotation: &freeCourtPerRotation, courtsUnavailability: courtsUnavailability)
                }
            }
        }
    }
    
    @discardableResult func updateBracketSchedule(tournament: Tournament, fromRoundId roundId: String?, fromMatchId matchId: String?, startDate: Date) -> Bool {

        let upperRounds: [Round] = tournament.rounds()
        let allMatches: [Match] = tournament.allMatches()

        var rounds = [Round]()
        
        if shouldEndRoundBeforeStartingNext {
            rounds = upperRounds.flatMap {
                [$0] + $0.loserRoundsAndChildren()
            }
        } else {
            rounds = upperRounds.map {
                $0
            } + upperRounds.flatMap {
                $0.loserRoundsAndChildren()
            }
        }
        
        let flattenedMatches = rounds.flatMap { round in
            round._matches().filter({ $0.disabled == 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(numberOfCourtsAvailablePerRotation: tournament.courtCount, 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 DataStore.shared.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 0 }
        let groupedBy = Dictionary(grouping: courtsUnavailability, by: { $0.courtIndex })
        let courts = groupedBy.keys
        return courts.filter {
            courtUnavailable(courtIndex: $0, from: startDate, to: endDate, source: courtsUnavailability)
        }.count
    }
    
    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 updateSchedule(tournament: Tournament) -> Bool {
        let lastDate = updateGroupStageSchedule(tournament: tournament)
        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)
    }
}

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)
    }
}
