package ru.durakscore.app

import android.content.Context
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import java.nio.ByteBuffer
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec


data class DurakSession(
    val uid: String,
    val name: String,
    val email: String,
    val token: String,
    val expiresAtEpochMillis: Long
)


object DurakAuthSession {

    private const val PREFS_NAME =
        "durakometr_auth_v1"

    private const val KEY_ALIAS =
        "durakometr_session_key_v1"

    private const val PREF_UID =
        "uid"

    private const val PREF_NAME =
        "name"

    private const val PREF_EMAIL =
        "email"

    private const val PREF_TOKEN =
        "token_enc"

    private const val PREF_EXPIRES =
        "expires"

    @Volatile
    private var cached:
        DurakSession? =
        null

    private lateinit var appContext:
        Context


    fun initialize(
        context: Context
    ) {

        appContext =
            context.applicationContext

        cached =
            loadFromDisk()
    }


    fun current():
        DurakSession? {

        val session =
            cached

        if (
            session != null &&
            session.expiresAtEpochMillis >
            System.currentTimeMillis()
        ) {
            return session
        }

        if (
            session != null
        ) {
            clear()
        }

        return null
    }


    fun save(
        session: DurakSession
    ) {

        check(
            ::appContext.isInitialized
        ) {
            "DurakAuthSession.initialize() must be called first"
        }

        val encryptedToken =
            encrypt(
                session.token
            )

        appContext
            .getSharedPreferences(
                PREFS_NAME,
                Context.MODE_PRIVATE
            )
            .edit()
            .putString(
                PREF_UID,
                session.uid
            )
            .putString(
                PREF_NAME,
                session.name
            )
            .putString(
                PREF_EMAIL,
                session.email
            )
            .putString(
                PREF_TOKEN,
                encryptedToken
            )
            .putLong(
                PREF_EXPIRES,
                session.expiresAtEpochMillis
            )
            .apply()

        cached =
            session
    }


    fun clear() {

        if (
            ::appContext.isInitialized
        ) {
            appContext
                .getSharedPreferences(
                    PREFS_NAME,
                    Context.MODE_PRIVATE
                )
                .edit()
                .clear()
                .apply()
        }

        cached =
            null
    }


    private fun loadFromDisk():
        DurakSession? {

        return try {

            val prefs =
                appContext
                    .getSharedPreferences(
                        PREFS_NAME,
                        Context.MODE_PRIVATE
                    )

            val uid =
                prefs
                    .getString(
                        PREF_UID,
                        null
                    )
                    ?.trim()
                    .orEmpty()

            val name =
                prefs
                    .getString(
                        PREF_NAME,
                        null
                    )
                    ?.trim()
                    .orEmpty()

            val email =
                prefs
                    .getString(
                        PREF_EMAIL,
                        null
                    )
                    ?.trim()
                    .orEmpty()

            val encryptedToken =
                prefs
                    .getString(
                        PREF_TOKEN,
                        null
                    )
                    .orEmpty()

            val expires =
                prefs
                    .getLong(
                        PREF_EXPIRES,
                        0L
                    )

            if (
                uid.isBlank() ||
                encryptedToken.isBlank() ||
                expires <=
                    System.currentTimeMillis()
            ) {
                return null
            }

            val token =
                decrypt(
                    encryptedToken
                )

            if (
                token.isBlank()
            ) {
                return null
            }

            DurakSession(
                uid =
                    uid,
                name =
                    name,
                email =
                    email,
                token =
                    token,
                expiresAtEpochMillis =
                    expires
            )

        } catch (
            error: Exception
        ) {

            appContext
                .getSharedPreferences(
                    PREFS_NAME,
                    Context.MODE_PRIVATE
                )
                .edit()
                .clear()
                .apply()

            null
        }
    }


    private fun key():
        SecretKey {

        val keyStore =
            KeyStore
                .getInstance(
                    "AndroidKeyStore"
                )
                .apply {
                    load(
                        null
                    )
                }

        val existing =
            keyStore
                .getKey(
                    KEY_ALIAS,
                    null
                )
                as? SecretKey

        if (
            existing != null
        ) {
            return existing
        }

        val generator =
            KeyGenerator
                .getInstance(
                    KeyProperties
                        .KEY_ALGORITHM_AES,
                    "AndroidKeyStore"
                )

        generator.init(
            KeyGenParameterSpec
                .Builder(
                    KEY_ALIAS,
                    KeyProperties.PURPOSE_ENCRYPT or
                        KeyProperties.PURPOSE_DECRYPT
                )
                .setBlockModes(
                    KeyProperties
                        .BLOCK_MODE_GCM
                )
                .setEncryptionPaddings(
                    KeyProperties
                        .ENCRYPTION_PADDING_NONE
                )
                .build()
        )

        return generator
            .generateKey()
    }


    private fun encrypt(
        value: String
    ): String {

        val cipher =
            Cipher.getInstance(
                "AES/GCM/NoPadding"
            )

        cipher.init(
            Cipher.ENCRYPT_MODE,
            key()
        )

        val encrypted =
            cipher.doFinal(
                value.toByteArray(
                    Charsets.UTF_8
                )
            )

        val iv =
            cipher.iv

        val packed =
            ByteBuffer
                .allocate(
                    4 +
                        iv.size +
                        encrypted.size
                )
                .putInt(
                    iv.size
                )
                .put(
                    iv
                )
                .put(
                    encrypted
                )
                .array()

        return Base64.encodeToString(
            packed,
            Base64.NO_WRAP
        )
    }


    private fun decrypt(
        packedValue: String
    ): String {

        val packed =
            Base64.decode(
                packedValue,
                Base64.NO_WRAP
            )

        val buffer =
            ByteBuffer.wrap(
                packed
            )

        val ivLength =
            buffer.int

        require(
            ivLength in
                12..32
        )

        val iv =
            ByteArray(
                ivLength
            )

        buffer.get(
            iv
        )

        val encrypted =
            ByteArray(
                buffer.remaining()
            )

        buffer.get(
            encrypted
        )

        val cipher =
            Cipher.getInstance(
                "AES/GCM/NoPadding"
            )

        cipher.init(
            Cipher.DECRYPT_MODE,
            key(),
            GCMParameterSpec(
                128,
                iv
            )
        )

        return cipher
            .doFinal(
                encrypted
            )
            .toString(
                Charsets.UTF_8
            )
    }
}
