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

import Foundation
import LeStorage

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

    var id: String = Store.randomId()
    var tournament: String
    var groupStage: String?
    var registrationDate: Date?
    var callDate: Date?
    var bracketPosition: Int?
    var groupStagePosition: Int?
    var comment: String?
    var source: String?
    var sourceValue: String?
    var logo: String?
    var name: String?
    
    var walkOut: Bool = false
    var wildCardBracket: Bool = false
    var wildCardGroupStage: Bool = false
    var weight: Int = 0
    var lockedWeight: Int?
    var confirmationDate: Date?
    var qualified: Bool = false
    var finalRanking: Int?
    var pointsEarned: Int?
    
    init(tournament: String, groupStage: String? = nil, registrationDate: Date? = nil, callDate: Date? = nil, bracketPosition: Int? = nil, groupStagePosition: Int? = nil, comment: String? = nil, source: String? = nil, sourceValue: String? = nil, logo: String? = nil, name: String? = nil, walkOut: Bool = false, wildCardBracket: Bool = false, wildCardGroupStage: Bool = false, weight: Int = 0, lockedWeight: Int? = nil, confirmationDate: Date? = nil, qualified: Bool = false) {
        self.tournament = tournament
        self.groupStage = groupStage
        self.registrationDate = registrationDate
        self.callDate = callDate
        self.bracketPosition = bracketPosition
        self.groupStagePosition = groupStagePosition
        self.comment = comment
        self.source = source
        self.sourceValue = sourceValue
        self.logo = logo
        self.name = name
        self.walkOut = walkOut
        self.wildCardBracket = wildCardBracket
        self.wildCardGroupStage = wildCardGroupStage
        self.weight = weight
        self.lockedWeight = lockedWeight
        self.confirmationDate = confirmationDate
        self.qualified = qualified
    }
    
    // MARK: - Computed dependencies
    
    func unsortedPlayers() -> [PlayerRegistration] {
        Store.main.filter { $0.teamRegistration == self.id }
    }
    
    // MARK: -
    
    override func deleteDependencies() throws {
        DataStore.shared.playerRegistrations.deleteDependencies(self.unsortedPlayers())
        DataStore.shared.teamScores.deleteDependencies(self.teamScores())
    }
    
    func hasArrived() {
        let unsortedPlayers = unsortedPlayers()
        unsortedPlayers.forEach({ $0.hasArrived = true })
        do {
            try DataStore.shared.playerRegistrations.addOrUpdate(contentOfs: unsortedPlayers)
        } catch {
            Logger.error(error)
        }
    }
    
    func isSeedable() -> Bool {
        bracketPosition == nil && groupStage == nil
    }
    
    func setSeedPosition(inSpot match: Match, slot: TeamPosition?, opposingSeeding: Bool) {
        let seedPosition: Int = match.lockAndGetSeedPosition(atTeamPosition: slot, opposingSeeding: opposingSeeding)
        tournamentObject()?.resetTeamScores(in: bracketPosition)
        self.bracketPosition = seedPosition
        tournamentObject()?.updateTeamScores(in: bracketPosition)
    }
    
    func expectedSummonDate() -> Date? {
        if let groupStageStartDate = groupStageObject()?.startDate {
            return groupStageStartDate
        } else if let roundMatchStartDate = initialMatch()?.startDate {
            return roundMatchStartDate
        }
        return nil
    }
    
    var initialWeight: Int {
        return lockedWeight ?? weight
    }
    
    func called() -> Bool {
        return callDate != nil
    }

    func confirmed() -> Bool {
        return confirmationDate != nil
    }

    func getPhoneNumbers() -> [String] {
        return players().compactMap { $0.phoneNumber }.filter({ $0.isMobileNumber() })
    }
    
    func getMail() -> [String] {
        let mails = players().compactMap({ $0.email })
        return mails
    }
    
    func isImported() -> Bool {
        return unsortedPlayers().allSatisfy({ $0.isImported() })
    }
    
    func isWildCard() -> Bool {
        return wildCardBracket || wildCardGroupStage
    }
    
    func isPlaying() -> Bool {
        return currentMatch() != nil
    }
    
    func currentMatch() -> Match? {
        return teamScores().compactMap { $0.matchObject() }.first(where: { $0.isRunning() })
    }
    
    func teamScores() -> [TeamScore] {
        return Store.main.filter(isIncluded: { $0.teamRegistration == id })
    }
        
    func wins() -> [Match] {
        return Store.main.filter(isIncluded: { $0.winningTeamId == id })
    }

    func loses() -> [Match] {
        return Store.main.filter(isIncluded: { $0.losingTeamId == id })
    }
    
    func matches() -> [Match] {
        return Store.main.filter(isIncluded: { $0.losingTeamId == id || $0.winningTeamId == id })
    }

    var tournamentCategory: TournamentCategory {
        tournamentObject()?.tournamentCategory ?? .men
    }
    
    @objc
    var canonicalName: String {
        players().map { $0.canonicalName }.joined(separator: " ")
    }
    
    func hasMemberOfClub(_ codeClubOrClubName: String?) -> Bool {
        guard let codeClubOrClubName else { return true }
        return unsortedPlayers().anySatisfy({
            $0.clubName?.contains(codeClubOrClubName) == true || $0.clubName?.contains(codeClubOrClubName) == true
        })
    }
    
    func updateWeight(inTournamentCategory tournamentCategory: TournamentCategory) {
        self.setWeight(from: self.players(), inTournamentCategory: tournamentCategory)
    }

    func teamLabel(_ displayStyle: DisplayStyle = .wide, twoLines: Bool = false) -> String {
        return players().map { $0.playerLabel(displayStyle) }.joined(separator: twoLines ? "\n" : " & ")
    }
    
    func index(in teams: [TeamRegistration]) -> Int? {
        return teams.firstIndex(where: { $0.id == id })
    }

    func formattedSeed(in teams: [TeamRegistration]) -> String {
        if let index = index(in: teams) {
            return "#\(index + 1)"
        } else {
            return "###"
        }
    }
    
    func contains(_ searchField: String) -> Bool {
        return unsortedPlayers().anySatisfy({ $0.contains(searchField) }) || self.name?.localizedCaseInsensitiveContains(searchField) == true
    }

    func containsExactlyPlayerLicenses(_ playerLicenses: [String?]) -> Bool {
        let arrayOfIds : [String] = unsortedPlayers().compactMap({ $0.licenceId?.strippedLicense?.canonicalVersion })
        let ids : Set<String> = Set<String>(arrayOfIds.sorted())
        let searchedIds = Set<String>(playerLicenses.compactMap({ $0?.strippedLicense?.canonicalVersion }).sorted())
        return ids.hashValue == searchedIds.hashValue
    }
    
    func includes(players: [PlayerRegistration]) -> Bool {
        return players.allSatisfy { player in
            includes(player: player)
        }
    }
    
    func includes(player: PlayerRegistration) -> Bool {
        return unsortedPlayers().anySatisfy { _player in
            _player.isSameAs(player)
        }
    }
    
    func canPlay() -> Bool {
        return matches().isEmpty == false || players().allSatisfy({ $0.hasPaid() || $0.hasArrived })
    }
    
    func availableForSeedPick() -> Bool {
        return groupStage == nil && bracketPosition == nil
    }
    
    func inGroupStage() -> Bool {
        return groupStagePosition != nil
    }
    
    func inRound() -> Bool {
        return bracketPosition != nil
    }
    
    func resetGroupeStagePosition() {
        groupStageObject()?._matches().forEach({ $0.updateTeamScores() })
        groupStage = nil
        groupStagePosition = nil
    }
    
    func resetBracketPosition() {
        guard let bracketPosition else { return }
        guard let tournamentObject = tournamentObject() else { return }
        if let match = tournamentObject.match(for: bracketPosition) {
            let teamScores = match.teamScores.filter({ $0.teamRegistration != self.id })
            tournamentObject.resetTeamScores(in: bracketPosition, outsideOf: teamScores)
        }
        self.bracketPosition = nil
    }
    
    func resetPositions() {
        resetGroupeStagePosition()
        resetBracketPosition()        
    }
    
    func pasteData() -> String {
        [playersPasteData(), formattedInscriptionDate(), name].compactMap({ $0 }).joined(separator: "\n")
    }
    
    var computedRegistrationDate: Date {
        return registrationDate ?? .distantFuture
    }
    
    func formattedInscriptionDate() -> String? {
        if let registrationDate {
            return "Inscrit le " + registrationDate.formatted(.dateTime.weekday().day().month().hour().minute())
        } else {
            return nil
        }
    }
    
    func playersPasteData() -> String {
        return players().map { $0.pasteData() }.joined(separator: "\n")
    }
    
    func updatePlayers(_ players: Set<PlayerRegistration>, inTournamentCategory tournamentCategory: TournamentCategory) {
        let previousPlayers = Set(unsortedPlayers())
        let playersToRemove = previousPlayers.subtracting(players)
        do {
            try DataStore.shared.playerRegistrations.delete(contentOfs: playersToRemove)
        } catch {
            Logger.error(error)
        }
        setWeight(from: Array(players), inTournamentCategory: tournamentCategory)

        players.forEach { player in
            player.teamRegistration = id
        }        
    }

    func qualifiedFromGroupStage() -> Bool {
        groupStagePosition != nil && bracketPosition != nil
    }
    
    typealias TeamRange = (left: TeamRegistration?, right: TeamRegistration?)
    
    func replacementRange() -> TeamRange? {
        guard let tournamentObject = tournamentObject() else { return nil }
        guard let index = tournamentObject.indexOf(team: self) else { return nil }
        let selectedSortedTeams = tournamentObject.selectedSortedTeams()
        let left = selectedSortedTeams[safe: index - 1]
        let right = selectedSortedTeams[safe: index + 1]
        return (left: left, right: right)
    }
    
    func replacementRangeExtended() -> TeamRange? {
        guard let tournamentObject = tournamentObject() else { return nil }
        guard let groupStagePosition else { return nil }
        let selectedSortedTeams = tournamentObject.selectedSortedTeams()
        var left: TeamRegistration? = nil
        if groupStagePosition == 0 {
            left = tournamentObject.seeds().last
        } else {
            let previousHat = selectedSortedTeams.filter({ $0.groupStagePosition == groupStagePosition - 1 }).sorted(by: \.weight)
            left = previousHat.last
        }
        var right: TeamRegistration? = nil
        if groupStagePosition == tournamentObject.teamsPerGroupStage - 1 {
            right = nil
        } else {
            let previousHat = selectedSortedTeams.filter({ $0.groupStagePosition == groupStagePosition + 1 }).sorted(by: \.weight)
            right = previousHat.first
        }
        return (left: left, right: right)
    }
    
    typealias AreInIncreasingOrder = (PlayerRegistration, PlayerRegistration) -> Bool

    func players() -> [PlayerRegistration] {
        Store.main.filter { $0.teamRegistration == self.id }.sorted { (lhs, rhs) in
            let predicates: [AreInIncreasingOrder] = [
                { $0.sex?.rawValue ?? 0 < $1.sex?.rawValue ?? 0 },
                { $0.rank ?? 0 < $1.rank ?? 0 },
                { $0.lastName < $1.lastName},
                { $0.firstName < $1.firstName }
            ]
            
            for predicate in predicates {
                if !predicate(lhs, rhs) && !predicate(rhs, lhs) {
                    continue
                }
                
                return predicate(lhs, rhs)
            }
            
            return false
        }
    }
    
    func setWeight(from players: [PlayerRegistration], inTournamentCategory tournamentCategory: TournamentCategory) {
        let significantPlayerCount = significantPlayerCount()
        weight = (players.prefix(significantPlayerCount).map { $0.computedRank } + missingPlayerType(inTournamentCategory: tournamentCategory).map { unrankValue(for: $0 == 1 ? true : false ) }).prefix(significantPlayerCount).reduce(0,+)
    }
    
    func significantPlayerCount() -> Int {
        return tournamentObject()?.significantPlayerCount() ?? 2
    }
    
    func missingPlayerType(inTournamentCategory tournamentCategory: TournamentCategory) -> [Int] {
        let players = unsortedPlayers()
        if players.count >= 2 { return [] }
        let s = players.compactMap { $0.sex?.rawValue }
        var missing = tournamentCategory.mandatoryPlayerType()
        s.forEach { i in
            if let index = missing.firstIndex(of: i) {
                missing.remove(at: index)
            }
        }
        return missing
    }
    
    func unrankValue(for malePlayer: Bool) -> Int {
        return tournamentObject()?.unrankValue(for: malePlayer) ?? 100_000
    }
    
    func groupStageObject() -> GroupStage? {
        guard let groupStage else { return nil }
        return Store.main.findById(groupStage)
    }
    
    func initialRound() -> Round? {
        guard let bracketPosition else { return nil }
        let roundIndex = RoundRule.roundIndex(fromMatchIndex: bracketPosition / 2)
        return Store.main.filter(isIncluded: { $0.tournament == tournament && $0.index == roundIndex }).first
    }

    func initialMatch() -> Match? {
        guard let bracketPosition else { return nil }
        guard let initialRoundObject = initialRound() else { return nil }
        return Store.main.filter(isIncluded: { $0.round == initialRoundObject.id && $0.index == bracketPosition / 2 }).first
    }

    
    func tournamentObject() -> Tournament? {
        return Store.main.findById(tournament)
    }
    
    enum CodingKeys: String, CodingKey {
        case _id = "id"
        case _tournament = "tournament"
        case _groupStage = "groupStage"
        case _registrationDate = "registrationDate"
        case _callDate = "callDate"
        case _bracketPosition = "bracketPosition"
        case _groupStagePosition = "groupStagePosition"
        case _comment = "comment"
        case _source = "source"
        case _sourceValue = "sourceValue"
        case _logo = "logo"
        case _name = "name"
        case _wildCardBracket = "wildCardBracket"
        case _wildCardGroupStage = "wildCardGroupStage"
        case _weight = "weight"
        case _walkOut = "walkOut"
        case _lockedWeight = "lockedWeight"
        case _confirmationDate = "confirmationDate"
        case _qualified = "qualified"
        case _finalRanking = "finalRanking"
        case _pointsEarned = "pointsEarned"
    }

    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)
        
        if let groupStage = groupStage {
            try container.encode(groupStage, forKey: ._groupStage)
        } else {
            try container.encodeNil(forKey: ._groupStage)
        }
        
        if let registrationDate = registrationDate {
            try container.encode(registrationDate, forKey: ._registrationDate)
        } else {
            try container.encodeNil(forKey: ._registrationDate)
        }
        
        if let callDate = callDate {
            try container.encode(callDate, forKey: ._callDate)
        } else {
            try container.encodeNil(forKey: ._callDate)
        }
        
        if let bracketPosition = bracketPosition {
            try container.encode(bracketPosition, forKey: ._bracketPosition)
        } else {
            try container.encodeNil(forKey: ._bracketPosition)
        }
        
        if let groupStagePosition = groupStagePosition {
            try container.encode(groupStagePosition, forKey: ._groupStagePosition)
        } else {
            try container.encodeNil(forKey: ._groupStagePosition)
        }
        
        if let comment = comment {
            try container.encode(comment, forKey: ._comment)
        } else {
            try container.encodeNil(forKey: ._comment)
        }
        
        if let source = source {
            try container.encode(source, forKey: ._source)
        } else {
            try container.encodeNil(forKey: ._source)
        }
        
        if let sourceValue = sourceValue {
            try container.encode(sourceValue, forKey: ._sourceValue)
        } else {
            try container.encodeNil(forKey: ._sourceValue)
        }
        
        if let logo = logo {
            try container.encode(logo, forKey: ._logo)
        } else {
            try container.encodeNil(forKey: ._logo)
        }
        
        if let name = name {
            try container.encode(name, forKey: ._name)
        } else {
            try container.encodeNil(forKey: ._name)
        }
        
        try container.encode(walkOut, forKey: ._walkOut)
        try container.encode(wildCardBracket, forKey: ._wildCardBracket)
        try container.encode(wildCardGroupStage, forKey: ._wildCardGroupStage)
        try container.encode(weight, forKey: ._weight)
        
        if let lockedWeight = lockedWeight {
            try container.encode(lockedWeight, forKey: ._lockedWeight)
        } else {
            try container.encodeNil(forKey: ._lockedWeight)
        }
        
        if let confirmationDate = confirmationDate {
            try container.encode(confirmationDate, forKey: ._confirmationDate)
        } else {
            try container.encodeNil(forKey: ._confirmationDate)
        }
        
        try container.encode(qualified, forKey: ._qualified)
        
        if let finalRanking {
            try container.encode(finalRanking, forKey: ._finalRanking)
        } else {
            try container.encodeNil(forKey: ._finalRanking)
        }
        
        if let pointsEarned {
            try container.encode(pointsEarned, forKey: ._pointsEarned)
        } else {
            try container.encodeNil(forKey: ._pointsEarned)
        }
    }

    func insertOnServer() throws {
        try DataStore.shared.teamRegistrations.writeChangeAndInsertOnServer(instance: self)
        for playerRegistration in self.unsortedPlayers() {
            try playerRegistration.insertOnServer()
        }
    }
    
}

extension TeamRegistration: Hashable {
    static func == (lhs: TeamRegistration, rhs: TeamRegistration) -> Bool {
        lhs.id == rhs.id
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

enum TeamDataSource: Int, Codable {
    case beachPadel
}
