package ch.threema.app.protocolsteps

import androidx.annotation.WorkerThread
import ch.threema.app.services.license.LicenseService
import ch.threema.app.utils.ConfigUtils
import ch.threema.base.ThreemaException
import ch.threema.base.utils.getThreemaLogger
import ch.threema.data.IdentityProvider
import ch.threema.data.datatypes.AvailabilityStatus
import ch.threema.data.datatypes.ConversationVisibility
import ch.threema.data.datatypes.PredefinedContact
import ch.threema.data.models.ContactModel
import ch.threema.data.models.ContactModelData
import ch.threema.data.repositories.ContactModelRepository
import ch.threema.data.toContactModelData
import ch.threema.domain.models.AcquaintanceLevel
import ch.threema.domain.models.BasicContact
import ch.threema.domain.models.ContactSyncState
import ch.threema.domain.models.IdentityState
import ch.threema.domain.models.IdentityType
import ch.threema.domain.models.ReadReceiptPolicy
import ch.threema.domain.models.TypingIndicatorPolicy
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.APIConnector.FetchIdentityResult
import ch.threema.domain.protocol.api.work.WorkContact
import ch.threema.domain.stores.ContactStore
import ch.threema.domain.taskmanager.NetworkException
import ch.threema.domain.taskmanager.ProtocolException
import ch.threema.domain.types.IdentityString
import java.time.Instant

private val logger = getThreemaLogger("ValidContactsLookupSteps")

sealed class ContactOrInit(val identity: IdentityString)

data class Contact(val contactModel: ContactModel) : ContactOrInit(contactModel.identity)

data class SpecialContact(val cachedContact: BasicContact) : ContactOrInit(cachedContact.identity)

data class Init(val contactModelData: ContactModelData) : ContactOrInit(contactModelData.identity)

class Invalid(identity: IdentityString) : ContactOrInit(identity)

class UserContact(identity: IdentityString) : ContactOrInit(identity)

class PredefinedContactPublicKeyMismatch(identity: IdentityString) : ContactOrInit(identity)

class ValidContactsLookupSteps(
    private val identityProvider: IdentityProvider,
    private val contactStore: ContactStore,
    private val contactModelRepository: ContactModelRepository,
    private val licenseService: LicenseService<*>,
    private val apiConnector: APIConnector,
) {

    @WorkerThread
    fun run(identity: IdentityString): ContactOrInit =
        run(setOf(identity))[identity] ?: error("Contact or init cannot be null")

    @WorkerThread
    fun run(identities: Set<IdentityString>): Map<IdentityString, ContactOrInit> {
        val contactOrInitMap = mutableMapOf<IdentityString, ContactOrInit>()
        val unknownIdentities = mutableSetOf<IdentityString>()
        val myIdentity = identityProvider.getIdentityString()!!

        for (identity in identities) {
            val result = checkLocally(
                identity = identity,
                myIdentity = myIdentity,
                contactStore = contactStore,
                contactModelRepository = contactModelRepository,
            )

            if (result != null) {
                contactOrInitMap[identity] = result
            } else {
                unknownIdentities.add(identity)
            }
        }

        if (unknownIdentities.isEmpty()) {
            return contactOrInitMap
        }

        val unknownWorkContacts: Map<IdentityString, WorkContact> =
            if (ConfigUtils.isWorkBuild()) {
                val credentials = licenseService.loadCredentials()
                if (credentials !is UserCredentials) {
                    logger.error("Work build without user credentials")
                    emptyMap()
                } else {
                    checkWorkAPI(unknownIdentities, apiConnector, credentials)
                }
            } else {
                emptyMap()
            }

        fetchIdentities(unknownIdentities, unknownWorkContacts, apiConnector)
            .forEach { contactOrInit ->
                contactOrInitMap[contactOrInit.identity] = contactOrInit
            }

        // TODO(SE-173): Run the contact import flow for `contactOrInitMap` and update verification
        //  level for all whose associated phone number / email could be matched. Import first name and
        //  last name and set sync state to imported. Clarify precedence regarding work API.

        return contactOrInitMap
    }

    private fun checkLocally(
        identity: IdentityString,
        myIdentity: IdentityString,
        contactStore: ContactStore,
        contactModelRepository: ContactModelRepository,
    ): ContactOrInit? {
        // Skip the user's identity
        if (identity == myIdentity) {
            return UserContact(myIdentity)
        }

        // Add special contact
        val specialContact = PredefinedContact.getSpecialContact(identity)
        if (specialContact != null) {
            return SpecialContact(specialContact.toBasicContact())
        }

        // Check if contact is known
        val contactModel = contactModelRepository.getByIdentity(identity)
        if (contactModel != null) {
            return Contact(contactModel)
        }

        // Check cache
        val cachedContact = contactStore.getContactForIdentityIncludingCache(identity)
        if (cachedContact is BasicContact) {
            // In case of a cached basic contact we have all the required information to skip fetching the contact from the server.
            return Init(
                contactModelData = cachedContact.toContactModelData(),
            )
        }

        return null
    }

    @WorkerThread
    private fun checkWorkAPI(
        unknownIdentities: Set<IdentityString>,
        apiConnector: APIConnector,
        credentials: UserCredentials,
    ): Map<IdentityString, WorkContact> {
        val workContacts =
            try {
                apiConnector.fetchWorkContacts(
                    /* username = */
                    credentials.username,
                    /* password = */
                    credentials.password,
                    /* identities = */
                    unknownIdentities.toTypedArray(),
                )
            } catch (e: Exception) {
                // TODO(ANDR-3262): Handle different work api server result codes
                logger.warn("Exception during work contact fetch", e)
                throw e
            }
        return workContacts.associateBy(WorkContact::threemaId)
    }

    @WorkerThread
    private fun fetchIdentities(
        unknownIdentities: Set<IdentityString>,
        unknownWorkContacts: Map<IdentityString, WorkContact>,
        apiConnector: APIConnector,
    ): Set<ContactOrInit> {
        val fetchResults = try {
            apiConnector.fetchIdentities(unknownIdentities.toList())
        } catch (e: Exception) {
            when (e) {
                is NetworkException -> throw ProtocolException(
                    e.message ?: "Could not fetch identities",
                )

                is ThreemaException -> throw ProtocolException(
                    e.message ?: "Could not fetch server url",
                )

                else -> throw e
            }
        }

        val fetchedContacts: MutableSet<ContactOrInit> = fetchResults
            .map { fetchIdentityResult ->
                fetchIdentityResult.toContactModelData(
                    workContact = unknownWorkContacts[fetchIdentityResult.identity],
                )
            }
            .map { contactModelData ->
                if (contactModelData.activityState == IdentityState.INVALID) {
                    Invalid(contactModelData.identity)
                } else {
                    Init(contactModelData)
                }
            }
            .map { contactOrInit ->
                val predefinedContact = PredefinedContact.getPredefinedContact(contactOrInit.identity)
                if (predefinedContact != null) {
                    when (contactOrInit) {
                        is Contact -> error("Contact or init cannot be a contact at this point")
                        is SpecialContact -> error("Contact or init cannot be a special contact at this point")
                        is Init -> {
                            if (contactOrInit.contactModelData.publicKey.contentEquals(predefinedContact.publicKey)) {
                                contactOrInit.copy(
                                    contactModelData = contactOrInit.contactModelData.copy(
                                        verificationLevel = VerificationLevel.FULLY_VERIFIED,
                                        nickname = predefinedContact.nickname,
                                    ),
                                )
                            } else {
                                logger.warn("Predefined contact has different public key")
                                PredefinedContactPublicKeyMismatch(contactOrInit.identity)
                            }
                        }

                        is Invalid -> contactOrInit
                        is UserContact -> error("Contact or init cannot be the user at this point")
                        is PredefinedContactPublicKeyMismatch -> error("Contact or init cannot be a public key mismatch at this point")
                    }
                } else {
                    contactOrInit
                }
            }
            .toMutableSet()

        // Identities where we did not receive a response from the server are considered invalid
        (unknownIdentities - fetchedContacts.map(ContactOrInit::identity).toSet())
            .forEach { identity ->
                fetchedContacts.add(Invalid(identity))
            }

        return fetchedContacts
    }

    private fun FetchIdentityResult.toContactModelData(workContact: WorkContact?) = ContactModelData(
        identity = identity,
        publicKey = publicKey.also { fetchedPublicKey ->
            require(workContact == null || fetchedPublicKey.contentEquals(workContact.publicKey)) {
                "Public key mismatch for contact $identity"
            }
        },
        createdAt = Instant.now(),
        lastUpdateAt = null,
        firstName = workContact?.firstName ?: "",
        lastName = workContact?.lastName ?: "",
        nickname = null,
        verificationLevel = workContact.getVerificationLevel(),
        workVerificationLevel = workContact.getWorkVerificationLevel(),
        identityType = getIdentityType(),
        activityState = getActivityState(),
        featureMask = featureMask.toULong(),
        typingIndicatorPolicy = TypingIndicatorPolicy.DEFAULT,
        readReceiptPolicy = ReadReceiptPolicy.DEFAULT,
        syncState = ContactSyncState.INITIAL,
        acquaintanceLevel = AcquaintanceLevel.DIRECT,
        conversationVisibility = ConversationVisibility.NORMAL,
        localAvatarExpires = null,
        androidContactLookupInfo = null,
        profilePictureBlobId = null,
        isRestored = false,
        jobTitle = workContact?.jobTitle,
        department = workContact?.department,
        notificationTriggerPolicyOverride = null,
        availabilityStatus = workContact?.getAvailabilityStatusOrNone() ?: AvailabilityStatus.None,
        workLastFullSyncAt = workContact?.workLastFullSyncAt,
    )

    private fun FetchIdentityResult.getIdentityType() = when (type) {
        0 -> IdentityType.REGULAR
        1 -> IdentityType.WORK
        else -> {
            logger.error("Got unexpected identity type {} for identity {}", type, identity)
            IdentityType.REGULAR
        }
    }

    private fun FetchIdentityResult.getActivityState() = when (state) {
        IdentityState.ACTIVE.value -> IdentityState.ACTIVE
        IdentityState.INACTIVE.value -> IdentityState.INACTIVE
        IdentityState.INVALID.value -> IdentityState.INVALID
        else -> {
            logger.error("Got unexpected activity state {} for identity {}", state, identity)
            IdentityState.ACTIVE
        }
    }

    private fun WorkContact?.getVerificationLevel() = if (this == null) {
        VerificationLevel.UNVERIFIED
    } else {
        VerificationLevel.SERVER_VERIFIED
    }

    private fun WorkContact?.getWorkVerificationLevel() = if (this == null) {
        WorkVerificationLevel.NONE
    } else {
        WorkVerificationLevel.WORK_SUBSCRIPTION_VERIFIED
    }

    private fun WorkContact.getAvailabilityStatusOrNone(): AvailabilityStatus =
        availability
            ?.let(AvailabilityStatus::fromProtocolBase64)
            ?: AvailabilityStatus.None
}
