package de.kif.backend

import de.kif.backend.repository.UserRepository
import de.kif.common.model.Permission
import de.kif.common.model.User
import io.ktor.application.Application
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.auth.*
import io.ktor.request.path
import io.ktor.response.respondRedirect
import io.ktor.sessions.*
import io.ktor.util.hex
import io.ktor.util.pipeline.PipelineContext
import org.mindrot.jbcrypt.BCrypt

interface ErrorContext {
    suspend infix fun onFailure(block: suspend () -> Unit)
}

object ErrorContextIgnore : ErrorContext {
    override suspend fun onFailure(block: suspend () -> Unit) {
        // do nothing
    }
}

class ErrorContextOccur() : ErrorContext {
    override suspend fun onFailure(block: suspend () -> Unit) {
        block()
    }
}

suspend inline fun PipelineContext<Unit, ApplicationCall>.authenticate(
    vararg permissions: Permission,
    block: (user: User) -> Unit
): ErrorContext {
    val user = call.sessions.get<PortalSession>()?.getUser(call)
    return if (user == null || permissions.any { !user.checkPermission(it) }) {
        ErrorContextOccur()
    } else {
        block(user)
        ErrorContextIgnore
    }
}

suspend inline fun PipelineContext<Unit, ApplicationCall>.authenticateOrRedirect(
    vararg permissions: Permission,
    block: (user: User) -> Unit
) {
    authenticate(*permissions, block = block) onFailure {
        call.respondRedirect("/login?redirect=${call.request.path()}}")
    }
}

suspend fun PipelineContext<Unit, ApplicationCall>.isAuthenticated(vararg permissions: Permission): User? {
    val user = call.sessions.get<PortalSession>()?.getUser(call)
    return if (user == null || permissions.any { !user.checkPermission(it) }) {
        null
    } else {
        user
    }
}

data class PortalSession(val userId: Long) {
    suspend fun getUser(call: ApplicationCall): User {
        val user = UserRepository.get(userId)

        if (user == null) {
            call.sessions.clear<PortalSession>()
            call.respondRedirect("/login?onFailure")
            throw IllegalAccessException()
        }

        return user
    }
}

data class UserPrinciple(val user: User) : Principal

fun checkPassword(password: String, hash: String): Boolean {
    return BCrypt.checkpw(password, hash)
}

fun hashPassword(password: String): String {
    return BCrypt.hashpw(password, BCrypt.gensalt())
}

fun Application.security() {

    install(Authentication) {
        form {
            userParamName = "username"
            passwordParamName = "password"
            challenge = FormAuthChallenge.Redirect { _ ->
                "/login?onFailure"
            }
            validate { credential: UserPasswordCredential ->
                val user = UserRepository.find(credential.name) ?: return@validate null
                if (checkPassword(credential.password, user.password)) UserPrinciple(user) else null
            }
        }
    }

    val signKey = hex(Configuration.Security.signKey.replace(" ", ""))

    install(Sessions) {
        cookie<PortalSession>(
            Configuration.Security.sessionName,
            directorySessionStorage(Configuration.Path.sessionsPath.toFile(), cached = false)
        ) {
            cookie.path = "/"
            transform(SessionTransportTransformerMessageAuthentication(signKey))
        }
    }
}
