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

import Foundation
import LeStorage
import Algorithms
import SwiftUI

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

    var id: String = Store.randomId()
    var tournament: String
    var index: Int
    var size: Int
    private var format: MatchFormat?
    var startDate: Date?
    var name: String?
    
    var matchFormat: MatchFormat {
        get {
            format ?? .defaultFormatForMatchType(.groupStage)
        }
        set {
            format = newValue
        }
    }
    
    internal init(tournament: String, index: Int, size: Int, matchFormat: MatchFormat? = nil, startDate: Date? = nil, name: String? = nil) {
        self.tournament = tournament
        self.index = index
        self.size = size
        self.format = matchFormat
        self.startDate = startDate
        self.name = name
    }
    
    // MARK: - Computed dependencies

    func _matches() -> [Match] {
        Store.main.filter { $0.groupStage == self.id }
    }
    
    func tournamentObject() -> Tournament? {
        Store.main.findById(self.tournament)
    }
    
    // MARK: -
    
    func teamAt(groupStagePosition: Int) -> TeamRegistration? {
        teams().first(where: { $0.groupStagePosition == groupStagePosition })
    }
    
    func groupStageTitle(_ displayStyle: DisplayStyle = .wide) -> String {
        if let name { return name }
        switch displayStyle {
        case .wide, .title:
            return "Poule \(index + 1)"
        case .short:
            return "#\(index + 1)"
        }
    }
    
    func isRunning() -> Bool { // at least a match has started
        _matches().anySatisfy({ $0.isRunning() })
    }
    
    func hasStarted() -> Bool { // meaning at least one match is over
        _matches().filter { $0.hasEnded() }.isEmpty == false
    }
    
    func hasEnded() -> Bool {
        guard teams().count == size else { return false }
        let _matches = _matches()
        if _matches.isEmpty { return false }
        return _matches.anySatisfy { $0.hasEnded() == false } == false
    }
    
    func buildMatches() {
        _removeMatches()
        
        var _matches = [Match]()
        var _teamScores = [TeamScore]()
        
        for i in 0..<_numberOfMatchesToBuild() {
            let newMatch = Match(groupStage: id, index: i, matchFormat: matchFormat, name: localizedMatchUpLabel(for: i))
            _teamScores.append(contentsOf: newMatch.createTeamScores())
            _matches.append(newMatch)
        }
        
        do {
            try DataStore.shared.matches.addOrUpdate(contentOfs: _matches)
            try DataStore.shared.teamScores.addOrUpdate(contentOfs: _teamScores)
        } catch {
            Logger.error(error)
        }
    }
    
    func playedMatches() -> [Match] {
        let ordered = _matches()
        if ordered.isEmpty == false && ordered.count == _matchOrder().count {
            return _matchOrder().map {
                ordered[$0]
            }
        } else {
            return ordered
        }
    }
    
    func updateGroupStageState() {
        if hasEnded(), let tournament = tournamentObject() {
            do {
                let teams = teams(true)
                for (index, team) in teams.enumerated() {
                    team.qualified = index < tournament.qualifiedPerGroupStage
                    if team.bracketPosition != nil && team.qualified == false {
                        tournamentObject()?.resetTeamScores(in: team.bracketPosition)
                        team.bracketPosition = nil
                    }
                }
                try DataStore.shared.teamRegistrations.addOrUpdate(contentOfs: teams)
            } catch {
                Logger.error(error)
            }
        }
    }
    
    func scoreLabel(forGroupStagePosition groupStagePosition: Int, score: TeamGroupStageScore? = nil) -> (wins: String, losses: String, setsDifference: String?, gamesDifference: String?)? {
        if let scoreData = (score ?? _score(forGroupStagePosition: groupStagePosition, nilIfEmpty: true)) {
            let hideSetDifference = matchFormat.setsToWin == 1
            let setDifference = scoreData.setDifference.formatted(.number.sign(strategy: .always(includingZero: false)))
            let gameDifference = scoreData.gameDifference.formatted(.number.sign(strategy: .always(includingZero: false)))
            return (wins: scoreData.wins.formatted(), losses: scoreData.loses.formatted(), setsDifference: hideSetDifference ? nil : setDifference, gamesDifference: gameDifference)
//            return "\(scoreData.wins)/\(scoreData.loses) " + differenceAsString
        } else {
            return nil
        }
    }
    
    func _score(forGroupStagePosition groupStagePosition: Int, nilIfEmpty: Bool = false) -> TeamGroupStageScore? {
        guard let team = teamAt(groupStagePosition: groupStagePosition) else { return nil }
        let matches = matches(forGroupStagePosition: groupStagePosition).filter({ $0.hasEnded() })
        if matches.isEmpty && nilIfEmpty { return nil }
        let wins = matches.filter { $0.winningTeamId == team.id }.count
        let loses = matches.filter { $0.losingTeamId == team.id }.count
        let differences = matches.compactMap { $0.scoreDifference(groupStagePosition) }
        let setDifference = differences.map { $0.set }.reduce(0,+)
        let gameDifference = differences.map { $0.game }.reduce(0,+)
        return (team, wins, loses, setDifference, gameDifference)
    }
    
    func matches(forGroupStagePosition groupStagePosition: Int) -> [Match] {
        let combos = Array((0..<size).combinations(ofCount: 2))
        var matchIndexes = [Int]()
        for (index, combo) in combos.enumerated() {
            if combo.contains(groupStagePosition) { //team is playing
                matchIndexes.append(index)
            }
        }
        return _matches().filter { matchIndexes.contains($0.index) }
    }
    
    func matchPlayed(by groupStagePosition: Int, againstPosition: Int) -> Match? {
        if groupStagePosition == againstPosition { return nil }
        let combos = Array((0..<size).combinations(ofCount: 2))
        var matchIndexes = [Int]()
        for (index, combo) in combos.enumerated() {
            if combo.contains(groupStagePosition) && combo.contains(againstPosition) { //teams are playing
                matchIndexes.append(index)
            }
        }
        return _matches().first(where: { matchIndexes.contains($0.index) })
    }
    
    func availableToStart(playedMatches: [Match], in runningMatches: [Match]) async -> [Match] {
        #if DEBUG_TIME
        let start = Date()
        defer {
            let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
            print("func group stage availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
        }
        #endif
        return playedMatches.filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false })
    }
    
    func runningMatches(playedMatches: [Match]) -> [Match] {
        #if DEBUG_TIME
        let start = Date()
        defer {
            let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
            print("func group stage runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
        }
        #endif
        return playedMatches.filter({ $0.isRunning() }).sorted(by: \.computedStartDateForSorting)
    }
    
    func asyncRunningMatches(playedMatches: [Match]) async -> [Match] {
        #if DEBUG_TIME
        let start = Date()
        defer {
            let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
            print("func group stage runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
        }
        #endif
        return playedMatches.filter({ $0.isRunning() }).sorted(by: \.computedStartDateForSorting)
    }


    func readyMatches(playedMatches: [Match]) async -> [Match] {
        #if DEBUG_TIME
        let start = Date()
        defer {
            let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
            print("func group stage readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
        }
        #endif
        return playedMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false })
    }

    func finishedMatches(playedMatches: [Match]) -> [Match] {
        #if DEBUG_TIME
        let start = Date()
        defer {
            let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
            print("func group stage finishedMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
        }
        #endif
        return playedMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed()
    }

    private func _matchOrder() -> [Int] {
        switch size {
        case 3:
            return [1, 2, 0]
        case 4:
            return [2, 3, 1, 4, 5, 0]
        case 5:
            return [5, 8, 0, 7, 3, 4, 2, 6, 1, 9]
//            return [3, 5, 8, 2, 6, 7, 1, 9, 4, 0]
        case 6:
            return [1, 7, 13, 11, 3, 6, 10, 2, 8, 12, 5, 4, 9, 14, 0]
            //return [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0]
        default:
            return []
        }
    }
    
    private func _matchUp(for matchIndex: Int) -> [Int] {
        Array((0..<size).combinations(ofCount: 2))[matchIndex]
    }
    
    func localizedMatchUpLabel(for matchIndex: Int) -> String {
        let matchUp = _matchUp(for: matchIndex)
        if let index = matchUp.first, let index2 = matchUp.last {
            return "#\(index + 1) contre #\(index2 + 1)"
        } else {
            return "--"
        }
    }
    
    func team(teamPosition team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? {
        let _teams = _teams(for: matchIndex)
        switch team {
        case .one:
            return _teams.first!
        case .two:
            return _teams.last!
        }
    }
    
    private func _teams(for matchIndex: Int) -> [TeamRegistration?] {
        let combinations = Array(0..<size).combinations(ofCount: 2).map {$0}
        return combinations[safe: matchIndex]?.map { teamAt(groupStagePosition: $0) } ?? []
    }
    
    private func _removeMatches() {
        do {
            try DataStore.shared.matches.delete(contentOfs: _matches())
        } catch {
            Logger.error(error)
        }
    }
        
    private func _numberOfMatchesToBuild() -> Int {
        (size * (size - 1)) / 2
    }
    
    func unsortedPlayers() -> [PlayerRegistration] {
        unsortedTeams().flatMap({ $0.unsortedPlayers() })
    }
    
    fileprivate typealias TeamScoreAreInIncreasingOrder = (TeamGroupStageScore, TeamGroupStageScore) -> Bool

    typealias TeamGroupStageScore = (team: TeamRegistration, wins: Int, loses: Int, setDifference: Int, gameDifference: Int)

    fileprivate func _headToHead(_ teamPosition: TeamRegistration, _ otherTeam: TeamRegistration) -> Bool {
        let indexes = [teamPosition, otherTeam].compactMap({ $0.groupStagePosition }).sorted()
        let combos = Array((0..<size).combinations(ofCount: 2))
        if let matchIndex = combos.firstIndex(of: indexes), let match = _matches().first(where: { $0.index == matchIndex }) {
            return teamPosition.id == match.losingTeamId
        } else {
            return false
        }
    }
    
    func unsortedTeams() -> [TeamRegistration] {
        Store.main.filter { $0.groupStage == self.id && $0.groupStagePosition != nil }
    }
    
    func teams(_ sortedByScore: Bool = false, scores: [TeamGroupStageScore]? = nil) -> [TeamRegistration] {
        if sortedByScore {
            return unsortedTeams().compactMap({ team in
                scores?.first(where: { $0.team.id == team.id }) ?? _score(forGroupStagePosition: team.groupStagePosition!)
            }).sorted { (lhs, rhs) in
                let predicates: [TeamScoreAreInIncreasingOrder] = [
                    { $0.wins < $1.wins },
                    { $0.setDifference < $1.setDifference },
                    { $0.gameDifference < $1.gameDifference},
                    { self._headToHead($0.team, $1.team) },
                    { $0.team.groupStagePosition! > $1.team.groupStagePosition! }
                ]
                
                for predicate in predicates {
                    if !predicate(lhs, rhs) && !predicate(rhs, lhs) {
                        continue
                    }
                    
                    return predicate(lhs, rhs)
                }
                
                return false
            }.map({ $0.team }).reversed()
        } else {
            return unsortedTeams().sorted(by: \TeamRegistration.groupStagePosition!)
        }
    }
    
    func updateMatchFormat(_ updatedMatchFormat: MatchFormat) {
        self.matchFormat = updatedMatchFormat
        self.updateAllMatchesFormat()
    }
    
    func updateAllMatchesFormat() {
        let playedMatches = playedMatches()
        playedMatches.forEach { match in
            match.matchFormat = matchFormat
        }
        do {
            try DataStore.shared.matches.addOrUpdate(contentOfs: playedMatches)
        } catch {
            Logger.error(error)
        }
    }

    override func deleteDependencies() throws {
        DataStore.shared.matches.deleteDependencies(self._matches())
    }
    
    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)
        try container.encode(size, forKey: ._size)
        
        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)
        }
        
        if let name = name {
            try container.encode(name, forKey: ._name)
        } else {
            try container.encodeNil(forKey: ._name)
        }
    }
    
    func insertOnServer() throws {
        try DataStore.shared.groupStages.writeChangeAndInsertOnServer(instance: self)
        for match in self._matches() {
            try match.insertOnServer()
        }
    }

}

extension GroupStage {
    enum CodingKeys: String, CodingKey {
        case _id = "id"
        case _tournament = "tournament"
        case _index = "index"
        case _size = "size"
        case _format = "format"
        case _startDate = "startDate"
        case _name = "name"
    }
}

extension GroupStage: Selectable {
    func selectionLabel() -> String {
        groupStageTitle()
    }
    
    func badgeValue() -> Int? {
        return runningMatches(playedMatches: _matches()).count
    }
    
    func badgeValueColor() -> Color? {
        return nil
    }
    
    func badgeImage() -> Badge? {
        if teams().count < size {
            return .xmark
        } else {
            return hasEnded() ? .checkmark : nil
        }
    }
}
