package de.sebse.fuplanner2.auth

import android.accounts.Account
import android.accounts.AccountManager
import android.accounts.AuthenticatorException
import android.content.Context
import android.os.Bundle
import androidx.work.*
import de.sebse.fuplanner2.preferences.AppPreferences
import de.sebse.fuplanner2.utils.console
import de.sebse.fuplanner2.worker.SyncWorker
import java.io.IOException
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

class AppAccounts(val context: Context) {
    private val accountManager = AccountManager.get(context)
    private val workManager = WorkManager.getInstance(context)

    val list: Array<out Account>
    get() = accountManager.getAccountsByType(FuplannerAccountConstants.FU_ACC_TYPE)

    var selectedAccount: Account?
    get() {
        val preferences = AppPreferences.getInstance()
        val accountName = preferences.getString(PREF_SELECTED_ACCOUNT)
        return getAccount(accountName)
            ?.also { preferences.set(PREF_SELECTED_ACCOUNT, it.name ) }
    }
    set(value) {
        val preferences = AppPreferences.getInstance()
        value
            ?.run { preferences.set(PREF_SELECTED_ACCOUNT, name ) }
            ?: preferences.remove(PREF_SELECTED_ACCOUNT)
    }

    fun hasAccounts(): Boolean {
        return list.isNotEmpty()
    }

    fun hasAccountName(name: String, accountType: String? = null): Boolean {
        return accountManager.getAccountsByType(accountType ?: FuplannerAccountConstants.FU_ACC_TYPE).find { it.name == name } != null
    }

    suspend fun refreshSuspended(account: Account): RefreshResults = suspendCoroutine { res -> refresh(account) { res.resume(it) } }

    fun refresh(account: Account, cb: (RefreshResults) -> Unit) {
        val authToken: String? = accountManager.peekAuthToken(account, FuplannerAccountConstants.FU_ACC_TOKEN_TYPE)
        accountManager.invalidateAuthToken(FuplannerAccountConstants.FU_ACC_TYPE, authToken)
        accountManager.getAuthToken(
            account,
            FuplannerAccountConstants.FU_ACC_TOKEN_TYPE,
            null,true, {
                try {
                    it.result
                    cb(RefreshResults.SUCCESS)
                } catch (e: AuthenticatorException) {
                    if (e.message == FuplannerAccountConstants.MSG_INVALID_CREDENTIALS) {
                        cb(RefreshResults.INVALID_PASSWORD)
                    } else {
                        console.error(e)
                        cb(RefreshResults.UNSPECIFIED_ERROR)
                    }
                } catch (e: IOException) {
                    console.error(e)
                    cb(RefreshResults.NETWORK_ERROR)
                } catch (e: Throwable) {
                    console.error(e)
                    cb(RefreshResults.UNSPECIFIED_ERROR)
                }
            }, null)
    }

    fun setPassword(account: Account, passWd: String?) = accountManager.setPassword(account, passWd)
    fun addAccountExplicitly(account: Account, password: String?, bundle: Bundle?) {
        accountManager.addAccountExplicitly(account, password, bundle)
    }

    fun setAuthToken(account: Account, tokenType: String?, authToken: String?) {
        accountManager.setAuthToken(account, tokenType, authToken)
    }

    fun getAccount(accountName: String?): Account? {
        val account = list.find { it.name == accountName }
        return account?.let { return it }
            ?: list.getOrNull(0)
    }

    fun setPeriodicSync() {
        workManager.cancelAllWorkByTag(TAG_SYNC)
        val preferences = AppPreferences.getInstance()
        val syncPeriod = preferences.getLong(PREF_SYNC_PERIOD_MINUTES) ?: 15L
        if (syncPeriod != null) {
            list.forEach {
                val syncWork = PeriodicWorkRequestBuilder<SyncWorker>(syncPeriod, TimeUnit.MINUTES)
                    .setInputData(workDataOf(SyncWorker.KEY_ACCOUNT_NAME to it.name))
                    .setConstraints(Constraints.Builder()
                        //.setRequiresDeviceIdle(true)
                        .setRequiresStorageNotLow(true)
                        //.setRequiredNetworkType(NetworkType.UNMETERED)
                        .setRequiresBatteryNotLow(true)
                        .build()
                    )
                    .addTag(TAG_SYNC)
                    .addTag(it.name)
                    .build()
                workManager.enqueue(syncWork)
            }
        } else {
            console.warn("No sync period set! Removed all accounts from syncing!")
        }
    }

    enum class RefreshResults {
        SUCCESS,
        INVALID_PASSWORD,
        NETWORK_ERROR,
        UNSPECIFIED_ERROR
    }


    companion object {
        const val PREF_SELECTED_ACCOUNT = "PREF_SELECTED_ACCOUNT"
        const val PREF_SYNC_PERIOD_MINUTES = "PREF_SYNC_PERIOD_MINUTES"
        const val TAG_SYNC = "TAG_SYNC"

        private var sInstance: AppAccounts? = null

        fun initialize(context: Context): AppAccounts? {
            if (sInstance == null) {
                synchronized(AppAccounts::class.java) {
                    if (sInstance == null) {
                        sInstance = AppAccounts(context.applicationContext)
                    }
                }
            }
            return sInstance
        }

        fun getInstance(): AppAccounts {
            sInstance?.let {
                return it
            }
            throw NullPointerException("Please call initialize() before getting the instance.")
        }
    }
}
