import Foundation
import Fluent
import Vapor

typealias PasswordHash = String
typealias SessionToken = String

final class SQLiteDatabase {

    /// 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]()

    private let tables: TableManagement

    init(db: Database) throws {
        self.tables = try TableManagement(db: db)
    }

    func registerPlayer(named name: PlayerName, hash: PasswordHash, in database: Database) -> EventLoopFuture<SessionToken> {
        User.query(on: database).filter(\.$name == name).first()
            .guard({ $0 == nil }, else: Abort(.conflict)).flatMap { _ in
                let user = User(name: name, hash: hash)
                return user.create(on: database).map {
                    // Create a new token and store it for the user
                    let token = SessionToken.newToken()
                    self.sessionTokenForPlayer[name] = token
                    self.playerNameForToken[token] = name
                    return token
                }
            }
    }

    func passwordHashForExistingPlayer(named name: PlayerName, in database: Database) -> EventLoopFuture<PasswordHash> {
        User.query(on: database).filter(\.$name == name).first()
            .unwrap(or: Abort(.forbidden)).map { $0.passwordHash }
    }

    func deletePlayer(named name: PlayerName, in database: Database)  -> EventLoopFuture<Void> {
        user(named: name, in: database).flatMap { user in
            self.tables.leaveTable(player: user, in: database)
        }.flatMap {
            User.query(on: database).filter(\.$name == name).delete()
        }
    }

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

    func startSession(socket: WebSocket, sessionToken token: SessionToken) -> Bool {
        guard let player = playerNameForToken[token] else {
            return false
        }
        return tables.connect(player: player, using: socket)
    }

    private func didReceive(message: String, forSessionToken token: SessionToken) {
        // TODO: Handle client requests
        print("Session \(token.prefix(6)): \(message)")
    }

    func endSession(forSessionToken sessionToken: SessionToken) {
        guard let player = endExistingSession(forSessionToken: sessionToken) else {
            return
        }
        tables.disconnect(player: player)
    }

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


    /**
     Start a new session for an existing user.
     - Parameter name: The user 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 registeredPlayerExists(withSessionToken token: SessionToken) -> PlayerName? {
        playerNameForToken[token]
    }

    func currentTableOfPlayer(named player: PlayerName) -> TableInfo? {
        tables.tableInfo(player: player)
    }

    private func points(for player: PlayerName, in database: Database) -> EventLoopFuture<Int> {
        User.query(on: database)
            .filter(\.$name == player)
            .first()
            .unwrap(or: Abort(.notFound))
            .map { $0.points }
    }

    private func user(named name: PlayerName, in database: Database) -> EventLoopFuture<User> {
        User.query(on: database)
            .filter(\.$name == name)
            .first()
            .unwrap(or: Abort(.notFound))
    }

    private func user(withToken token: SessionToken, in database: Database) -> EventLoopFuture<User> {
        database.eventLoop
            .future()
            .map { self.playerNameForToken[token] }
            .unwrap(or: Abort(.unauthorized))
            .flatMap { name in
                self.user(named: name, in: database)
            }
    }

    // MARK: Tables

    /**
     Create a new table with optional players.
     - Parameter name: The name of the table
     - Parameter players: The player creating the table
     - Parameter isPublic: Indicates that this is a game joinable by everyone
     - Returns: The table id
     */
    func createTable(named name: TableName, player: PlayerName, isPublic: Bool, in database: Database) -> EventLoopFuture<TableInfo> {
        user(named: player, in: database).flatMap { player in
            self.tables.createTable(named: name, player: player, isPublic: isPublic, in: database)
        }
    }

    func getPublicTableInfos() -> [PublicTableInfo] {
        tables.publicTableList
    }

    func join(tableId: UUID, playerToken: SessionToken, in database: Database) -> EventLoopFuture<TableInfo> {
        user(withToken: playerToken, in: database).flatMap { player in
            self.tables.join(tableId: tableId, player: player, in: database)
        }
    }

    func leaveTable(playerToken: SessionToken, in database: Database) -> EventLoopFuture<Void> {
        user(withToken: playerToken, in: database).flatMap { player in
            self.tables.leaveTable(player: player, in: database)
        }
    }

    func performAction(playerToken: SessionToken, action: PlayerAction) -> PlayerActionResult {
        guard let player = playerNameForToken[playerToken] else {
            return .invalidToken
        }
        return tables.performAction(player: player, action: action)
    }

    func select(game: GameType, playerToken: SessionToken) -> PlayerActionResult {
        guard let player = playerNameForToken[playerToken] else {
            return .invalidToken
        }
        return tables.select(game: game, player: player)
    }

    func play(card: Card, playerToken: SessionToken) -> PlayerActionResult {
        guard let player = playerNameForToken[playerToken] else {
            return .invalidToken
        }
        return tables.play(card: card, player: player)
    }
}
