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

import Foundation
import LeStorage
import SwiftUI

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

    var id: String = Store.randomId()
    var tournament: String
    var index: Int
    var parent: String?
    private(set) var format: MatchFormat?
    var startDate: Date?
    
    internal init(tournament: String, index: Int, parent: String? = nil, matchFormat: MatchFormat? = nil, startDate: Date? = nil) {
        self.tournament = tournament
        self.index = index
        self.parent = parent
        self.format = matchFormat
        self.startDate = startDate
    }
    
    // MARK: - Computed dependencies
    
    func tournamentObject() -> Tournament? {
        return Store.main.findById(tournament)
    }
    
    func _matches() -> [Match] {
        return Store.main.filter { $0.round == self.id }
    }
    
    func getDisabledMatches() -> [Match] {
        return Store.main.filter { $0.round == self.id && $0.disabled == true }
    }

    // MARK: -
    
    var matchFormat: MatchFormat {
        get {
            format ?? .defaultFormatForMatchType(.bracket)
        }
        set {
            format = newValue
        }
    }
    
    
    func hasStarted() -> Bool {
        return playedMatches().anySatisfy({ $0.hasStarted() })
    }
    
    func hasEnded() -> Bool {
        return playedMatches().anySatisfy({ $0.hasEnded() == false }) == false
    }

    func upperMatches(ofMatch match: Match) -> [Match] {
        if parent != nil, previousRound() == nil, let parentRound {
            let matchIndex = match.index
            let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex)
            return [parentRound.getMatch(atMatchIndexInRound: indexInRound * 2), parentRound.getMatch(atMatchIndexInRound: indexInRound * 2 + 1)].compactMap({ $0 })
        }
        return []
    }

    func previousMatches(ofMatch match: Match) -> [Match] {
        guard let previousRound = previousRound() else { return [] }
        return Store.main.filter {
            ($0.index == match.topPreviousRoundMatchIndex() || $0.index == match.bottomPreviousRoundMatchIndex()) && $0.round == previousRound.id
        }
    }

    func precedentMatches(ofMatch match: Match) -> [Match] {
        let upper = upperMatches(ofMatch: match)
        if upper.isEmpty == false {
            return upper
        }
        let previous : [Match] = previousMatches(ofMatch: match)
        if previous.isEmpty == false && previous.allSatisfy({ $0.disabled }), let previousRound = previousRound() {
            return previous.flatMap({ previousRound.precedentMatches(ofMatch: $0) })
        } else {
            return previous
        }        
    }
    
    func team(_ team: TeamPosition, inMatch match: Match) -> TeamRegistration? {
        switch team {
        case .one:
            return roundProjectedTeam(.one, inMatch: match)
        case .two:
            return roundProjectedTeam(.two, inMatch: match)
        }
    }
    
    func seed(_ team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? {
        return Store.main.filter(isIncluded: {
            $0.tournament == tournament
            && $0.bracketPosition != nil
            && ($0.bracketPosition! / 2) == matchIndex
            && ($0.bracketPosition! % 2) == team.rawValue
        }).first
    }
    
    func seeds(inMatchIndex matchIndex: Int) -> [TeamRegistration] {
        return Store.main.filter(isIncluded: {
            $0.tournament == tournament
            && $0.bracketPosition != nil
            && ($0.bracketPosition! / 2) == matchIndex
        })
    }
    
    func seeds() -> [TeamRegistration] {
        let initialMatchIndex = RoundRule.matchIndex(fromRoundIndex: index)
        let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: index)
        return Store.main.filter(isIncluded: {
            $0.tournament == tournament
            && $0.bracketPosition != nil
            && ($0.bracketPosition! / 2) >= initialMatchIndex
            && ($0.bracketPosition! / 2) < initialMatchIndex + numberOfMatches
        })
    }

    func losers() -> [TeamRegistration] {
        return _matches().compactMap { $0.losingTeamId }.compactMap { Store.main.findById($0) }
    }
    
    func teams() -> [TeamRegistration] {
        return playedMatches().flatMap({ $0.teams() })
    }
    
    func roundProjectedTeam(_ team: TeamPosition, inMatch match: Match) -> TeamRegistration? {
        if isLoserBracket() == false, let seed = seed(team, inMatchIndex: match.index) {
            return seed
        }

        switch team {
        case .one:
            if let luckyLoser = match.teamScores.first(where: { $0.luckyLoser == match.index * 2 }) {
                return luckyLoser.team
            } else if let parent = upperBracketTopMatch(ofMatchIndex: match.index)?.losingTeamId {
                return Store.main.findById(parent)
            } else if let previousMatch = topPreviousRoundMatch(ofMatch: match) {
                if let teamId = previousMatch.winningTeamId {
                    return Store.main.findById(teamId)
                } else if previousMatch.disabled {
                    return previousMatch.teams().first
                }
            }
        case .two:
            if let luckyLoser = match.teamScores.first(where: { $0.luckyLoser == match.index * 2 + 1 }) {
                return luckyLoser.team
            } else if let parent = upperBracketBottomMatch(ofMatchIndex: match.index)?.losingTeamId {
                return Store.main.findById(parent)
            } else if let previousMatch = bottomPreviousRoundMatch(ofMatch: match) {
                if let teamId = previousMatch.winningTeamId {
                    return Store.main.findById(teamId)
                } else if previousMatch.disabled {
                    return previousMatch.teams().first
                }
            }
        }
        
        return nil
    }
    
    func upperBracketTopMatch(ofMatchIndex matchIndex: Int) -> Match? {
        let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex)
        if isLoserBracket(), previousRound() == nil, let parentRound = parentRound, let upperBracketTopMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2) {
            return upperBracketTopMatch
        }
        return nil
    }

    func upperBracketBottomMatch(ofMatchIndex matchIndex: Int) -> Match? {
        let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex)
        if isLoserBracket(), previousRound() == nil, let parentRound = parentRound, let upperBracketBottomMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2 + 1) {
            return upperBracketBottomMatch
        }
        return nil
    }
    
    func topPreviousRoundMatch(ofMatch match: Match) -> Match? {
        guard let previousRound = previousRound() else { return nil }
        let matches: [Match] = Store.main.filter {
            $0.index == match.topPreviousRoundMatchIndex() && $0.round == previousRound.id
        }
        return matches.sorted(by: \.index).first
    }
    
    func bottomPreviousRoundMatch(ofMatch match: Match) -> Match? {
        guard let previousRound = previousRound() else { return nil }
        let matches: [Match] = Store.main.filter {
            $0.index == match.bottomPreviousRoundMatchIndex() && $0.round == previousRound.id
        }
        return matches.sorted(by: \.index).first
    }
    
    func getMatch(atMatchIndexInRound matchIndexInRound: Int) -> Match? {
        Store.main.filter(isIncluded: {
            let index = RoundRule.matchIndexWithinRound(fromMatchIndex: $0.index)
            return $0.round == id && index == matchIndexInRound
        }).first
    }

    func enabledMatches() -> [Match] {
        return Store.main.filter { $0.round == self.id && $0.disabled == false }
    }
    
    func displayableMatches() -> [Match] {
        if index == 0 && isUpperBracket() {
            var matches : [Match?] = [playedMatches().first]
            matches.append(loserRounds().first?.playedMatches().first)
            return matches.compactMap({ $0 })
        } else {
            return playedMatches()
        }
    }
    
    func playedMatches() -> [Match] {
        if parent == nil {
            return enabledMatches()
        } else {
            return _matches()
        }
    }
    
    func previousRound() -> Round? {
        return Store.main.filter(isIncluded: { $0.tournament == tournament && $0.parent == parent && $0.index == index + 1 }).first
    }

    func nextRound() -> Round? {
        return Store.main.filter(isIncluded: { $0.tournament == tournament && $0.parent == parent && $0.index == index - 1 }).first
    }
    
    func loserRounds(forRoundIndex roundIndex: Int) -> [Round] {
        return loserRoundsAndChildren().filter({ $0.index == roundIndex }).sorted(by: \.theoryCumulativeMatchCount)
    }
    
    func isDisabled() -> Bool {
        return _matches().allSatisfy({ $0.disabled })
    }

    func isRankDisabled() -> Bool {
        return _matches().allSatisfy({ $0.disabled && $0.teamScores.isEmpty })
    }
    
    func resetFromRoundAllMatchesStartDate() {
        _matches().forEach({
            $0.startDate = nil
        })
        loserRoundsAndChildren().forEach { round in
            round.resetFromRoundAllMatchesStartDate()
        }
        nextRound()?.resetFromRoundAllMatchesStartDate()
    }
    
    func resetFromRoundAllMatchesStartDate(from match: Match) {
        let matches = _matches()
        if let index = matches.firstIndex(where: { $0.id == match.id }) {
            matches[index...].forEach { match in
                match.startDate = nil
            }
        }
        loserRoundsAndChildren().forEach { round in
            round.resetFromRoundAllMatchesStartDate()
        }
        nextRound()?.resetFromRoundAllMatchesStartDate()
    }
    
    func getActiveLoserRound() -> Round? {
        let rounds = loserRounds().filter({ $0.isDisabled() == false }).sorted(by: \.index).reversed()
        return rounds.first(where: { $0.hasStarted() && $0.hasEnded() == false }) ?? rounds.first
    }
    
    func enableRound() {
        _toggleRound(disable: false)
    }
    
    func disableRound() {
        _toggleRound(disable: true)
    }
    
    private func _toggleRound(disable: Bool) {
        let _matches = _matches()
        _matches.forEach { match in
            match.disabled = disable
            match.resetMatch()
            //we need to keep teamscores to handle disable ranking match round stuff
//            do {
//                try DataStore.shared.teamScores.delete(contentOfs: match.teamScores)
//            } catch {
//                Logger.error(error)
//            }
        }
        do {
            try DataStore.shared.matches.addOrUpdate(contentOfs: _matches)
        } catch {
            Logger.error(error)
        }
    }
    
    var cumulativeMatchCount: Int {
        var totalMatches = playedMatches().count
        if let parentRound {
            totalMatches += parentRound.cumulativeMatchCount
        }
        return totalMatches
    }
    
    func initialRound() -> Round? {
        if let parentRound {
            return parentRound.initialRound()
        } else {
            return self
        }
    }
    
    func estimatedEndDate(_ additionalEstimationDuration: Int) -> Date? {
        return enabledMatches().last?.estimatedEndDate(additionalEstimationDuration)
    }
    
    func getLoserRoundStartDate() -> Date? {
        return loserRoundsAndChildren().first(where: { $0.isDisabled() == false })?.enabledMatches().first?.startDate
    }
    
    func estimatedLoserRoundEndDate(_ additionalEstimationDuration: Int) -> Date? {
        let lastMatch = loserRoundsAndChildren().last(where: { $0.isDisabled() == false })?.enabledMatches().last
        return lastMatch?.estimatedEndDate(additionalEstimationDuration)
    }

    func disabledMatches() -> [Match] {
        return _matches().filter({ $0.disabled })
    }
    
    var theoryCumulativeMatchCount: Int {
        var totalMatches = RoundRule.numberOfMatches(forRoundIndex: index)
        if let parentRound {
            totalMatches += parentRound.theoryCumulativeMatchCount
        }
        return totalMatches
    }

    
    func correspondingLoserRoundTitle(_ displayStyle: DisplayStyle = .wide) -> String {
        let initialMatchIndexFromRoundIndex = RoundRule.matchIndex(fromRoundIndex: index)
        let seedsAfterThisRound : [TeamRegistration] = Store.main.filter(isIncluded: {
            $0.tournament == tournament
            && $0.bracketPosition != nil
            && ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex
        })
        let playedMatches = playedMatches()
        let seedInterval = SeedInterval(first: playedMatches.count + seedsAfterThisRound.count + 1, last: playedMatches.count * 2 + seedsAfterThisRound.count)
        return seedInterval.localizedLabel(displayStyle)
    }
    
    func hasNextRound() -> Bool {
        return nextRound()?.isDisabled() == false
    }
    
    func seedInterval() -> SeedInterval? {
        if parent == nil {
            let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: index + 1)
            let initialMatchIndexFromRoundIndex = RoundRule.matchIndex(fromRoundIndex: index)
            let seedsAfterThisRound : [TeamRegistration] = Store.main.filter(isIncluded: {
                $0.tournament == tournament
                && $0.bracketPosition != nil
                && ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex
            })
            let playedMatches = playedMatches()
            let reduce = numberOfMatches / 2 - (playedMatches.count + seedsAfterThisRound.count)
            return SeedInterval(first: 1, last: numberOfMatches, reduce: reduce)
        }
        
        if let previousRound = previousRound() {
            return previousRound.seedInterval()?.chunks()?.first
        } else if let parentRound {
            return parentRound.seedInterval()?.chunks()?.last
        }
        
        return nil
    }
    
    func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String {
        if parent != nil {
            return seedInterval()?.localizedLabel(displayStyle) ?? "Round pas trouvé"
        }
        return RoundRule.roundName(fromRoundIndex: index, displayStyle: displayStyle)
    }

    func updateTournamentState() {
        if let tournamentObject = tournamentObject(), index == 0, isUpperBracket(), hasEnded() {
            tournamentObject.endDate = Date()
            do {
                try DataStore.shared.tournaments.addOrUpdate(instance: tournamentObject)
            } catch {
                Logger.error(error)
            }
        }
    }
    
    func roundStatus() -> String {
        let hasEnded = hasEnded()
        if hasStarted() && hasEnded == false {
            return "en cours"
        } else if hasEnded {
            return "terminée"
        } else {
            return "à démarrer"
        }
    }
        
    func loserRounds() -> [Round] {
        return Store.main.filter(isIncluded: { $0.parent == id }).sorted(by: \.index).reversed()
    }
    
    func loserRoundsAndChildren() -> [Round] {
        let loserRounds = loserRounds()
        return loserRounds + loserRounds.flatMap({ $0.loserRoundsAndChildren() })
    }
    
    func isUpperBracket() -> Bool {
        return parent == nil
    }
    
    func isLoserBracket() -> Bool {
        return parent != nil
    }
        
    func buildLoserBracket() {
        guard loserRounds().isEmpty else { return }
        let currentRoundMatchCount = RoundRule.numberOfMatches(forRoundIndex: index)
        guard currentRoundMatchCount > 1 else { return }
        let roundCount = RoundRule.numberOfRounds(forTeams: currentRoundMatchCount)
        let loserBracketMatchFormat = tournamentObject()?.loserBracketMatchFormat
        
        let rounds = (0..<roundCount).map { //index 0 is the final
            let round = Round(tournament: tournament, index: $0, matchFormat: loserBracketMatchFormat)
            round.parent = id //parent
            return round
        }
        
        do {
            try DataStore.shared.rounds.addOrUpdate(contentOfs: rounds)
        } catch {
            Logger.error(error)
        }
        let matchCount = RoundRule.numberOfMatches(forTeams: currentRoundMatchCount)

        let matches = (0..<matchCount).map { //0 is final match
            let roundIndex = RoundRule.roundIndex(fromMatchIndex: $0)
            let round = rounds[roundIndex]
            return Match(round: round.id, index: $0, matchFormat: loserBracketMatchFormat, name: round.roundTitle())
        }
        
        do {
            try DataStore.shared.matches.addOrUpdate(contentOfs: matches)
        } catch {
            Logger.error(error)
        }
         
        loserRounds().forEach { round in
            round.buildLoserBracket()
        }
    }
    
    var parentRound: Round? {
        guard let parent = parent else { return nil }
        return Store.main.findById(parent)
    }

    func updateMatchFormat(_ updatedMatchFormat: MatchFormat, checkIfPossible: Bool, andLoserBracket: Bool) {
        if updatedMatchFormat.weight < self.matchFormat.weight {
            updateMatchFormatAndAllMatches(updatedMatchFormat)
            if andLoserBracket {
                loserRoundsAndChildren().forEach { round in
                    round.updateMatchFormat(updatedMatchFormat, checkIfPossible: checkIfPossible, andLoserBracket: true)
                }
            }
        }
    }
    
    func updateMatchFormatAndAllMatches(_ updatedMatchFormat: MatchFormat) {
        self.matchFormat = updatedMatchFormat
        self.updateMatchFormatOfAllMatches(updatedMatchFormat)
    }
    
    func updateMatchFormatOfAllMatches(_ updatedMatchFormat: MatchFormat) {
        let playedMatches = _matches()
        playedMatches.forEach { match in
            match.matchFormat = updatedMatchFormat
        }
        do {
            try DataStore.shared.matches.addOrUpdate(contentOfs: playedMatches)
        } catch {
            Logger.error(error)
        }
    }
    
    override func deleteDependencies() throws {
        DataStore.shared.matches.deleteDependencies(self._matches())
        DataStore.shared.rounds.deleteDependencies(self.loserRoundsAndChildren())
    }
    
    enum CodingKeys: String, CodingKey {
        case _id = "id"
        case _tournament = "tournament"
        case _index = "index"
        case _parent = "parent"
        case _format = "format"
        case _startDate = "startDate"
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        
        try container.encode(id, forKey: ._id)
        try container.encode(tournament, forKey: ._tournament)
        try container.encode(index, forKey: ._index)
        
        if let parent = parent {
            try container.encode(parent, forKey: ._parent)
        } else {
            try container.encodeNil(forKey: ._parent)
        }
        
        if let format = format {
            try container.encode(format, forKey: ._format)
        } else {
            try container.encodeNil(forKey: ._format)
        }
        
        if let startDate = startDate {
            try container.encode(startDate, forKey: ._startDate)
        } else {
            try container.encodeNil(forKey: ._startDate)
        }
    }
    
    func insertOnServer() throws {
        try DataStore.shared.rounds.writeChangeAndInsertOnServer(instance: self)
        for match in self._matches() {
            try match.insertOnServer()
        }
    }

}

extension Round: Selectable, Equatable {
    static func == (lhs: Round, rhs: Round) -> Bool {
        lhs.id == rhs.id
    }
    
    
    func selectionLabel() -> String {
        if let parentRound {
            return "Tour #\(parentRound.loserRounds().count - index)"
        } else {
            return roundTitle(.short)
        }
    }
    
    func badgeValue() -> Int? {
        if let parentRound {
            return parentRound.loserRounds(forRoundIndex: index).flatMap { $0.playedMatches() }.filter({ $0.isRunning() }).count
        } else {
            return playedMatches().filter({ $0.isRunning() }).count
        }
    }
    
    func badgeValueColor() -> Color? {
        return nil
    }
    
    func badgeImage() -> Badge? {
        hasEnded() ? .checkmark : nil
    }
}
