import Foundation

typealias PlayerName = String
typealias PasswordHash = String
typealias SessionToken = String

/// Manages player registration, session tokens and password hashes
final class PlayerManagement: DiskWriter {

    /// A mapping between player name and their password hashes
    private var playerPasswordHashes = [PlayerName: PasswordHash]()

    /// A mapping between player name and generated access tokens for a session
    private var sessionTokenForPlayer = [PlayerName: SessionToken]()

    /// A reverse mapping between generated access tokens and player name
    private var playerNameForToken = [SessionToken: PlayerName]()
    
    var storageFile: FileHandle
    
    let storageFileUrl: URL

    init(storageFolder: URL) throws {
        let url = storageFolder.appendingPathComponent("passwords.txt")
        
        storageFileUrl = url
        storageFile = try Self.prepareFile(at: url)

        var redundantEntries = 0
        try readLinesFromDisk().forEach { line in
            let parts = line.components(separatedBy: ":")
            // Token may contain the separator
            guard parts.count >= 2 else {
                print("Invalid line in password file")
                return
            }
            let name = parts[0]
            let token = parts.dropFirst().joined(separator: ":")
            if token == "" {
                playerPasswordHashes[name] = nil
                redundantEntries += 2 // One for creation, one for deletion
                return
            }
            if playerPasswordHashes[name] != nil {
                redundantEntries += 1
            }
            playerPasswordHashes[name] = token
        }
        
        let playerCount = playerPasswordHashes.count
        let totalEntries = playerCount + redundantEntries
        let percentage = playerCount * 100 / totalEntries
        print("Loaded \(playerCount) players from \(totalEntries) entries (\(percentage) % useful)")
        if percentage < 80 && redundantEntries > 10 {
            try optimizePlayerFile()
        }
    }

    private func optimizePlayerFile() throws {
        print("Optimizing player file...")
        let lines = playerPasswordHashes.map { $0.key + ":" + $0.value + "\n" }.joined()
        try replaceFile(data: lines)
        print("Done.")
    }
    
    private func save(password: PasswordHash, forPlayer player: PlayerName) -> Bool {
        writeToDisk(line: player + ":" + password)
    }
    
    private func deletePassword(forPlayer player: PlayerName) -> Bool {
        writeToDisk(line: player + ":")
    }

    /**
     Check if a player exists.
     - Parameter name: The name of the player
     - Returns: true, if the player exists
     */
    func hasRegisteredPlayer(named user: PlayerName) -> Bool {
        playerPasswordHashes[user] != nil
    }

    /**
     Get the password hash for a player, if the player exists.
     - Parameter name: The name of the player
     - Returns: The stored password hash, if the player exists
     */
    func passwordHash(ofRegisteredPlayer name: PlayerName) -> PasswordHash? {
        playerPasswordHashes[name]
    }

    /**
     Create a new player and assign an access token.
     - Parameter name: The name of the new player
     - Parameter hash: The password hash of the player
     - Returns: The generated access token for the session
     */
    func registerPlayer(named name: PlayerName, hash: PasswordHash) -> SessionToken? {
        guard !hasRegisteredPlayer(named: name) else {
            return nil
        }
        guard save(password: hash, forPlayer: name) else {
            return nil
        }
        self.playerPasswordHashes[name] = hash
        return startNewSessionForRegisteredPlayer(named: name)
    }

    /**
     Delete a player
     - Parameter name: The name of the player to delete.
     - Returns: The session token of the current player, if one exists
     */
    func deletePlayer(named name: PlayerName) -> SessionToken? {
        guard deletePassword(forPlayer: name) else {
            return nil
        }
        playerPasswordHashes.removeValue(forKey: name)
        guard let sessionToken = sessionTokenForPlayer.removeValue(forKey: name) else {
            return nil
        }
        playerNameForToken.removeValue(forKey: sessionToken)
        return sessionToken
    }

    func isValid(sessionToken token: SessionToken) -> Bool {
        playerNameForToken[token] != nil
    }

    func sessionToken(forPlayer player: PlayerName) -> SessionToken? {
        sessionTokenForPlayer[player]
    }

    /**
     Start a new session for an existing player.
     - Parameter name: The player name
     - Returns: The generated access token for the session
     */
    func startNewSessionForRegisteredPlayer(named name: PlayerName) -> SessionToken {
        let token = SessionToken.newToken()
        self.sessionTokenForPlayer[name] = token
        self.playerNameForToken[token] = name
        return token
    }

    func endSession(forSessionToken token: SessionToken) -> PlayerName? {
        guard let player = playerNameForToken.removeValue(forKey: token) else {
            return nil
        }
        sessionTokenForPlayer.removeValue(forKey: player)
        return player
    }

    /**
     Get the player for a session token.
     - Parameter token: The access token for the player
     - Returns: The name of the player, if it exists
     */
    func registeredPlayerExists(withSessionToken token: SessionToken) -> PlayerName? {
        playerNameForToken[token]
    }


}
