import Vapor

/// The JSON encoder for responses
private let encoder = JSONEncoder()

/// The maximum length of a valid player name
private let maximumPlayerNameLength = 40

/// The maximum length of a valid password
private let maximumPasswordLength = 40

func encodeJSON<T>(_ response: T) throws -> String where T: Encodable {
    let data = try encoder.encode(response)
    return String(data: data, encoding: .utf8)!
}

func routes(_ app: Application) throws {
    
    // MARK: Players & Sessions
    
    /**
     Create a new player
     - Parameter name: The name of the player, included in the url
     - Parameter password: The password of the player, as a string in the request body
     - Throws:
     - 400: Missing name or password
     - 406: Password or name too long
     - 409: A player with the same name already exists
     - 424: The password could not be hashed
     - Returns: The session token for the registered user
     */
    app.post("player", "register", ":name") { req -> String in
        guard let name = req.parameters.get("name"),
              let password = req.body.string else {
                  throw Abort(.badRequest) // 400
              }
        guard name.count < maximumPlayerNameLength,
              password.count < maximumPasswordLength else {
                  throw Abort(.notAcceptable) // 406
              }
        
        guard let hash = try? req.password.hash(password) else {
            throw Abort(.failedDependency) // 424
        }
        guard let token = database.registerPlayer(named: name, hash: hash) else {
            throw Abort(.conflict) // 409
        }
        return token
    }
    
    /**
     Delete a player.
     - Parameter name: The name of the player, included in the url
     - Parameter password: The password of the player, as a string in the request body
     - Throws:
     - 400: Missing name or password
     - 403: The password or user name is invalid
     - 424: The password could not be hashed
     - Returns: Nothing
     */
    app.post("player", "delete", ":name") { req -> String in
        guard let name = req.parameters.get("name"),
              let password = req.body.string else {
                  throw Abort(.badRequest) // 400
              }
        guard let hash = database.passwordHashForExistingPlayer(named: name) else {
            throw Abort(.forbidden) // 403
        }
        guard let isValid = try? req.password.verify(password, created: hash) else {
            throw Abort(.failedDependency) // 424
        }
        guard isValid else {
            throw Abort(.forbidden) // 403
        }
        database.deletePlayer(named: name)
        return ""
    }
    
    /**
     Log in as an existing player.
     - Parameter name: The name of the player, included in the url
     - Parameter password: The password of the player, as a string in the request body
     - Throws:
     - 400: Missing name or password
     - 403: The password or user name is invalid
     - 424: The password could not be hashed
     - Returns: The session token for the user
     */
    app.post("player", "login", ":name") { req -> String in
        guard let name = req.parameters.get("name"),
              let password = req.body.string else {
                  throw Abort(.badRequest) // 400
              }
        guard let hash = database.passwordHashForExistingPlayer(named: name) else {
            throw Abort(.forbidden) // 403
        }
        guard let isValid = try? req.password.verify(password, created: hash) else {
            throw Abort(.failedDependency) // 424
        }
        guard isValid else {
            throw Abort(.forbidden) // 403
        }
        let token = database.startNewSessionForRegisteredPlayer(named: name)
        return token
    }
    
    /**
     Log in using a session token.
     - Parameter token: The session token of the player, as a string in the request body
     - Throws:
     - 400: Missing token
     - 401: The token is invalid
     - Returns: The player name associated with the session token
     */
    app.post("player", "resume") { req -> String in
        guard let token = req.body.string else {
            throw Abort(.badRequest) // 400
        }
        guard let player = database.registeredPlayerExists(withSessionToken: token) else {
            throw Abort(.unauthorized) // 401
        }
        return player
    }
    
    /**
     Log out.
     - Parameter name: The name of the player, included in the url
     - Parameter token: The session token of the player, as a string in the request body
     - Throws:
     - 400: Missing token
     - Returns: Nothing
     - Note: The request always succeeds when correctly formed, even for invalid and expired tokens
     */
    app.post("player", "logout") { req -> String in
        guard let token = req.body.string else {
            throw Abort(.badRequest) // 400
        }
        database.endSession(forSessionToken: token)
        return ""
    }
    
    /**
     Get the current table of the player, if one exists.
     - Parameter token: The session token of the player, as a string in the request body
     - Throws:
     - 400: Missing token
     - 401: Invalid token
     - Returns: The table info, or an empty string
     */
    app.post("player", "table") { req -> String in
        guard let token = req.body.string else {
            throw Abort(.badRequest) // 400
        }
        guard let player = database.registeredPlayerExists(withSessionToken: token) else {
            throw Abort(.unauthorized) // 401
        }
        guard let info = database.currentTableOfPlayer(named: player) else {
            return ""
        }
        return try encodeJSON(info)
    }
    
    /**
     Start a new websocket connection for the client to receive table updates from the server
     - Returns: Nothing
     - Note: The first (and only) message from the client over the connection must be a valid session token.
     */
    app.webSocket("session", "start") { req, socket in
        socket.onText { socket, text in
            guard database.startSession(socket: socket, sessionToken: text) else {
                _ = socket.close()
                return
            }
        }
    }
    
    // MARK: Tables
    
    /**
     Create a new table.
     - Parameter visibility: Indicate a `"public"` or `"private"` table
     - Parameter token: The session token of the player, as a string in the request body
     - Returns: The table id
     - Throws:
     - 400: Missing token, table name or invalid visibility
     - 401: The session token is invalid
     */
    app.post("table", "create", ":visibility", ":name") { req -> String in
        guard let visibility = req.parameters.get("visibility"),
              let tableName = req.parameters.get("name"),
              let token = req.body.string else {
                  throw Abort(.badRequest) // 400
              }
        let isPublic: Bool
        if visibility == "private" {
            isPublic = false
        } else if visibility == "public" {
            isPublic = true
        } else {
            throw Abort(.badRequest) // 400
        }
        
        guard let player = database.registeredPlayerExists(withSessionToken: token) else {
            throw Abort(.unauthorized) // 401
        }
        let table = database.createTable(named: tableName, player: player, isPublic: isPublic)
        return try encodeJSON(table)
    }
    
    /**
     List the public tables.
     - Parameter token: The session token of the player, as a string in the request body
     - Throws:
     - 400: Missing token
     - 403: The session token is invalid
     - Returns: A JSON object with a list of public tables (id, name, player list)
     */
    app.post("tables", "public") { req -> String in
        guard let token = req.body.string else {
            throw Abort(.badRequest) // 400
        }
        guard database.isValid(sessionToken: token) else {
            throw Abort(.forbidden) // 403
        }
        let list = database.getPublicTableInfos()
        return try encodeJSON(list)
    }
    
    /**
     Join a table.
     - Parameter table: The table id
     - Parameter token: The session token of the player, as a string in the request body
     - Throws:
     - 400: Missing token
     - 401: The session token is invalid
     - 403: The player already sits at another table
     - 410: The table id doesn't exist
     - 417: The table is already full and can't be joined
     - Returns: Nothing
     */
    app.post("table", "join", ":table") { req -> String in
        guard let table = req.parameters.get("table"),
              let token = req.body.string else {
                  throw Abort(.badRequest)
              }
        switch database.join(tableId: table, playerToken: token) {
        case .success(let table):
            return try encodeJSON(table)
        case .failure(let result):
            switch result {
            case .invalidToken:
                throw Abort(.unauthorized) // 401
            case .alreadyJoinedOtherTable:
                throw Abort(.forbidden) // 403
            case .tableNotFound:
                throw Abort(.gone) // 410
            case .tableIsFull:
                throw Abort(.expectationFailed) // 417
            }
        }
    }
    
    /**
     Leave the current table.
     - Parameter token: The session token of the player, as a string in the request body
     - Throws:
     - 400: Missing token
     - 401: The session token is invalid
     - Returns: Nothing
     */
    app.post("table", "leave") { req -> String in
        guard let token = req.body.string else {
            throw Abort(.badRequest)
        }
        guard database.leaveTable(playerToken: token) else {
            throw Abort(.unauthorized) // 401
        }
        return ""
    }
    
    app.post("player", "action", ":action") { req -> String in
        guard let token = req.body.string,
              let actionString = req.parameters.get("action") else {
                  throw Abort(.badRequest)
              }
        let result: PlayerActionResult
        if let action = PlayerAction(rawValue: actionString) {
            result = database.performAction(playerToken: token, action: action)
        } else if let game = GameType(rawValue: actionString) {
            result = database.select(game: game, playerToken: token)
        } else {
            throw Abort(.badRequest)
        }
        switch result {
        case .success:
            return ""
        case .invalidToken:
            throw Abort(.unauthorized) // 401
        case .noTableJoined:
            throw Abort(.preconditionFailed) // 412
        case .tableNotFull:
            throw Abort(.preconditionFailed) // 412
        case .tableStateInvalid:
            throw Abort(.preconditionFailed) // 412
        case .invalidCard:
            throw Abort(.preconditionFailed) // 412
        }
    }

    app.post("player", "card", ":card") { req -> String in
        guard let token = req.body.string,
              let cardId = req.parameters.get("card"),
              let card = Card(id: cardId) else {
                  throw Abort(.badRequest)
              }
        switch database.play(card: card, playerToken: token) {
        case .success:
            return ""
        case .invalidToken:
            throw Abort(.unauthorized) // 401
        case .noTableJoined:
            throw Abort(.preconditionFailed) // 412
        case .tableStateInvalid:
            throw Abort(.preconditionFailed) // 412
        case .invalidCard:
            throw Abort(.preconditionFailed) // 412
        case .tableNotFull:
            throw Abort(.preconditionFailed) // 412
        }
    }
}
