package dev.inmo.postssystem.features.auth.server.tokens

import com.soywiz.klock.DateTime
import com.soywiz.klock.milliseconds
import dev.inmo.postssystem.features.auth.common.*
import dev.inmo.postssystem.features.common.common.Milliseconds
import dev.inmo.postssystem.features.users.common.*
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
import dev.inmo.micro_utils.repos.create
import dev.inmo.micro_utils.repos.deleteById
import dev.inmo.micro_utils.repos.exposed.AbstractExposedCRUDRepo
import dev.inmo.micro_utils.repos.exposed.initTable
import kotlinx.coroutines.*
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.statements.InsertStatement
import org.jetbrains.exposed.sql.statements.UpdateStatement
import org.jetbrains.exposed.sql.transactions.transaction

private class ExposedAuthTokensRepo(
    override val database: Database
) : AuthTokensRepo, AbstractExposedCRUDRepo<AuthTokenModel, AuthToken, AuthTokenModel>(
    tableName = "ExposedAuthTokensRepo"
) {
    private val tokenColumn = text("token")
    private val refreshTokenColumn = text("refreshToken")
    private val userIdColumn = long("userId")
    private val expiringColumn = double("expiring")
    private val dieColumn = double("die")
    override val primaryKey: PrimaryKey = PrimaryKey(tokenColumn)

    override val selectByIds: SqlExpressionBuilder.(List<AuthToken>) -> Op<Boolean> = {
        tokenColumn.inList(it.map { it.string })
    }

    override val selectById: SqlExpressionBuilder.(AuthToken) -> Op<Boolean> = {
        tokenColumn.eq(it.string)
    }
    override val ResultRow.asObject: AuthTokenModel
        get() = AuthTokenModel(
            AuthToken(get(tokenColumn)),
            RefreshToken(get(refreshTokenColumn)),
            UserId(get(userIdColumn)),
            DateTime(get(expiringColumn)),
            DateTime(get(dieColumn))
        )

    init {
        initTable()
    }

    override fun insert(value: AuthTokenModel, it: InsertStatement<Number>) {
        it[tokenColumn] = value.token.string
        it[refreshTokenColumn] = value.refreshToken.string
        it[userIdColumn] = value.userId.long
        it[expiringColumn] = value.expiring.unixMillisDouble
        it[dieColumn] = value.die.unixMillisDouble
    }

    override fun update(id: AuthToken, value: AuthTokenModel, it: UpdateStatement) {
        it[tokenColumn] = value.token.string
        it[refreshTokenColumn] = value.refreshToken.string
        it[userIdColumn] = value.userId.long
        it[expiringColumn] = value.expiring.unixMillisDouble
        it[dieColumn] = value.die.unixMillisDouble
    }

    override fun InsertStatement<Number>.asObject(value: AuthTokenModel): AuthTokenModel = AuthTokenModel(
        AuthToken(get(tokenColumn)),
        RefreshToken(get(refreshTokenColumn)),
        UserId(get(userIdColumn)),
        DateTime(get(expiringColumn)),
        DateTime(get(dieColumn))
    )

    override suspend fun getByRefreshToken(refreshToken: RefreshToken): AuthTokenModel? = transaction(database) {
        select { refreshTokenColumn.eq(refreshToken.string) }.limit(1).firstOrNull() ?.asObject
    }

    override suspend fun replaceToken(toRemove: AuthToken, toInsert: AuthTokenModel): Boolean = transaction {
        deleteWhere { tokenColumn.eq(toRemove.string) } > 0 && insert {
            insert(toInsert, it)
        }.insertedCount > 0
    }

    override suspend fun deleteDied(
        now: DateTime
    ) = transaction(database) {
        val nowAsDouble = now.unixMillisDouble
        val tokens = select { dieColumn.less(nowAsDouble) }.map { it[tokenColumn] }
        deleteWhere { dieColumn.less(nowAsDouble) }
        tokens
    }.forEach {
        deleteObjectsIdsChannel.emit(AuthToken(it))
    }
}

class DefaultAuthTokensService(
    private val authTokensRepo: AuthTokensRepo,
    private val usersRepo: ReadUsersStorage,
    private val userAuthenticator: UserAuthenticator,
    private val tokenLifetime: Milliseconds,
    private val cleaningScope: CoroutineScope
) : AuthTokensService, AuthFeature {
    private val tokenDieLifetime = tokenLifetime * 2

    constructor(
        database: Database,
        usersRepo: ReadUsersStorage,
        userAuthenticator: UserAuthenticator,
        tokenLifetime: Milliseconds,
        cleaningScope: CoroutineScope
    ): this(
        ExposedAuthTokensRepo(database),
        usersRepo,
        userAuthenticator,
        tokenLifetime,
        cleaningScope
    )

    init {
        cleaningScope.launchSafelyWithoutExceptions {
            while (isActive) {
                authTokensRepo.deleteDied()
                delay(tokenLifetime)
            }
        }
    }

    override suspend fun getUserPrincipal(authToken: AuthToken): Result<User> {
        val authTokenModel = authTokensRepo.getById(authToken) ?: return Result.failure(AuthTokenNotFoundException)
        return if (authTokenModel.expiring < DateTime.now()) {
            Result.failure(AuthTokenExpiredException)
        } else {
            val user = usersRepo.getById(authTokenModel.userId) ?: return Result.failure(UserNotFoundException)
            Result.success(user)
        }
    }

    override suspend fun auth(creds: AuthCreds): AuthTokenInfo? {
        val user = userAuthenticator(creds) ?: return null
        val now = DateTime.now()
        val preAuthTokenModel = AuthTokenModel(
            AuthToken(),
            RefreshToken(),
            user.id,
            now + tokenLifetime.milliseconds,
            now + tokenDieLifetime.milliseconds
        )
        val tokenModel = authTokensRepo.create(preAuthTokenModel).firstOrNull() ?: return null
        return AuthTokenInfo(
            tokenModel.token,
            tokenModel.refreshToken
        )
    }

    override suspend fun refresh(refresh: RefreshToken): AuthTokenInfo? {
        val previousAuthTokenModel = authTokensRepo.getByRefreshToken(refresh) ?: return null
        val now = DateTime.now()

        if (previousAuthTokenModel.die < now) {
            authTokensRepo.deleteById(previousAuthTokenModel.token)
            return null
        }

        val newAuthTokenModel = AuthTokenModel(
            AuthToken(),
            RefreshToken(),
            previousAuthTokenModel.userId,
            now + tokenLifetime.milliseconds,
            now + tokenDieLifetime.milliseconds
        )
        return if (authTokensRepo.replaceToken(previousAuthTokenModel.token, newAuthTokenModel)) {
            AuthTokenInfo(newAuthTokenModel.token, newAuthTokenModel.refreshToken)
        } else {
            null
        }
    }

    override suspend fun getMe(authToken: AuthToken): User? {
        return usersRepo.getById(
            authTokensRepo.getById(authToken) ?.userId ?: return null
        )
    }
}
