package com.andr3h3nriqu3s.applications

import java.sql.ResultSet
import java.util.UUID
import kotlin.io.encoding.Base64
import kotlin.random.Random
import org.springframework.http.MediaType
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.jdbc.core.RowMapper
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder
import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

enum class UserLevel(val level: Int) {
    NORMAL(1),
    ADMIN(255)
}

data class UserDb(
        var id: String,
        var username: String,
        var email: String,
        var password: String,
        var level: Int
) {
    fun toSafe(): User {
        return User(this.id, this.username, this.email, this.level)
    }

    fun checkLevel(level: Int): Boolean {
        return (level and this.level) == level
    }

    fun checkLevelThrow(level: Int) {
        if (!this.checkLevel(level)) {
            throw NotAuth()
        }
    }

    companion object : RowMapper<UserDb> {
        override public fun mapRow(rs: ResultSet, rowNum: Int): UserDb {
            return UserDb(
                    rs.getString("id"),
                    rs.getString("username"),
                    rs.getString("email"),
                    rs.getString("passwd"),
                    rs.getInt("level")
            )
        }
    }
}

data class UserCreateRequest(val email: String, val password: String, val username: String)

data class LoginUserRequest(val email: String, val password: String)

data class LoggedInUser(val token: String, val user: User)

data class User(val id: String, var username: String, var email: String, var level: Int)

@RestController
@ControllerAdvice
@RequestMapping("/api/user")
@kotlin.io.encoding.ExperimentalEncodingApi
class UserController(
        val userService: UserService,
        val sessionService: SessionService,
        val sessionServiceCreator: SessionServiceCreator,
) {

    @PostMapping(path = ["/login"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun login(@RequestBody user: LoginUserRequest): LoggedInUser {
        val userdb = userService.login(user.email, user.password)
        val session = sessionServiceCreator.createSession(userdb)

        return LoggedInUser(session.token, userdb.toSafe())
    }

    @GetMapping(path = ["/list"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun list(@RequestHeader("token") token: String): List<User> {
        sessionService.verifyTokenThrow(token).checkLevelThrow(UserLevel.ADMIN.ordinal)
        return userService.findUsers().map { elm -> elm.toSafe() }
    }

    @PostMapping(path = ["/register"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun register(@RequestBody user: UserCreateRequest): LoggedInUser {
        var new_user = userService.createUser(user)
        val session = sessionServiceCreator.createSession(new_user)

        return LoggedInUser(session.token, new_user.toSafe())
    }
}

@Service
class UserService(val db: JdbcTemplate) {

    fun login(email: String, passwd: String): UserDb {
        var user = this.findUserByEmail(email)
        if (user == null) {
            throw UserNotFound()
        }

        var arg2SpringSecurity = Argon2PasswordEncoder(16, 32, 1, 60000, 10)

        // TODO make this safe
        if (!arg2SpringSecurity.matches(passwd, user.password)) {
            throw UserNotFound()
        }
        return user
    }

    fun findUserByEmail(email: String): UserDb? {
        var users = db.query("select * from users where email=?", arrayOf(email), UserDb).toList()

        if (users.size == 0) {
            return null
        }

        return users[0]
    }

    fun findUserById(id: String): UserDb? {
        var users = db.query("select * from users where id=?", arrayOf(id), UserDb).toList()

        if (users.size == 0) {
            return null
        }

        return users[0]
    }

    fun createUser(user: UserCreateRequest): UserDb {
        var user_check = findUserByEmail(user.email)
        if (user_check != null) {
            throw Exception("User already exists")
        }

        val id = UUID.randomUUID().toString()

        var arg2SpringSecurity = Argon2PasswordEncoder(16, 32, 1, 60000, 10)
        val hashPassword = arg2SpringSecurity.encode(user.password)

        var new_user = UserDb(id, user.username, user.email, hashPassword, UserLevel.NORMAL.ordinal)

        db.update(
                "insert into users (id, username, email, passwd, level) values (?, ?, ?, ?, ?)",
                new_user.id,
                user.username,
                user.email,
                new_user.password,
                new_user.level,
        )

        return new_user
    }

    fun findUsers(): List<UserDb> =
            db.query("select * from users") { response, _ ->
                UserDb(
                        response.getString("id"),
                        response.getString("username"),
                        response.getString("email"),
                        response.getString("passwd"),
                        response.getInt("level"),
                )
            }
}

data class Session(val token: String, val user_id: String)

@Service
class SessionService(val db: JdbcTemplate) {

    fun verifyToken(token: String?): UserDb? {
        if (token == null) {
            return null
        }
        var users =
                db
                        .query(
                                "select * from tokens as t inner join users as u on id = user_id where token = ?",
                                arrayOf(token)
                        ) { response, _ ->
                            UserDb(
                                    response.getString("id"),
                                    response.getString("username"),
                                    response.getString("email"),
                                    response.getString("passwd"),
                                    response.getInt("level"),
                            )
                        }
                        .toList()

        if (users.size == 0) {
            return null
        }

        return users[0]
    }

    fun verifyTokenThrow(token: String): UserDb {
        val new_user = this.verifyToken(token)
        if (new_user == null) {
            throw NoToken()
        }
        return new_user
    }
}

@Service
@kotlin.io.encoding.ExperimentalEncodingApi
class SessionServiceCreator(val db: JdbcTemplate) {

    fun createSession(user: UserDb): Session {
        val session = Session(this.generateToken(), user.id)

        db.update(
                "insert into tokens (token, user_id) values (?,?);",
                session.token,
                session.user_id
        )

        return session
    }

    fun generateToken(): String {
        var b = ByteArray(60)
        Random.nextBytes(b)

        return Base64.encode(b)
    }
}
