//
//  Match.swift
//  Padel Tournament
//
//  Created by razmig on 10/03/2024.
//

import Foundation
import LeStorage

@Observable
final class Match: BaseMatch, SideStorable {
    
    static func == (lhs: Match, rhs: Match) -> Bool {
        lhs.id == rhs.id && lhs.startDate == rhs.startDate
    }

    static func setServerTitle(upperRound: Round, matchIndex: Int) -> String {
        if upperRound.index == 0 { return upperRound.roundTitle() }
        return upperRound.roundTitle() + " #" + (matchIndex + 1).formatted()
    }
    
    var byeState: Bool = false
    
    init(round: String? = nil, groupStage: String? = nil, startDate: Date? = nil, endDate: Date? = nil, index: Int, format: MatchFormat? = nil, servingTeamId: String? = nil, winningTeamId: String? = nil, losingTeamId: String? = nil, name: String? = nil, disabled: Bool = false, courtIndex: Int? = nil, confirmed: Bool = false) {
        
        super.init(round: round, groupStage: groupStage, startDate: startDate, endDate: endDate, index: index, format: format, servingTeamId: servingTeamId, winningTeamId: winningTeamId, losingTeamId: losingTeamId, name: name, disabled: disabled, courtIndex: courtIndex, confirmed: confirmed)
        
    }
    
    required init(from decoder: Decoder) throws {
        try super.init(from: decoder)
    }
    
    required public init() {
        super.init()
    }
    
    func setMatchName(_ serverName: String?) {
        self.name = serverName
    }
    
    func isFromLastRound() -> Bool {
        guard let roundObject, roundObject.parent == nil else { return false }
        guard let currentTournament = currentTournament() else { return false }
        if currentTournament.rounds().count - 1 == roundObject.index {
            return true
        } else {
            return false
        }
    }
    
    var tournamentStore: TournamentStore? {
        if let id = self.store?.identifier {
            return TournamentLibrary.shared.store(tournamentId: id)
        }
        fatalError("missing store for \(String(describing: type(of: self)))")
    }
    
    var courtIndexForSorting: Int {
        courtIndex ?? Int.max
    }
    
    // MARK: - Computed dependencies

    var teamScores: [TeamScore] {
        guard let tournamentStore = self.tournamentStore else { return [] }
        return tournamentStore.teamScores.filter { $0.match == self.id }
    }
    
    // MARK: -
    
    override func deleteDependencies() {
        let teamScores = self.teamScores
        for teamScore in teamScores {
            teamScore.deleteDependencies()
        }
        self.tournamentStore?.teamScores.deleteDependencies(teamScores)
    }
    
    func indexInRound(in matches: [Match]? = nil) -> Int {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
    let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
    print("func indexInRound(in", matches?.count, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
        if groupStage != nil {
            return index
        } else if let index = (matches ?? roundObject?.playedMatches().sorted(by: \.index))?.firstIndex(where: { $0.id == id }) {
            return index
        }
        return RoundRule.matchIndexWithinRound(fromMatchIndex: index)
    }
    
    func matchWarningSubject() -> String {
        [roundTitle(), matchTitle(.short)].compacted().joined(separator: " ")
    }
    
    func matchWarningMessage() -> String {
        [roundAndMatchTitle(), startDate?.localizedDate(), courtName(), matchFormat.computedLongLabel].compacted().joined(separator: "\n")
    }
    
    func matchTitle(_ displayStyle: DisplayStyle = .wide, inMatches matches: [Match]? = nil) -> String {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
    let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
    print("func matchTitle", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
        
        if roundObject?.groupStageLoserBracket == true {
            return "\(index)\(index.ordinalFormattedSuffix()) place"
        }
        if let groupStageObject {
            return groupStageObject.localizedMatchUpLabel(for: index)
        }

        switch displayStyle {
        case .wide, .title:
            return "Match \(indexInRound(in: matches) + 1)"
        case .short:
            return "n˚\(indexInRound(in: matches) + 1)"
        }
    }
    
    func isSeedLocked(atTeamPosition teamPosition: TeamPosition) -> Bool {
        return previousMatch(teamPosition)?.disabled == true
    }
    
    func unlockSeedPosition(atTeamPosition teamPosition: TeamPosition) {
        previousMatch(teamPosition)?.enableMatch()
    }
    
    @discardableResult
    func lockAndGetSeedPosition(atTeamPosition teamPosition: TeamPosition) -> Int {
        let matchIndex = index
        previousMatch(teamPosition)?.disableMatch()
        return matchIndex * 2 + teamPosition.rawValue
    }
    
    func isSeededBy(team: TeamRegistration, inTeamPosition teamPosition: TeamPosition) -> Bool {
        guard let roundObject, roundObject.isUpperBracket() else { return false }
        guard let bracketPosition = team.bracketPosition else { return false }
        return index * 2 + teamPosition.rawValue == bracketPosition
    }
    
    func estimatedEndDate(_ additionalEstimationDuration: Int) -> Date? {
        let minutesToAdd = Double(matchFormat.getEstimatedDuration(additionalEstimationDuration))
        return startDate?.addingTimeInterval(minutesToAdd * 60.0)
    }
    
    func winner() -> TeamRegistration? {
        guard let winningTeamId else { return nil }
        return self.tournamentStore?.teamRegistrations.findById(winningTeamId)
    }
    
    func loser() -> TeamRegistration? {
        guard let losingTeamId else { return nil }
        return self.tournamentStore?.teamRegistrations.findById(losingTeamId)
    }

    
    func localizedStartDate() -> String {
        if let startDate {
            return startDate.formatted(date: .abbreviated, time: .shortened)
        } else {
            return ""
        }
    }
    
    func scoreLabel() -> String {
        if hasWalkoutTeam() == true {
            return "WO"
        }
        let scoreOne = teamScore(.one)?.score?.components(separatedBy: ",") ?? []
        let scoreTwo = teamScore(.two)?.score?.components(separatedBy: ",") ?? []
        let tuples = zip(scoreOne, scoreTwo).map { ($0, $1) }
        let scores = tuples.map { $0 + "/" + $1 }.joined(separator: " ")
        return scores
    }
    
    func cleanScheduleAndSave(_ targetStartDate: Date? = nil) {
        startDate = targetStartDate ?? startDate
        confirmed = false
        endDate = nil
        followingMatch()?.cleanScheduleAndSave(nil)
        _loserMatch()?.cleanScheduleAndSave(nil)
        self.tournamentStore?.matches.addOrUpdate(instance: self)
    }

    func resetMatch() {
        losingTeamId = nil
        winningTeamId = nil
        endDate = nil
        removeCourt()
        servingTeamId = nil
        groupStageObject?.updateGroupStageState()
        roundObject?.updateTournamentState()
        teams().forEach({ $0.resetRestingTime() })
    }
    
    func resetScores() {
        teamScores.forEach({ $0.score = nil })
        self.tournamentStore?.teamScores.addOrUpdate(contentOfs: teamScores)
    }
    
    func teamWillBeWalkOut(_ team: TeamRegistration) {
        resetMatch()
        let existingTeamScore = teamScore(ofTeam: team) ?? TeamScore(match: id, team: team)
        existingTeamScore.walkOut = 1
        self.tournamentStore?.teamScores.addOrUpdate(instance: existingTeamScore)
    }

    func luckyLosers() -> [TeamRegistration] {
        return roundObject?.previousRound()?.losers() ?? []
    }
    
    func isWalkOutSpot(_ teamPosition: TeamPosition) -> Bool {
        return teamScore(teamPosition)?.walkOut == 1
    }
    
    func setLuckyLoser(team: TeamRegistration, teamPosition: TeamPosition) {
        resetMatch()
        
        let matchIndex = index
        let position = matchIndex * 2 + teamPosition.rawValue
        
        let previousScores = teamScores.filter({ $0.luckyLoser == position })
        self.tournamentStore?.teamScores.delete(contentOfs: previousScores)
                
        let teamScoreLuckyLoser = teamScore(ofTeam: team) ?? TeamScore(match: id, team: team)
        teamScoreLuckyLoser.luckyLoser = position
        self.tournamentStore?.teamScores.addOrUpdate(instance: teamScoreLuckyLoser)
    }

    func disableMatch() {
        _toggleMatchDisableState(true)
    }
    
    func enableMatch() {
        _toggleMatchDisableState(false)
    }
    
    private func _loserMatch() -> Match? {
        let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: index)
        return roundObject?.loserRounds().first?.getMatch(atMatchIndexInRound: indexInRound / 2)
    }

    func _toggleLoserMatchDisableState(_ state: Bool) {
        guard let loserMatch = _loserMatch() else { return }
        guard let next = _otherMatch() else { return }
        loserMatch.byeState = true
        if next.disabled {
            loserMatch.byeState = false
        }
        loserMatch._toggleMatchDisableState(state, forward: true)
    }
    
    fileprivate func _otherMatch() -> Match? {
        guard let round else { return nil }
        guard index > 0 else { return nil }
        let nextIndex = (index - 1) / 2
        let topMatchIndex = (nextIndex * 2) + 1
        let bottomMatchIndex = (nextIndex + 1) * 2
        let isTopMatch = topMatchIndex + 1 == index
        let lookingForIndex = isTopMatch ? topMatchIndex : bottomMatchIndex
        
        return self.tournamentStore?.matches.first(where: { $0.round == round && $0.index == lookingForIndex })
        
    }
    
    private func _forwardMatch(inRound round: Round) -> Match? {
        guard let roundObjectNextRound = round.nextRound() else { return nil }
        let nextIndex = (index - 1) / 2
        return self.tournamentStore?.matches.first(where: { $0.round == roundObjectNextRound.id && $0.index == nextIndex })
    }

    func _toggleForwardMatchDisableState(_ state: Bool) {
        guard let roundObject else { return }
        guard roundObject.parent != nil else { return }
        guard let forwardMatch = _forwardMatch(inRound: roundObject) else { return }
        guard let next = _otherMatch() else { return }
        if next.disabled && byeState == false && next.byeState == false {
            if forwardMatch.disabled != state || forwardMatch.byeState {
                forwardMatch.byeState = false
                forwardMatch._toggleMatchDisableState(state, forward: true)
            }
        } else if byeState && next.byeState {
            print("don't disable forward match")
            if forwardMatch.byeState || forwardMatch.disabled {
                forwardMatch.byeState = false
                forwardMatch._toggleMatchDisableState(false, forward: true)
            }
        } else if forwardMatch.byeState == false {
            forwardMatch.byeState = true
            forwardMatch._toggleMatchDisableState(false, forward: true)
        } else if forwardMatch.disabled != state {
            forwardMatch._toggleMatchDisableState(state, forward: true)
        }
        
//        if next.disabled == false {
//            forwardMatch.byeState = state
//        }
        
        
        
//        
//        if next.disabled == state {
//            if next.byeState != byeState {
//                //forwardMatch.byeState = state
//                forwardMatch._toggleMatchDisableState(state)
//            } else {
//                forwardMatch._toggleByeState(state)
//            }
//        } else {
//        }
//        forwardMatch._toggleByeState(state)
    }
    
    func isSeededBy(team: TeamRegistration) -> Bool {
        isSeededBy(team: team, inTeamPosition: .one) || isSeededBy(team: team, inTeamPosition: .two)
    }
    
    func isSeeded() -> Bool {
        return isSeededAt(.one) || isSeededAt(.two)
    }
    
    func isSeededAt(_ teamPosition: TeamPosition) -> Bool {
        if let team = team(teamPosition) {
            return isSeededBy(team: team)
        }
        return false
    }
    
    func _toggleMatchDisableState(_ state: Bool, forward: Bool = false, single: Bool = false) {
        //if disabled == state { return }
        let currentState = disabled
        disabled = state
        

        if disabled != currentState {
            do {
                try self.tournamentStore?.teamScores.delete(contentOfs: teamScores)
            } catch {
                Logger.error(error)
            }
        }
        if state == true, state != currentState {
            let teams = teams()
            for team in teams {
                if isSeededBy(team: team) {
                    team.bracketPosition = nil
                    self.tournamentStore?.teamRegistrations.addOrUpdate(instance: team)
                }
            }
        }
        //byeState = false
        
        if state != currentState {
            roundObject?._cachedSeedInterval = nil
            name = nil
            do {
                try self.tournamentStore?.matches.addOrUpdate(instance: self)
            } catch {
                Logger.error(error)
            }
        }

        if single == false {
            _toggleLoserMatchDisableState(state)
            if forward {
                _toggleForwardMatchDisableState(state)
            } else {
                topPreviousRoundMatch()?._toggleMatchDisableState(state)
                bottomPreviousRoundMatch()?._toggleMatchDisableState(state)
            }
        }
    }

    func next() -> Match? {
        guard let tournamentStore = self.tournamentStore else { return nil }
        let matches: [Match] = tournamentStore.matches.filter { $0.round == round && $0.index > index && $0.disabled == false }
        return matches.sorted(by: \.index).first
    }
    
    func followingMatch() -> Match? {
        guard let nextRoundId = roundObject?.nextRound()?.id else { return nil }
        return getFollowingMatch(fromNextRoundId: nextRoundId)
    }
    
    func getFollowingMatch(fromNextRoundId nextRoundId: String) -> Match? {
        return self.tournamentStore?.matches.first(where: { $0.round == nextRoundId && $0.index == (index - 1) / 2 })
    }
    
    func getDuration() -> Int {
        if let tournament = currentTournament() {
            matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)
        } else {
            matchFormat.getEstimatedDuration()
        }
    }
    
    func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String? {
        if groupStage != nil { return groupStageObject?.groupStageTitle() }
        else if let roundObject { return roundObject.roundTitle() }
        else { return nil }
    }
    
    func roundAndMatchTitle(_ displayStyle: DisplayStyle = .wide) -> String {
        [roundTitle(displayStyle), matchTitle(displayStyle)].compactMap({ $0 }).joined(separator: " ")
    }
    
    func topPreviousRoundMatchIndex() -> Int {
        return index * 2 + 1
    }

    func bottomPreviousRoundMatchIndex() -> Int {
        return (index + 1) * 2
    }
    
    func topPreviousRoundMatch() -> Match? {
        guard let roundObject else { return nil }
        let topPreviousRoundMatchIndex = topPreviousRoundMatchIndex()
        let roundObjectPreviousRoundId = roundObject.previousRound()?.id
        return self.tournamentStore?.matches.first(where: { match in
            match.round != nil && match.round == roundObjectPreviousRoundId && match.index == topPreviousRoundMatchIndex
        })
    }
    
    func bottomPreviousRoundMatch() -> Match? {
        guard let roundObject else { return nil }
        let bottomPreviousRoundMatchIndex = bottomPreviousRoundMatchIndex()
        let roundObjectPreviousRoundId = roundObject.previousRound()?.id
        return self.tournamentStore?.matches.first(where: { match in
            match.round != nil && match.round == roundObjectPreviousRoundId && match.index == bottomPreviousRoundMatchIndex
        })
    }
    
    func previousMatch(_ teamPosition: TeamPosition) -> Match? {
        if teamPosition == .one {
            return topPreviousRoundMatch()
        } else {
            return bottomPreviousRoundMatch()
        }
    }
    
    func loserMatches() -> [Match] {
        guard let roundObject else { return [] }
        return [roundObject.upperBracketTopMatch(ofMatchIndex: index, previousRound: nil), roundObject.upperBracketBottomMatch(ofMatchIndex: index, previousRound: nil)].compactMap({ $0 })
    }
    
    func loserMatch(_ teamPosition: TeamPosition) -> Match? {
        if teamPosition == .one {
            return roundObject?.upperBracketTopMatch(ofMatchIndex: index, previousRound: nil)
        } else {
            return roundObject?.upperBracketBottomMatch(ofMatchIndex: index, previousRound: nil)
        }

    }
    
    var computedOrder: Int {
        if let groupStageObject {
            return (groupStageObject.index + 1) * 100 + groupStageObject.indexOf(index)
        }
        guard let roundObject else { return index }
        return (300 - (roundObject.theoryCumulativeMatchCount * 10 + roundObject.index * 22)) * 10 + indexInRound()
    }
    
    func previousMatches() -> [Match] {
        guard let roundObject else { return [] }
        guard let tournamentStore = self.tournamentStore else { return [] }
        
        let roundObjectPreviousRoundId = roundObject.previousRound()?.id
        return tournamentStore.matches.filter { match in
            match.round == roundObjectPreviousRoundId && (match.index == topPreviousRoundMatchIndex() || match.index == bottomPreviousRoundMatchIndex())
        }.sorted(by: \.index)
    }
    
    var matchFormat: MatchFormat {
        get {
            format ?? .defaultFormatForMatchType(.groupStage)
        }
        set {
            format = newValue
        }
    }
    
    func removeWalkOut() {
        teamScores.forEach { teamScore in
            teamScore.walkOut = nil
            teamScore.score = nil
        }
        tournamentStore?.teamScores.addOrUpdate(contentOfs: teamScores)
        endDate = nil
        winningTeamId = nil
        losingTeamId = nil
        groupStageObject?.updateGroupStageState()
        roundObject?.updateTournamentState()
        updateFollowingMatchTeamScore()
    }

    func setWalkOut(_ teamPosition: TeamPosition) {
        let teamScoreWalkout = teamScore(teamPosition) ?? TeamScore(match: id, team: team(teamPosition))
        teamScoreWalkout.walkOut = 0
        teamScoreWalkout.score = matchFormat.defaultWalkOutScore(true).compactMap({ String($0) }).joined(separator: ",")
        let teamScoreWinning = teamScore(teamPosition.otherTeam) ?? TeamScore(match: id, team: team(teamPosition.otherTeam))
        teamScoreWinning.walkOut = nil
        teamScoreWinning.score = matchFormat.defaultWalkOutScore(false).compactMap({ String($0) }).joined(separator: ",")
        do {
            try self.tournamentStore?.teamScores.addOrUpdate(contentOfs: [teamScoreWalkout, teamScoreWinning])
        } catch {
            Logger.error(error)
        }

        if endDate == nil {
            endDate = Date()
        }
        teams().forEach({ $0.resetRestingTime() })
        winningTeamId = teamScoreWinning.teamRegistration
        losingTeamId = teamScoreWalkout.teamRegistration
        groupStageObject?.updateGroupStageState()
        roundObject?.updateTournamentState()
        updateFollowingMatchTeamScore()
    }
    
    func setUnfinishedScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) {
        updateScore(fromMatchDescriptor: matchDescriptor)
        if endDate == nil {
            endDate = Date()
        }
        if startDate == nil {
            startDate = endDate?.addingTimeInterval(Double(-getDuration()*60))
        } else if let startDate, let endDate, startDate >= endDate {
            self.startDate = endDate.addingTimeInterval(Double(-getDuration()*60))
        }
        confirmed = true
    }
    
    func setScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) {
        updateScore(fromMatchDescriptor: matchDescriptor)
        if endDate == nil {
            endDate = Date()
        }
        if startDate == nil {
            startDate = endDate?.addingTimeInterval(Double(-getDuration()*60))
        } else if let startDate, let endDate, startDate >= endDate {
            self.startDate = endDate.addingTimeInterval(Double(-getDuration()*60))
        }
        
        let teamOne = team(matchDescriptor.winner)
        let teamTwo = team(matchDescriptor.winner.otherTeam)
        
        teamOne?.hasArrived()
        teamTwo?.hasArrived()
        teamOne?.resetRestingTime()
        teamTwo?.resetRestingTime()

        winningTeamId = teamOne?.id
        losingTeamId = teamTwo?.id
        
        confirmed = true
        
        groupStageObject?.updateGroupStageState()
        roundObject?.updateTournamentState()
        if let tournament = currentTournament(), let endDate, let startDate {
            if endDate.isEarlierThan(tournament.startDate) {
                tournament.startDate = startDate
            }
            do {
                try DataStore.shared.tournaments.addOrUpdate(instance: tournament)
            } catch {
                Logger.error(error)
            }
        }
        updateFollowingMatchTeamScore()
    }

    func updateScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) {
        let teamScoreOne = teamScore(.one) ?? TeamScore(match: id, team: team(.one))
        teamScoreOne.score = matchDescriptor.teamOneScores.joined(separator: ",")
        let teamScoreTwo = teamScore(.two) ?? TeamScore(match: id, team: team(.two))
        teamScoreTwo.score = matchDescriptor.teamTwoScores.joined(separator: ",")
        
        if matchDescriptor.teamOneScores.last?.contains("-") == true && matchDescriptor.teamTwoScores.last?.contains("-") == false {
            teamScoreTwo.score = (matchDescriptor.teamTwoScores.dropLast() + [matchDescriptor.teamTwoScores.last! + "-0"]).joined(separator: ",")
        } else if matchDescriptor.teamTwoScores.last?.contains("-") == true && matchDescriptor.teamOneScores.last?.contains("-") == false {
            teamScoreOne.score = (matchDescriptor.teamOneScores.dropLast() + [matchDescriptor.teamOneScores.last! + "-0"]).joined(separator: ",")
        }
        
        do {
            try self.tournamentStore?.teamScores.addOrUpdate(contentOfs: [teamScoreOne, teamScoreTwo])
        } catch {
            Logger.error(error)
        }
        matchFormat = matchDescriptor.matchFormat
    }
    
    func updateFollowingMatchTeamScore() {
        followingMatch()?.updateTeamScores()
        _loserMatch()?.updateTeamScores()
    }
    
    func resetTeamScores(outsideOf newTeamScores: [TeamScore]) {
        let ids = newTeamScores.map { $0.id }
        let teamScores = teamScores.filter({ ids.contains($0.id) == false })
        if teamScores.isEmpty == false {
            self.tournamentStore?.teamScores.delete(contentOfs: teamScores)
            followingMatch()?.resetTeamScores(outsideOf: [])
            _loserMatch()?.resetTeamScores(outsideOf: [])
        }
    }

    func createTeamScores() -> [TeamScore] {
        let teamOne = team(.one)
        let teamTwo = team(.two)
        let teams = [teamOne, teamTwo].compactMap({ $0 }).map { TeamScore(match: id, team: $0) }
        return teams
    }
    
    func getOrCreateTeamScores() -> [TeamScore] {
        let teamOne = team(.one)
        let teamTwo = team(.two)
        let teams = [teamOne, teamTwo].compactMap({ $0 }).map { teamScore(ofTeam: $0) ?? TeamScore(match: id, team: $0) }
        return teams
    }
    
    func updateTeamScores() {
        let teams = getOrCreateTeamScores()

        self.tournamentStore?.teamScores.addOrUpdate(contentOfs: teams)
        resetTeamScores(outsideOf: teams)
        if teams.isEmpty == false {
            updateFollowingMatchTeamScore()
        }
    }
    
    func validateMatch(fromStartDate: Date, toEndDate: Date, fieldSetup: MatchFieldSetup, forced: Bool = false) {
        if hasEnded() == false {
            startDate = fromStartDate

            switch fieldSetup {
            case .fullRandom:
                if let _courtIndex = allCourts().randomElement() {
                    setCourt(_courtIndex)
                }
            case .random:
                let runningMatches: [Match] = DataStore.shared.runningMatches()
                if let _courtIndex = availableCourts(runningMatches: runningMatches).randomElement() {
                    setCourt(_courtIndex)
                }
            case .field(let _courtIndex):
                setCourt(_courtIndex)
            }

        } else {
            startDate = fromStartDate
            endDate = toEndDate
        }
        
        if let startDate, startDate.timeIntervalSinceNow <= 300 {
            confirmed = true
        } else {
            confirmed = false
        }
    }
    
    func courtName() -> String? {
        guard let courtIndex else { return nil }
        if let courtName = currentTournament()?.courtName(atIndex: courtIndex) {
            return courtName
        } else {
            return Court.courtIndexedTitle(atIndex: courtIndex)
        }
    }
    
    func courtName(for selectedIndex: Int) -> String {
        if let courtName = currentTournament()?.courtName(atIndex: selectedIndex) {
            return courtName
        } else {
            return Court.courtIndexedTitle(atIndex: selectedIndex)
        }
    }
    
    func courtCount() -> Int {
        return currentTournament()?.courtCount ?? 1
    }
    
    func courtIsAvailable(_ courtIndex: Int, in runningMatches: [Match]) -> Bool {
        let courtUsed = currentTournament()?.courtUsed(runningMatches: runningMatches) ?? []
        return courtUsed.contains(courtIndex) == false
    }
    
    func courtIsPreferred(_ courtIndex: Int) -> Bool {
        return false
    }
    
    func allCourts() -> [Int] {
        let availableCourts = Array(0..<courtCount())
        return availableCourts
    }
    
    func availableCourts(runningMatches: [Match]) -> [Int] {
        let courtUsed = currentTournament()?.courtUsed(runningMatches: runningMatches) ?? []
        return Set(allCourts().map { $0 }).subtracting(Set(courtUsed)).sorted()
    }
    
    func removeCourt() {
        courtIndex = nil
    }
    
    func setCourt(_ courtIndex: Int) {
        self.courtIndex = courtIndex
    }
    
    func canBeStarted(inMatches matches: [Match], checkCanPlay: Bool) -> Bool {
        let teams = teamScores
        guard teams.count == 2 else {
            //print("teams.count != 2")
            return false
        }
        guard hasEnded() == false else { return false }
        guard hasStarted() == false else { return false }
        return teams.compactMap({ $0.team }).allSatisfy({
            ((checkCanPlay && $0.canPlay()) || checkCanPlay == false) && isTeamPlaying($0, inMatches: matches) == false
        })
    }
    
    func isTeamPlaying(_ team: TeamRegistration, inMatches matches: [Match]) -> Bool {
        return matches.filter({ $0.teamScores.compactMap { $0.teamRegistration }.contains(team.id) }).isEmpty == false
    }

    var computedStartDateForSorting: Date {
        return startDate ?? .distantFuture
    }

    var computedEndDateForSorting: Date {
        return endDate ?? .distantFuture
    }

    func hasSpaceLeft() -> Bool {
        return teamScores.count < 2
    }

    func isReady() -> Bool {
        return teamScores.count >= 2
//        teams().count == 2
    }
    
    func isEmpty() -> Bool {
        return teamScores.isEmpty
//        teams().isEmpty
    }
    
    func hasEnded() -> Bool {
        return endDate != nil
    }
    
    func isGroupStage() -> Bool {
        return groupStage != nil
    }
    
    func isBracket() -> Bool {
        return round != nil
    }
        
    func walkoutTeam() -> [TeamRegistration] { 
        //walkout 0 means real walkout, walkout 1 means lucky loser situation
        return scores().filter({ $0.walkOut == 0 }).compactMap { $0.team }
    }
    
    func hasWalkoutTeam() -> Bool {
        return walkoutTeam().isEmpty == false
    }
    
    func currentTournament() -> Tournament? {
        return groupStageObject?.tournamentObject() ?? roundObject?.tournamentObject()
    }

    func tournamentId() -> String? {
        return groupStageObject?.tournament ?? roundObject?.tournament
    }
    
    func scores() -> [TeamScore] {
        guard let tournamentStore = self.tournamentStore else { return [] }
        return tournamentStore.teamScores.filter { $0.match == id }
    }
    
    func teams() -> [TeamRegistration] {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
    let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
    print("func teams()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif

        if groupStage != nil {
            return [groupStageProjectedTeam(.one), groupStageProjectedTeam(.two)].compactMap { $0 }
        }
        guard let roundObject else { return [] }
        let previousRound = roundObject.previousRound()
        return [roundObject.roundProjectedTeam(.one, inMatch: self, previousRound: previousRound), roundObject.roundProjectedTeam(.two, inMatch: self, previousRound: previousRound)].compactMap { $0 }

//        return [roundProjectedTeam(.one), roundProjectedTeam(.two)].compactMap { $0 }
    }
    
    func scoreDifference(_ teamPosition: Int, atStep step: Int) -> (set: Int, game: Int)? {
        guard let teamScoreTeam = teamScore(.one), let teamScoreOtherTeam = teamScore(.two) else { return nil }
        var reverseValue = 1
        if teamPosition == team(.two)?.groupStagePositionAtStep(step) {
            reverseValue = -1
        }
        let endedSetsOne = teamScoreTeam.score?.components(separatedBy: ",").compactMap({ $0.components(separatedBy: "-").first }).compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreTeam.isWalkOut())
        let endedSetsTwo = teamScoreOtherTeam.score?.components(separatedBy: ",").compactMap({ $0.components(separatedBy: "-").first }).compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreOtherTeam.isWalkOut())
        var setDifference : Int = 0
        let zip = zip(endedSetsOne, endedSetsTwo)
        if matchFormat.setsToWin == 1 {
            setDifference = endedSetsOne[0] - endedSetsTwo[0]
        } else {
            setDifference = zip.filter { $0 > $1 }.count - zip.filter { $1 > $0 }.count
        }
        
        // si 3 sets et 3eme set super tie break, different des 2 premiers sets, alors super tie points ne sont pas des jeux et doivent etre compté comme un jeu
        
        if matchFormat.canSuperTie, endedSetsOne.count == 3 {
            let games = zip.map { ($0, $1) }
            let gameDifference = games.enumerated().map({ index, pair in
                if index < 2 {
                    return pair.0 - pair.1
                } else {
                    return pair.0 < pair.1 ? -1 : 1
                }
            })
            .reduce(0,+)
            return (setDifference * reverseValue, gameDifference * reverseValue)
        } else {
            let gameDifference = zip.map { ($0, $1) }.map { $0.0 - $0.1 }.reduce(0,+)
            return (setDifference * reverseValue, gameDifference * reverseValue)
        }
    }

    func groupStageProjectedTeam(_ team: TeamPosition) -> TeamRegistration? {
        guard let groupStageObject else { return nil }
        return groupStageObject.team(teamPosition: team, inMatchIndex: index)
    }
    
    func roundProjectedTeam(_ team: TeamPosition) -> TeamRegistration? {
        guard let roundObject else { return nil }
        let previousRound = roundObject.previousRound()
        return roundObject.roundProjectedTeam(team, inMatch: self, previousRound: previousRound)
    }
    
    func teamWon(_ team: TeamRegistration?) -> Bool {
        guard let winningTeamId else { return false }
        return winningTeamId == team?.id
    }

    func teamWon(atPosition teamPosition: TeamPosition) -> Bool {
        guard let winningTeamId else { return false }
        return winningTeamId == team(teamPosition)?.id
    }
    
    func team(_ team: TeamPosition) -> TeamRegistration? {
        #if _DEBUG_TIME //DEBUGING TIME
        let start = Date()
        defer {
            let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
            print("func match get team", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
        }
        #endif
        if groupStage != nil {
            return groupStageProjectedTeam(team)
        } else {
            return roundProjectedTeam(team)
        }
    }

    func teamNames(_ team: TeamRegistration?) -> [String]? {
        return team?.players().map { $0.playerLabel() }
    }

    func teamWalkOut(_ team: TeamRegistration?) -> Bool {
        return teamScore(ofTeam: team)?.isWalkOut() == true
    }
    
    func teamScore(_ team: TeamPosition) -> TeamScore? {
        return teamScore(ofTeam: self.team(team))
    }

    func teamScore(ofTeam team: TeamRegistration?) -> TeamScore? {
        return scores().first(where: { $0.teamRegistration == team?.id })
    }
    
    func isRunning() -> Bool { // at least a match has started
        return confirmed && hasStarted() && hasEnded() == false
    }
    
    func hasStarted() -> Bool { // meaning at least one match is over
        if let startDate {
            return startDate.timeIntervalSinceNow < 0 && confirmed
        }
        if hasEnded() {
            return true
        }
        return false
        
        //todo scores
//        if let score {
//            return score.hasEnded == false && score.sets.isEmpty == false
//        } else {
//            return false
//        }
    }
    
    var roundObject: Round? {
        guard let round else { return nil }
        return self.tournamentStore?.rounds.findById(round)
    }
    
    var groupStageObject: GroupStage? {
        guard let groupStage else { return nil }
        return self.tournamentStore?.groupStages.findById(groupStage)
    }
    
    var isLoserBracket: Bool {
        if let roundObject {
            if roundObject.parent != nil ||  roundObject.groupStageLoserBracket {
                return true
            }
        }
        return false
    }
    
    var matchType: MatchType {
        if isLoserBracket {
            return .loserBracket
        } else if isGroupStage() {
            return .groupStage
        } else {
            return .bracket
        }
    }
    
    var restingTimeForSorting: TimeInterval {
        (teams().compactMap({ $0.restingTime() }).max() ?? .distantFuture).timeIntervalSinceNow
    }
    
    func isValidSpot() -> Bool {
        previousMatches().allSatisfy({ $0.isSeeded() == false })
    }
    
    func expectedToBeRunning() -> Bool {
        guard let startDate else { return false }
        return confirmed == false && startDate.timeIntervalSinceNow < 0
    }
    
    func expectedFormattedStartDate(canBePlayedInSpecifiedCourt: Bool, availableCourts: [Int], estimatedStartDate: CourtIndexAndDate?, updatedField: Int?) -> String {
        guard let startDate else { return "" }
        guard hasEnded() == false, isRunning() == false else { return "" }
        let depthReadiness = depthReadiness()
        if depthReadiness == 0 {
            if canBePlayedInSpecifiedCourt {
                return "possible tout de suite"
            } else if let updatedField, availableCourts.contains(updatedField) {
                return "possible tout de suite \(courtName(for: updatedField))"
            } else if let first = availableCourts.first {
                return "possible tout de suite \(courtName(for: first))"
            } else if let estimatedStartDate {
                return "dans ~" + estimatedStartDate.1.timeElapsedString() + " " + courtName(for: estimatedStartDate.0)
            }
            return "était prévu à " + startDate.formattedAsHourMinute()
        } else if depthReadiness == 1 {
            return "possible prochaine rotation"
        } else {
            return "dans \(depthReadiness) rotation\(depthReadiness.pluralSuffix), ~\((getDuration() * depthReadiness).durationInHourMinutes())"
        }
    }
    
    func runningDuration() -> String {
        guard let startDate else { return "" }
        return " depuis " + startDate.timeElapsedString()
    }
    
    func canBePlayedInSpecifiedCourt(runningMatches: [Match]) -> Bool {
        guard let courtIndex else { return false }
        if expectedToBeRunning() {
            return courtIsAvailable(courtIndex, in: runningMatches)
        } else {
            return true
        }
    }
    
    typealias CourtIndexAndDate = (courtIndex: Int, startDate: Date)
    
    func nextCourtsAvailable(availableCourts: [Int], runningMatches: [Match]) -> [CourtIndexAndDate] {
        guard let tournament = currentTournament() else { return [] }
        let startDate = Date().withoutSeconds()
        if runningMatches.isEmpty {
            return availableCourts.map {
                ($0, startDate)
            }
        }
        
        let optionalDates : [CourtIndexAndDate?] = runningMatches.map({ match in
            guard let endDate = match.estimatedEndDate(tournament.additionalEstimationDuration) else { return nil }
            guard let courtIndex = match.courtIndex else { return nil }
            if endDate <= startDate {
                return (courtIndex, startDate.addingTimeInterval(600))
            } else {
                return (courtIndex, endDate)
            }
        })
        
        let dates : [CourtIndexAndDate] = optionalDates.compacted().sorted { a, b in
            a.1 < b.1
        }
        return dates
    }
    
    func estimatedStartDate(availableCourts: [Int], runningMatches: [Match]) -> CourtIndexAndDate? {
        guard isReady() else { return nil }
        guard let tournament = currentTournament() else { return nil }
        let availableCourts = nextCourtsAvailable(availableCourts: availableCourts, runningMatches: runningMatches)
        return availableCourts.first(where: { (courtIndex, startDate) in
            let endDate = startDate.addingTimeInterval(TimeInterval(matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) * 60)
            if tournament.courtUnavailable(courtIndex: courtIndex, from: startDate, to: endDate) == false {
                return true
            }
            
            return false
        })
    }
    
    func depthReadiness() -> Int {
        // Base case: If this match is ready, the depth is 0
        if isReady() {
            return 0
        }
        
        // Recursive case: If not ready, check the maximum depth of readiness among previous matches
        // If previousMatches() is empty, return a default depth of -1
        let previousDepth = ancestors().map { $0.depthReadiness() }.max() ?? -1
        return previousDepth + 1
    }
    
    func ancestors() -> [Match] {
        previousMatches() + loserMatches()
    }

    func matchSpots() -> [MatchSpot] {
        [MatchSpot(match: self, teamPosition: .one), MatchSpot(match: self, teamPosition: .two)]
    }

    func insertOnServer() {
        self.tournamentStore?.matches.writeChangeAndInsertOnServer(instance: self)
        for teamScore in self.teamScores {
            teamScore.insertOnServer()
        }
    }
    
}

enum MatchDateSetup: Hashable, Identifiable {
    case inMinutes(Int)
    case now
    case customDate
    case previousRotation
    case nextRotation
    
    var id: Int { hashValue }
}

enum MatchFieldSetup: Hashable, Identifiable {
    case random
    case fullRandom
//        case firstAvailable
    case field(Int)
    
    var courtIndex: Int? {
        switch self {
        case .random:
            return nil
        case .fullRandom:
            return nil
        case .field(let int):
            return int
        }
    }
    
    var id: Int { hashValue }
}
