package ch.threema.app.workers

import android.content.Context
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationCompat
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ExistingWorkPolicy
import androidx.work.ForegroundInfo
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequest
import androidx.work.OutOfQuotaPolicy
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.await
import ch.threema.android.buildNotification
import ch.threema.android.buildOneTimeWorkRequest
import ch.threema.android.buildPeriodicWorkRequest
import ch.threema.android.setConstraints
import ch.threema.android.setInputData
import ch.threema.app.R
import ch.threema.app.applogo.UpdateAppLogoUseCase
import ch.threema.app.asynctasks.AddOrUpdateWorkContactBackgroundTask
import ch.threema.app.di.awaitAppFullyReadyWithTimeout
import ch.threema.app.notifications.NotificationChannels
import ch.threema.app.notifications.NotificationIDs
import ch.threema.app.preference.service.PreferenceService
import ch.threema.app.restrictions.AppRestrictionService
import ch.threema.app.restrictions.AppRestrictions
import ch.threema.app.routines.UpdateWorkInfoRoutine
import ch.threema.app.services.ContactService
import ch.threema.app.services.UserService
import ch.threema.app.services.license.LicenseService
import ch.threema.app.services.notification.NotificationService
import ch.threema.app.tasks.BatchUpdateContactAvailabilityStatusTask
import ch.threema.app.tasks.BatchUpdateContactWorkLastFullSyncAtTask
import ch.threema.app.utils.ConfigUtils
import ch.threema.app.utils.RuntimeUtil
import ch.threema.app.utils.WorkManagerUtil
import ch.threema.base.utils.getThreemaLogger
import ch.threema.common.Http
import ch.threema.data.datatypes.AvailabilityStatus
import ch.threema.data.models.ContactModel
import ch.threema.data.repositories.ContactModelRepository
import ch.threema.domain.models.UserCredentials
import ch.threema.domain.models.VerificationLevel
import ch.threema.domain.models.WorkVerificationLevel
import ch.threema.domain.protocol.api.APIConnector
import ch.threema.domain.protocol.api.work.WorkData
import ch.threema.domain.stores.IdentityStore
import ch.threema.domain.taskmanager.TaskManager
import ch.threema.domain.types.IdentityString
import java.time.Instant
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject

private val logger = getThreemaLogger("WorkSyncWorker")

class WorkSyncWorker(
    private val context: Context,
    workerParameters: WorkerParameters,
) : CoroutineWorker(context, workerParameters), KoinComponent {

    private val contactService: ContactService by inject()
    private val preferenceService: PreferenceService by inject()
    private val licenseService: LicenseService<*> by inject()
    private val apiConnector: APIConnector by inject()
    private val notificationService: NotificationService by inject()
    private val userService: UserService by inject()
    private val identityStore: IdentityStore by inject()
    private val contactModelRepository: ContactModelRepository by inject()
    private val appRestrictions: AppRestrictions by inject()
    private val updateAppLogoUseCase: UpdateAppLogoUseCase by inject()
    private val appRestrictionService: AppRestrictionService by inject()
    private val taskManager: TaskManager by inject()

    override suspend fun doWork(): Result {
        val forceUpdate: Boolean = inputData.getBoolean(EXTRA_FORCE_UPDATE, false)
        val newWorkContacts: MutableList<ContactModel> = mutableListOf()

        logger.info("Refreshing work data. Force = {}", forceUpdate)

        awaitAppFullyReadyWithTimeout(timeout = 20.seconds)
            ?: return Result.failure()

        if (!ConfigUtils.isWorkBuild()) {
            logger.info("Not allowed to run in a non work environment")
            return Result.failure()
        }

        val credentials =
            licenseService.loadCredentials() as? UserCredentials ?: return Result.failure()

        val workData: WorkData?
        try {
            // TODO(ANDR-3172): Get all contacts via contact model repository
            val allContacts: List<ch.threema.storage.models.ContactModel> = contactService.all
            val identities = arrayOfNulls<String>(allContacts.size)
            for (n in allContacts.indices) {
                identities[n] = allContacts[n].identity
            }
            workData = apiConnector.fetchWorkData(
                /* username = */
                credentials.username,
                /* password = */
                credentials.password,
                /* identities = */
                identities,
            )
        } catch (e: Exception) {
            logger.error("Failed to fetch2 work data from API", e)
            notificationService.cancelWorkSyncProgress()
            return Result.failure()
        }

        if (workData.responseCode > 0) {
            logger.error(
                "Failed to fetch2 work data. Server response code = {}",
                workData.responseCode,
            )
            if (workData.responseCode == Http.StatusCode.UNAUTHORIZED) {
                RuntimeUtil.runOnUiThread {
                    Toast.makeText(context, R.string.fetch2_failure, Toast.LENGTH_LONG).show()
                }
            }
            notificationService.cancelWorkSyncProgress()
            return Result.failure()
        }

        val existingWorkIdentities = contactService.allWork.map { it.identity }.toSet()
        val fetchedWorkIdentities = workData.workContacts.map { it.threemaId }.toSet()

        val pendingContactWorkLastFullSyncAtUpdates = mutableMapOf<IdentityString, Instant>()
        val pendingContactAvailabilityStatusUpdates = mutableMapOf<IdentityString, AvailabilityStatus>()

        // Create or update work contacts
        val refreshedWorkIdentities = workData
            .workContacts
            .mapNotNull { workContact ->
                AddOrUpdateWorkContactBackgroundTask(
                    workContact = workContact,
                    myIdentity = userService.identity!!,
                    contactModelRepository = contactModelRepository,
                    pendingUpdateContactWorkLastFullSyncAt = { identity, workLastFullSyncAt ->
                        pendingContactWorkLastFullSyncAtUpdates[identity] = workLastFullSyncAt
                    },
                    pendingUpdateContactAvailabilityStatus = { identity, availabilityStatus ->
                        pendingContactAvailabilityStatusUpdates[identity] = availabilityStatus
                    },
                ).runSynchronously()
            }
            .map(ContactModel::identity)

        // Schedule tasks to batch-update contacts workLastFullSyncAtTimestamp values
        if (pendingContactWorkLastFullSyncAtUpdates.isNotEmpty()) {
            val batchUpdateContactWorkLastFullSyncAtTasks = BatchUpdateContactWorkLastFullSyncAtTask.createBatchedTasks(
                workLastFullSyncAtTimestamps = pendingContactWorkLastFullSyncAtUpdates,
            )
            batchUpdateContactWorkLastFullSyncAtTasks.forEach(taskManager::schedule)
        }

        // Schedule tasks to batch-update contacts availability status values
        if (pendingContactAvailabilityStatusUpdates.isNotEmpty()) {
            val batchUpdateContactAvailabilityStatusTasks = BatchUpdateContactAvailabilityStatusTask.createBatchedTasks(
                availabilityStatuses = pendingContactAvailabilityStatusUpdates,
            )
            batchUpdateContactAvailabilityStatusTasks.forEach(taskManager::schedule)
        }

        val newWorkIdentities = refreshedWorkIdentities - existingWorkIdentities
        newWorkContacts.addAll(
            newWorkIdentities.mapNotNull(contactModelRepository::getByIdentity),
        )

        // Downgrade work contacts
        val downgradedIdentities = existingWorkIdentities - fetchedWorkIdentities
        downgradedIdentities.mapNotNull { contactModelRepository.getByIdentity(it) }.forEach {
            // The contact is no longer a work contact, so set work verification level to none
            it.setWorkVerificationLevelFromLocal(WorkVerificationLevel.NONE)

            // Additionally, the contact may not be server verified anymore (except it has been
            // fully verified before)
            if (it.data?.verificationLevel == VerificationLevel.SERVER_VERIFIED) {
                it.setVerificationLevelFromLocal(VerificationLevel.UNVERIFIED)
            }
        }

        // update app-logos
        logger.trace("Updating app logos")
        val updateAppLogoJob = coroutineScope {
            launch {
                updateAppLogoUseCase.call(
                    lightUrl = workData.logoLight,
                    darkUrl = workData.logoDark,
                    forceUpdate = forceUpdate,
                )
            }
        }
        preferenceService.setCustomSupportUrl(workData.supportUrl)
        // Save the Mini-MDM Parameters to a local file
        appRestrictionService.storeWorkMDMSettings(workData.mdm)

        // update work info
        UpdateWorkInfoRoutine(
            /* apiConnector = */
            apiConnector,
            /* identityStore = */
            identityStore,
            /* deviceService = */
            null,
            /* licenseService = */
            licenseService,
            /* appRestrictions */
            appRestrictions,
        ).run()
        preferenceService.setWorkDirectoryEnabled(workData.directory.enabled)
        preferenceService.setWorkDirectoryCategories(workData.directory.categories)
        preferenceService.setWorkOrganization(workData.organization)
        logger.trace("CheckInterval = {}", workData.checkInterval)
        if (workData.checkInterval > 0) {
            // schedule next interval
            preferenceService.setWorkSyncCheckInterval(workData.checkInterval.seconds)
        }

        notificationService.cancelWorkSyncProgress()

        updateAppLogoJob.join()

        logger.info("Refreshing work data successfully finished")

        if (newWorkContacts.isEmpty()) {
            return Result.success()
        }
        val wasSuccessful = ContactUpdateWorker.fetchAndUpdateContactModels(
            contactModels = newWorkContacts,
            apiConnector = apiConnector,
            preferenceService = preferenceService,
        )
        return if (wasSuccessful) {
            Result.success()
        } else {
            Result.failure()
        }
    }

    override suspend fun getForegroundInfo(): ForegroundInfo {
        val notification = buildNotification(context, NotificationChannels.NOTIFICATION_CHANNEL_WORK_SYNC) {
            setSound(null)
            setSmallIcon(R.drawable.ic_sync_notification)
            setContentTitle(context.getString(R.string.wizard1_sync_work))
            setProgress(0, 0, true)
            setPriority(NotificationCompat.PRIORITY_LOW)
            setAutoCancel(true)
            setLocalOnly(true)
            setOnlyAlertOnce(true)
            setVisibility(NotificationCompat.VISIBILITY_SECRET)
        }

        return ForegroundInfo(NotificationIDs.WORK_SYNC_NOTIFICATION_ID, notification)
    }

    class Scheduler(
        private val workManager: WorkManager,
        private val preferenceService: PreferenceService,
    ) {
        fun schedulePeriodicWorkSync() {
            if (!ConfigUtils.isWorkBuild()) {
                logger.debug("Do not start work sync worker in non-work build")
                return
            }

            val schedulePeriod = preferenceService.getWorkSyncCheckInterval()
            logger.info("Scheduling periodic work sync. Schedule period: {}", schedulePeriod)

            try {
                val policy = if (WorkManagerUtil.shouldScheduleNewWorkManagerInstance(
                        workManager,
                        WorkerNames.WORKER_PERIODIC_WORK_SYNC,
                        schedulePeriod.inWholeMilliseconds,
                    )
                ) {
                    ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
                } else {
                    ExistingPeriodicWorkPolicy.KEEP
                }
                logger.info(
                    "{}: {} existing periodic work",
                    WorkerNames.WORKER_PERIODIC_WORK_SYNC,
                    policy,
                )
                val workRequest = buildPeriodicWorkRequest(schedulePeriod)
                workManager.enqueueUniquePeriodicWork(
                    WorkerNames.WORKER_PERIODIC_WORK_SYNC,
                    policy,
                    workRequest,
                )
            } catch (e: IllegalStateException) {
                logger.error("Unable to schedule periodic work sync work", e)
            }
        }

        suspend fun cancelPeriodicWorkSyncAwait() {
            logger.info("Cancelling periodic work sync")
            val result = workManager.cancelUniqueWork(WorkerNames.WORKER_PERIODIC_WORK_SYNC).await()
            logger.info("Cancelled periodic work sync, result={}", result)
        }

        private fun buildOneTimeWorkRequest(
            forceUpdate: Boolean,
            tag: String?,
        ): OneTimeWorkRequest =
            buildOneTimeWorkRequest<WorkSyncWorker> {
                setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
                setInputData {
                    putBoolean(EXTRA_FORCE_UPDATE, forceUpdate)
                }
                if (tag != null) {
                    addTag(tag)
                }
            }

        private fun buildPeriodicWorkRequest(schedulePeriodMs: Duration): PeriodicWorkRequest =
            buildPeriodicWorkRequest<WorkSyncWorker>(
                repeatInterval = schedulePeriodMs,
            ) {
                setConstraints {
                    setRequiredNetworkType(NetworkType.CONNECTED)
                }
                addTag(schedulePeriodMs.inWholeMilliseconds.toString())
                setInputData {
                    putBoolean(EXTRA_FORCE_UPDATE, false)
                }
            }

        /**
         * Start a one time work sync request. Existing work will be [ExistingWorkPolicy.REPLACE]d.
         *
         * If it is required to run a callback when the request was successful or failed, use
         * `performOneTimeWorkSync(Activity, Runnable, Runnable)`.
         */
        fun performOneTimeWorkSync(
            forceUpdate: Boolean,
            tag: String?,
        ) {
            val workRequest = buildOneTimeWorkRequest(forceUpdate, tag)
            workManager.enqueueUniqueWork(
                WorkerNames.WORKER_WORK_SYNC,
                ExistingWorkPolicy.REPLACE,
                workRequest,
            )
        }

        /**
         * Start a one time work sync request.
         *
         * @param onSuccess is run when the work sync request was successful
         * @param onFail    is run when the work sync request was unsuccessful
         */
        fun performOneTimeWorkSync(
            activity: AppCompatActivity,
            onSuccess: Runnable,
            onFail: Runnable,
        ) {
            val workerTag = "OneTimeWorkSyncWorker"
            val workRequest = buildOneTimeWorkRequest(
                forceUpdate = true,
                tag = workerTag,
            )
            workManager.getWorkInfoByIdLiveData(workRequest.id)
                .observe(activity) { workInfo: WorkInfo? ->
                    when (workInfo?.state) {
                        WorkInfo.State.SUCCEEDED -> onSuccess.run()
                        WorkInfo.State.FAILED -> onFail.run()
                        null -> {
                            // Just log this. It is likely that a non-null work info will be observed
                            // later and the sync completes successfully.
                            logger.info("Work info is null")
                        }
                        else -> Unit
                    }
                }
            workManager.enqueueUniqueWork(
                WorkerNames.WORKER_WORK_SYNC,
                ExistingWorkPolicy.REPLACE,
                workRequest,
            )
        }
    }

    companion object {
        private const val EXTRA_FORCE_UPDATE = "FORCE_UPDATE"
    }
}
