package dp.kotul.jwt

import dp.kotul.jwt.model.User
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import io.jsonwebtoken.Claims
import io.jsonwebtoken.Jws
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.MalformedJwtException
import io.jsonwebtoken.jackson.io.JacksonDeserializer
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.BufferedReader
import java.io.InputStreamReader
import java.security.KeyFactory
import java.security.PublicKey
import java.security.spec.X509EncodedKeySpec
import java.util.Base64

const val BEARER_PREFIX = "Bearer "

class JwtParser(private val mapper: ObjectMapper) {

    val publicKey = getJwtPublicKey(getJwtPublicKeyFromFile())
    val parser = Jwts.parserBuilder().setSigningKey(publicKey).deserializeJsonWith(JacksonDeserializer(mapper)).build()

    private val log: Logger = LoggerFactory.getLogger(JwtParser::class.java)

    constructor(): this(ObjectMapper().registerKotlinModule())

    fun verifyBearerAndGetUser(jwt: String?): User {
        return verifyAndGetUser(jwt.stripBearerPrefix())
    }

    fun verifyBearerAndGetNullableUser(jwt: String?): User? {
        return verifyAndGetNullableUser(jwt.stripBearerPrefix())
    }

    private fun verifyAndGetJwtClaims(jwt: String?): Jws<Claims> {
        if (jwt.isNullOrEmpty()) {
            throw MalformedJwtException("JWT does not have 'Bearer ' prefix or is null")
        }

        try {
            return parser.parseClaimsJws(jwt)
        } catch (exception: IllegalArgumentException) {
            throw MalformedJwtException(exception.message, exception)
        }
    }

    private fun verifyAndGetUser(jwt: String?): User {
        log.info("Parsing JWT into User")
        val claims = verifyAndGetJwtClaims(jwt)
        return getUserFromJwtClaims(claims)
    }

    private fun getUserFromJwtClaims(claims: Jws<Claims>): User {
        val user = mapper.convertValue(claims.body["user"], User::class.java)
        log.info("Successfully parsed user from JWT - userId:${user.usrId}, comId:${user.comId}")
        return user
    }


    fun verifyBearerAndGetJwtClaims(jwt: String?): Jws<Claims> {
        return verifyAndGetJwtClaims(jwt.stripBearerPrefix())
    }

    fun verifyBearerAndGetNullableJwtClaims(jwt: String?): Jws<Claims>? {
        return verifyAndGetNullableJwtClaims(jwt.stripBearerPrefix())
    }

    fun verifyAndGetNullableUser(jwt: String?): User? {
        log.info("Parsing JWT into nullable User")
        return try {
            val claims = verifyAndGetNullableJwtClaims(jwt) ?: return null
            getUserFromJwtClaims(claims)
        } catch (exception: Exception) {
            log.warn(exception.message, exception)
            null
        }
    }

    fun verifyAndGetNullableJwtClaims(jwt: String?): Jws<Claims>? {
        return try {
            verifyAndGetJwtClaims(jwt)
        } catch (exception: Exception) {
            log.warn("Couldn't parse JWT", exception)
            null
        }
    }

    fun getJwtPublicKey(key: String): PublicKey {
        val factory = KeyFactory.getInstance("EC")
        val publicSpec = X509EncodedKeySpec(Base64.getMimeDecoder().decode(key))
        return factory.generatePublic(publicSpec)
    }

    fun getJwtPublicKeyFromFile(): String {
        InputStreamReader(JwtParser::class.java.classLoader.getResourceAsStream("jwt-public-key.txt")!!).use { inputStreamReader ->
            BufferedReader(inputStreamReader).use { bufferedReader ->
                return bufferedReader.readLine()
            }
        }
    }

    fun String?.stripBearerPrefix(): String? {
        if (this == null) {
            return null
        }

        return if (this.startsWith(prefix = BEARER_PREFIX)) {
            this.substringAfter(BEARER_PREFIX)
        } else {
            return null
        }
    }
}
