/*
 * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
 */

package at.bitfire.davdroid.sync

import android.accounts.Account
import android.text.format.Formatter
import androidx.annotation.OpenForTesting
import at.bitfire.dav4jvm.ktor.DavCalendar
import at.bitfire.dav4jvm.ktor.MultiStatusItem
import at.bitfire.dav4jvm.ktor.exception.DavException
import at.bitfire.dav4jvm.ktor.responses
import at.bitfire.dav4jvm.ktor.selfResponse
import at.bitfire.dav4jvm.property.caldav.CalDAV
import at.bitfire.dav4jvm.property.caldav.CalendarData
import at.bitfire.dav4jvm.property.caldav.MaxResourceSize
import at.bitfire.dav4jvm.property.caldav.ScheduleTag
import at.bitfire.dav4jvm.property.webdav.GetETag
import at.bitfire.dav4jvm.property.webdav.WebDAV
import at.bitfire.davdroid.ProductIds
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.di.qualifier.IoDispatcher
import at.bitfire.davdroid.di.qualifier.SyncTransferSemaphore
import at.bitfire.davdroid.resource.LocalJtxCollection
import at.bitfire.davdroid.resource.LocalJtxObject
import at.bitfire.davdroid.resource.LocalResource
import at.bitfire.davdroid.util.DavUtils
import at.bitfire.davdroid.util.DavUtils.lastSegment
import at.bitfire.synctools.exception.InvalidResourceException
import at.bitfire.synctools.icalendar.CalendarUidSplitter
import at.bitfire.synctools.icalendar.ICalendarGenerator
import at.bitfire.synctools.icalendar.ICalendarParser
import at.bitfire.synctools.mapping.jtx.JtxObjectBuilder
import at.bitfire.synctools.mapping.jtx.JtxObjectHandler
import at.bitfire.synctools.mapping.jtx.handler.AndroidAttachmentFetcher
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.ktor.client.HttpClient
import io.ktor.http.Url
import io.ktor.http.content.TextContent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.sync.Semaphore
import net.fortuna.ical4j.model.Component
import net.fortuna.ical4j.model.component.CalendarComponent
import net.fortuna.ical4j.model.property.ProdId
import java.io.Reader
import java.io.StringReader
import java.io.StringWriter
import java.util.logging.Level

class JtxSyncManager @AssistedInject constructor(
    @Assisted account: Account,
    @Assisted httpClient: HttpClient,
    @Assisted syncResult: SyncResult,
    @Assisted localCollection: LocalJtxCollection,
    @Assisted collection: Collection,
    @Assisted resync: ResyncType?,
    @IoDispatcher ioDispatcher: CoroutineDispatcher,
    private val productIds: ProductIds,
    @SyncTransferSemaphore syncTransferSemaphore: Semaphore
): SyncManager<LocalJtxObject, LocalJtxCollection, DavCalendar>(
    account,
    httpClient,
    SyncDataType.TASKS,
    syncResult,
    localCollection,
    collection,
    resync,
    ioDispatcher,
    syncTransferSemaphore
) {

    @AssistedFactory
    interface Factory {
        fun jtxSyncManager(
            account: Account,
            httpClient: HttpClient,
            syncResult: SyncResult,
            localCollection: LocalJtxCollection,
            collection: Collection,
            resync: ResyncType?
        ): JtxSyncManager
    }


    override suspend fun prepare(): Boolean {
        davCollection = DavCalendar(httpClient, collection.url)

        return true
    }

    override suspend fun queryCapabilities() =
        SyncException.wrapWithRemoteResource(collection.url) {
            val response =
                davCollection.propfind(0, CalDAV.GetCTag, CalDAV.MaxResourceSize, WebDAV.SyncToken).selfResponse()
                    ?: return@wrapWithRemoteResource null

            response[MaxResourceSize::class.java]?.maxSize?.let { maxSize ->
                logger.info("Collection accepts resources up to ${Formatter.formatFileSize(context, maxSize)}")
            }

            syncState(response)
        }

    override fun generateUpload(resource: LocalJtxObject): GeneratedResource {
        val localJtxObject = resource.jtxObjectAndExceptions
        logger.log(Level.FINE, "Preparing upload of icalobject #{0}: {1}", arrayOf(resource.id, localJtxObject))

        // Map jtx object to iCalendar (also generates UID, if necessary)
        val handler = JtxObjectHandler(
            prodId = ProdId(productIds.iCalProdId),
            attachmentFetcher = AndroidAttachmentFetcher(
                client = resource.collection.client,
                account = resource.collection.account
            )
        )
        val mappedJtxObjects = handler.mapToCalendarComponents(localJtxObject)

        // Persist UID if it was generated
        if (mappedJtxObjects.generatedUid) {
            resource.updateUid(mappedJtxObjects.uid)
        }

        // generate iCalendar and convert to request body
        val iCalWriter = StringWriter()
        ICalendarGenerator().write(mappedJtxObjects.associatedComponents, iCalWriter)
        val outgoingContent = TextContent(
            text = iCalWriter.toString(),
            contentType = DavCalendar.MIME_ICALENDAR_UTF8
        )

        return GeneratedResource(
            suggestedFileName = DavUtils.fileNameFromUid(mappedJtxObjects.uid, "ics"),
            content = outgoingContent
        )
    }

    override fun syncAlgorithm() = SyncAlgorithm.PROPFIND_REPORT

    override fun listAllRemote(): Flow<MultiStatusItem> = flow {
        SyncException.wrapWithRemoteResource(collection.url) {
            if (localCollection.supportsVTODO) {
                logger.info("Querying tasks")
                emitAll(davCollection.calendarQuery("VTODO", null, null))
            }

            if (localCollection.supportsVJOURNAL) {
                logger.info("Querying journals")
                emitAll(davCollection.calendarQuery("VJOURNAL", null, null))
            }
        }
    }

    override suspend fun downloadRemote(bunch: List<Url>) {
        logger.info("Downloading ${bunch.size} iCalendars: $bunch")
        // multiple iCalendars, use calendar-multi-get
        SyncException.wrapWithRemoteResource(collection.url) {
            davCollection.multiget(bunch).responses().collect { response ->
                // See CalendarSyncManager for more information about the multi-get response
                SyncException.wrapWithRemoteResource(response.href) wrapResource@{
                    if (!response.isSuccess()) {
                        logger.warning("Ignoring non-successful multi-get response for ${response.href}")
                        return@wrapResource
                    }

                    val iCal = response[CalendarData::class.java]?.iCalendar
                    if (iCal == null) {
                        logger.warning("Ignoring multi-get response without calendar-data")
                        return@wrapResource
                    }

                    val eTag = response[GetETag::class.java]?.eTag
                        ?: throw DavException("Received multi-get response without ETag")
                    val scheduleTag = response[ScheduleTag::class.java]?.scheduleTag
                    val fileName = response.href.lastSegment

                    try {
                        processICalObject(fileName, eTag, scheduleTag, StringReader(iCal))
                    } catch (e: InvalidResourceException) {
                        logger.log(Level.WARNING, "Error while processing jtx object", e)
                        notifyInvalidResource(e, fileName)
                    }
                }
            }
        }
    }

    override suspend fun postProcess() {
        localCollection.updateLastSync()
    }

    override fun notifyInvalidResourceTitle(): String =
        context.getString(R.string.sync_invalid_event)


    @OpenForTesting
    internal suspend fun processICalObject(fileName: String, eTag: String, scheduleTag: String?, reader: Reader) {
        val calendar = ICalendarParser().parse(reader)

        val uidsAndJournals = CalendarUidSplitter<CalendarComponent>().associateByUid(calendar, Component.VJOURNAL)
        val uidsAndTasks = CalendarUidSplitter<CalendarComponent>().associateByUid(calendar, Component.VTODO)

        if (uidsAndJournals.size + uidsAndTasks.size != 1) {
            logger.warning("Received iCalendar with not exactly one UID; ignoring $fileName")
            return
        }

        val uidsAndComponents = uidsAndJournals.ifEmpty { uidsAndTasks }
        val component = uidsAndComponents.values.first()

        val jtxEntityAndExceptions = JtxObjectBuilder(
            collectionId = localCollection.jtxCollection.id,
            fileName = fileName,
            eTag = eTag,
            scheduleTag = scheduleTag,
            flags = LocalResource.FLAG_REMOTELY_PRESENT
        ).build(component)

        val local = localCollection.findByName(fileName)
        if (local != null) {
            SyncException.wrapWithLocalResource(local) {
                logger.info("Updating $fileName in local jtx collection: $component")
                local.update(jtxEntityAndExceptions)
            }
        } else {
            logger.info("Adding $fileName to local jtx collection: $component")
            localCollection.add(jtxEntityAndExceptions)
        }
    }
}
