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) {
    registerPlayer(app)
    requestPlayerPasswordReset(app)
    resetPlayerPasswordWithEmailToken(app)
    deletePlayer(app)
    loginPlayer(app)
    resumeSession(app)
    logoutPlayer(app)
    getTableForPlayer(app)
    openWebsocket(app)
    createTable(app)
    getPublicTables(app)
    joinTable(app)
    leaveTable(app)
    performActionForPlayer(app)
    playCard(app)
}

// MARK: Players & Sessions

/**
 Create a new player.

 Headers:
 - `name`: The username of the player
 - `password`: The password of the player
 - `email`: Optional email address for password reset

 Possible responses:
 - `200`: On success, with the session token for the registered user in the reponse body
 - `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
 */
func registerPlayer(_ app: Application) {
    app.post("player", "register") { request async throws -> SessionToken in
        let name = try request.header(.name)
        let hash = try request.hashedPassword() // errors: 400, 424
        let mail = request.optionalHeader(.email)?.trimmed.nonEmpty

        guard name.count < maximumPlayerNameLength else {
            throw Abort(.notAcceptable) // 406
        }

        // Can throw conflict (409)
        // if either the player exists, or the email is already in use
        return try await server.registerPlayer(named: name, hash: hash, email: mail, in: request.db)
    }
}

/**
 Request an email to reset the password of a player.

 Headers:
 - `name`: The player name

 Possible responses:
 - `200`: Success, email will be sent
 - `400`: Missing name header
 - `417`: Player name not found or no email registered
 */
func requestPlayerPasswordReset(_ app: Application) {
    app.post("player", "password", "reset") { request async throws -> HTTPResponseStatus in
        let name = try request.header(.name) // Error: 400
        try await server.sendPasswordResetEmailIfPossible(name: name, request: request) // 417
        return .ok
    }
}

/**
 Use a token from a password reset email to change the password.

 Headers:
 - `token`: The one-time recovery token
 - `password`: The new password for the user

 Possible responses:
 - `200`: Success, password changed
 - `400`: Missing token or password header
 - `417`: Player name not found or no email registered
 - `424`: Password could not be hashed
 */
func resetPlayerPasswordWithEmailToken(_ app: Application) {
    app.post("player", "password", "new") { req async throws -> HTTPResponseStatus in
        let token = try req.header(.token) // 400
        let hash = try req.hashedPassword() // errors: 400, 424
        try await server.updatePassword(password: hash, forResetToken: token, in: req.db) // 417
        return .ok
    }
}

/**
 Delete a player.

 **Headers**
 - `name`: The name of the player
 - `password`: The password of the player

 **Possible errors**
 - `400`: Missing name or password
 - `401`: The password or user name is invalid
 - `424`: The password could not be hashed
 */
func deletePlayer(_ app: Application) {
    app.post("player", "delete") { request async throws -> HTTPResponseStatus in
        let name = try request.header(.name) // 400
        let password = try request.header(.password) // 400

        let hash = try await server.passwordHashForExistingPlayer(named: name, in: request.db)
        guard try request.password.verify(password, created: hash) else {
            return .unauthorized // 401
        }
        try await server.deletePlayer(named: name, in: request.db)
        return .ok
    }
}

/**
 Log in as an existing player.

 **Headers**
 - `name`: The name of the player
 - `password`: The password of the player

 **Possible errors**
 - `400`: Missing name or password
 - `401`: The password or user name is invalid
 - `424`: The password could not be hashed

 **Response**
 - `body`: The session token for the user
 */
func loginPlayer(_ app: Application) {
    app.post("player", "login") { request async throws -> String in
        let name = try request.header(.name) // 400
        let password = try request.header(.password) // 400

        let hash = try await server.passwordHashForExistingPlayer(named: name, in: request.db)
        guard try request.password.verify(password, created: hash) else {
            throw Abort(.unauthorized) // 401
        }
        return server.startNewSessionForRegisteredPlayer(named: name)
    }
}

/**
 Log in using a session token.

 **Headers**
 - `token`: The session token of the player

 **Possible errors**
 - `400`: Missing token
 - `401`: The token is invalid

 **Response**
 - `body`: The player name associated with the session token
 */
func resumeSession(_ app: Application) {
    app.post("player", "resume") { request -> String in
        let token = try request.header(.token)
        
        guard let player = server.registeredPlayerExists(withSessionToken: token) else {
            throw Abort(.unauthorized) // 401
        }
        return player
    }
}

/**
 Log out.

 **Headers**
 - `token`: The session token of the player

 **Possible errors**
 - `400`: Missing token

 - Note: The request always succeeds when correctly formed, even for invalid and expired tokens
 */
func logoutPlayer(_ app: Application) {
    app.post("player", "logout") { request -> HTTPResponseStatus in
        let token = try request.header(.token)

        server.endSession(forSessionToken: token)
        return .ok
    }
}

/**
 Get the current table of the player, if one exists.

 **Headers**
 - `token`: The session token of the player

 **Possible errors**
 - `400`: Missing token
 - `401`: The token is invalid

 **Response**
 - `body`: The table info, or an empty string
 */
func getTableForPlayer(_ app: Application) {
    app.post("player", "table") { request -> String in
        let token = try request.header(.token)

        guard let player = server.registeredPlayerExists(withSessionToken: token) else {
            throw Abort(.unauthorized) // 401
        }
        guard let info = server.currentTableOfPlayer(named: player) else {
            return ""
        }
        return try encodeJSON(info)
    }
}

/**
 Start a new websocket connection for the client to receive table updates from the server

 - Note: The first (and only) message from the client over the connection must be a valid session token.
 */
func openWebsocket(_ app: Application) {
    app.webSocket("session", "start") { req, socket in
        socket.onText { socket, text in
            guard server.startSession(socket: socket, sessionToken: text) else {
                _ = socket.close()
                return
            }
        }
    }
}

// MARK: Tables

/**
 Create a new table.

 **Headers**
 - `name`: The name of the table
 - `token`: The session token of the player
 - `visibility`: The visibility of the table (`private` or `public`)

 **Errors**
 - `400`: Missing token, table name or invalid visibility
 - `401`: The session token is invalid

 **Response**
 - `body`: The table id
 */
func createTable(_ app: Application) {
    app.post("table", "create") { request -> String in
        let tableName = try request.header(.name)
        let token = try request.header(.token)
        let visibility = try request.header(.visibility)

        let isPublic: Bool
        if visibility == "private" {
            isPublic = false
        } else if visibility == "public" {
            isPublic = true
        } else {
            throw Abort(.badRequest) // 400
        }
        
        guard let player = server.registeredPlayerExists(withSessionToken: token) else {
            throw Abort(.unauthorized) // 401
        }
        let result = try await server.createTable(named: tableName, player: player, isPublic: isPublic, in: request.db)
        return try encodeJSON(result)
    }
}

/**
 List the public tables.

 **Headers**
 - `token`: The session token of the player

 **Possible errors**
 - `400`: Missing token
 - `401`: The session token is invalid

 **Response**
 - `body`: A JSON object with a list of public tables (id, name, player list)
 */
func getPublicTables(_ app: Application) {
    app.post("tables", "public") { request -> String in
        let token = try request.header(.token)

        guard server.isValid(sessionToken: token) else {
            throw Abort(.unauthorized) // 401
        }
        let list = server.getPublicTableInfos()
        return try encodeJSON(list)
    }
}

/**
 Join a table.

 **Headers**
 - `name`: The table id
 - `token`: The session token of the player

 **Possible errors**
 - `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
 */
func joinTable(_ app: Application) {
    app.post("table", "join") { request -> String in
        let string = try request.header(.name)
        let token = try request.header(.token)
        guard let table = UUID(uuidString: string) else {
            throw Abort(.badRequest)
        }
        let result = try await server.join(tableId: table, playerToken: token, in: request.db)
        return try encodeJSON(result)
    }
}

/**
 Leave the current table.

 **Headers**
 - `token`: The session token of the player

 **Possible errors**
 - `400`: Missing token
 - `401`: The session token is invalid
 */
func leaveTable(_ app: Application) {
    app.post("table", "leave") { request -> HTTPResponseStatus in
        let token = try request.header(.token)

        try await server.leaveTable(playerToken: token, in: request.db)
        return .ok
    }
}

/**
 Perform an action, either a game selection or a player action.

 **Headers**
 - `token`: The session token of the player
 - `action`: The action to perform

 **Possible errors**
 - `400`: Missing token or action
 - `401`: The session token is invalid
 - `412`: The action is not allowed
 */
func performActionForPlayer(_ app: Application) {
    app.post("player", "action") { request -> HTTPResponseStatus in
        let token = try request.header(.token)
        let actionString = try request.header(.action)

        let result: PlayerActionResult
        if let action = PlayerAction(rawValue: actionString) {
            result = server.performAction(playerToken: token, action: action)
        } else if let game = GameType(rawValue: actionString) {
            result = server.select(game: game, playerToken: token)
        } else {
            throw Abort(.badRequest)
        }
        switch result {
        case .success:
            return .ok
        case .invalidToken:
            return .unauthorized // 401
        case .noTableJoined:
            return .preconditionFailed // 412
        case .tableNotFull:
            return .preconditionFailed // 412
        case .tableStateInvalid:
            return .preconditionFailed // 412
        case .invalidCard:
            return .preconditionFailed // 412
        }
    }
}

/**
 Play a card as the active player.

 **Headers**
 - `token`: The session token of the player
 - `action`: The id of the card to play

 **Possible errors**
 - `400`: Missing token or card id
 - `401`: The session token is invalid
 - `412`: The action is not allowed
 */
func playCard(_ app: Application) {
    app.post("player", "card") { request async throws -> HTTPResponseStatus in
        let token = try request.header(.token)
        let cardId = try request.header(.action)
        guard let card = Card(id: cardId) else {
            throw Abort(.badRequest)
        }

        switch try await server.play(card: card, playerToken: token, in: request.db) {
        case .success:
            return .ok
        case .invalidToken:
            return .unauthorized // 401
        case .noTableJoined:
            return .preconditionFailed // 412
        case .tableStateInvalid:
            return .preconditionFailed // 412
        case .invalidCard:
            return .preconditionFailed // 412
        case .tableNotFull:
            return .preconditionFailed // 412
        }
    }
}
