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

package at.bitfire.davdroid.servicedetection

import at.bitfire.dav4jvm.Property
import at.bitfire.dav4jvm.ktor.DavResource
import at.bitfire.dav4jvm.ktor.exception.HttpException
import at.bitfire.dav4jvm.ktor.parent
import at.bitfire.dav4jvm.ktor.resolve
import at.bitfire.dav4jvm.ktor.responses
import at.bitfire.dav4jvm.ktor.withTrailingSlash
import at.bitfire.dav4jvm.property.caldav.CalDAV
import at.bitfire.dav4jvm.property.caldav.CalendarHomeSet
import at.bitfire.dav4jvm.property.caldav.CalendarProxyReadFor
import at.bitfire.dav4jvm.property.caldav.CalendarProxyWriteFor
import at.bitfire.dav4jvm.property.carddav.AddressbookHomeSet
import at.bitfire.dav4jvm.property.carddav.CardDAV
import at.bitfire.dav4jvm.property.common.HrefListProperty
import at.bitfire.dav4jvm.property.webdav.GroupMembership
import at.bitfire.dav4jvm.property.webdav.ResourceType
import at.bitfire.dav4jvm.property.webdav.WebDAV
import at.bitfire.davdroid.db.HomeSet
import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.repository.DavHomeSetRepository
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.ktor.client.HttpClient
import io.ktor.http.Url
import java.util.logging.Level
import java.util.logging.Logger
import javax.annotation.WillNotClose

/**
 * ServiceRefresher is used to discover and save home sets of a given service.
 */
class ServiceRefresher @AssistedInject constructor(
    @Assisted private val service: Service,
    @Assisted @WillNotClose private val httpClient: HttpClient,
    private val logger: Logger,
    private val homeSetRepository: DavHomeSetRepository
) {

    @AssistedFactory
    interface Factory {
        fun create(service: Service, httpClient: HttpClient): ServiceRefresher
    }

    /**
     * Home-set class to use depending on the given service type.
     */
    private val homeSetClass: Class<out HrefListProperty> =
        when (service.type) {
            Service.TYPE_CARDDAV -> AddressbookHomeSet::class.java
            Service.TYPE_CALDAV -> CalendarHomeSet::class.java
            else -> throw IllegalArgumentException()
        }

    /**
     * Home-set properties to ask for in a PROPFIND request to the principal URL,
     * depending on the given service type.
     */
    private val homeSetProperties: Array<Property.Name> =
        arrayOf(                        // generic WebDAV properties
            WebDAV.DisplayName,
            WebDAV.GroupMembership,
            WebDAV.ResourceType
        ) + when (service.type) {       // service-specific CalDAV/CardDAV properties
            Service.TYPE_CARDDAV -> arrayOf(
                CardDAV.AddressbookHomeSet,
            )

            Service.TYPE_CALDAV -> arrayOf(
                CalDAV.CalendarHomeSet,
                CalDAV.CalendarProxyReadFor,
                CalDAV.CalendarProxyWriteFor
            )

            else -> throw IllegalArgumentException()
        }

    /**
     * Starting at given principal URL, tries to recursively find and save all user relevant home sets.
     *
     * @param principalUrl              URL of principal to query (user-provided principal or current-user-principal)
     * @param level                     Current recursion level (limited to 0, 1 or 2):
     *   - 0: We assume found home sets belong to the current-user-principal
     *   - 1 or 2: We assume found home sets don't directly belong to the current-user-principal
     * @param alreadyQueriedPrincipals  The HttpUrls of principals which have been queried already, to avoid querying principals more than once.
     * @param alreadySavedHomeSets      The HttpUrls of home sets which have been saved to database already, to avoid saving home sets
     * more than once, which could overwrite the already set "personal" flag with `false`.
     *
     * @throws java.io.IOException                          on I/O errors
     * @throws HttpException                                on HTTP errors
     * @throws at.bitfire.dav4jvm.exception.DavException    on application-level or logical errors
     */
    internal suspend fun discoverHomesets(
        principalUrl: Url,
        level: Int = 0,
        alreadyQueriedPrincipals: MutableSet<Url> = mutableSetOf(),
        alreadySavedHomeSets: MutableSet<Url> = mutableSetOf()
    ) {
        logger.fine("Discovering homesets of $principalUrl")
        val relatedResources = mutableSetOf<Url>()

        // Query the URL
        val principal = DavResource(httpClient, principalUrl)
        val personal = level == 0
        try {
            principal.propfind(0, *homeSetProperties).responses().collect { davResponse ->
                alreadyQueriedPrincipals += davResponse.href

                // If response holds home sets, save them
                davResponse[homeSetClass]?.let { homeSets ->
                    for (homeSetHref in homeSets.hrefs)
                        principal.location.resolve(homeSetHref)?.let { homesetUrl ->
                            val resolvedHomeSetUrl = homesetUrl.withTrailingSlash()
                            if (!alreadySavedHomeSets.contains(resolvedHomeSetUrl)) {
                                homeSetRepository.insertOrUpdateByUrlBlocking(
                                    // HomeSet is considered personal if this is the outer recursion call,
                                    // This is because we assume the first call to query the current-user-principal
                                    // Note: This is not be be confused with the DAV:owner attribute. Home sets can be owned by
                                    // other principals while still being considered "personal" (belonging to the current-user-principal)
                                    // and an owned home set need not always be personal either.
                                    HomeSet(0, service.id, personal, resolvedHomeSetUrl)
                                )
                                alreadySavedHomeSets += resolvedHomeSetUrl
                            }
                        }
                }

                // Add related principals to be queried afterward
                if (personal) {
                    val relatedResourcesTypes = listOf(
                        // current resource is a read/write-proxy for other principals
                        CalendarProxyReadFor::class.java,
                        CalendarProxyWriteFor::class.java,
                        // current resource is a member of a group (principal that can also have proxies)
                        GroupMembership::class.java
                    )
                    for (type in relatedResourcesTypes)
                        davResponse[type]?.let {
                            for (href in it.hrefs)
                                principal.location.resolve(href)?.let { url ->
                                    relatedResources += url
                                }
                        }
                }

                // If current resource is a calendar-proxy-read/write, it's likely that its parent is a principal, too.
                davResponse[ResourceType::class.java]?.let { resourceType ->
                    val proxyProperties = arrayOf(
                        CalDAV.CalendarProxyRead,
                        CalDAV.CalendarProxyWrite
                    )
                    if (proxyProperties.any { resourceType.types.contains(it) })
                        relatedResources += davResponse.href.parent()
                }
            }

        } catch (e: HttpException) {
            if (e.isClientError)
                logger.log(Level.INFO, "Ignoring Client Error 4xx while looking for ${service.type} home sets", e)
            else
                throw e
        }

        // query related resources
        if (level <= 1)
            for (resource in relatedResources)
                if (alreadyQueriedPrincipals.contains(resource))
                    logger.warning("$resource already queried, skipping")
                else
                    discoverHomesets(
                        principalUrl = resource,
                        level = level + 1,
                        alreadyQueriedPrincipals = alreadyQueriedPrincipals,
                        alreadySavedHomeSets = alreadySavedHomeSets
                    )
    }

}
