package com.readrops.api.localfeed

import android.accounts.NetworkErrorException
import androidx.annotation.WorkerThread
import com.readrops.api.localfeed.json.JSONFeedAdapter
import com.readrops.api.localfeed.json.JSONItemsAdapter
import com.readrops.api.utils.ApiUtils
import com.readrops.api.utils.AuthInterceptor
import com.readrops.api.utils.exceptions.ParseException
import com.readrops.api.utils.exceptions.UnknownFormatException
import com.readrops.db.entities.Feed
import com.readrops.db.entities.Item
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okio.Buffer
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import java.io.ByteArrayInputStream
import java.io.IOException
import java.io.InputStream
import java.net.HttpURLConnection

class LocalRSSDataSource(private val httpClient: OkHttpClient): KoinComponent {

    /**
     * Query RSS url
     * @param url url to query
     * @param headers request headers
     * @return a Feed object with its items
     */
    @Throws(ParseException::class, UnknownFormatException::class, NetworkErrorException::class, IOException::class)
    @WorkerThread
    fun queryRSSResource(url: String, headers: Headers?): Pair<Feed, List<Item>>? {
        get<AuthInterceptor>().credentials = null
        val response = queryUrl(url, headers)

        return when {
            response.isSuccessful -> {
                val header = response.header(ApiUtils.CONTENT_TYPE_HEADER)
                        ?: throw UnknownFormatException("Unable to get $url content-type")

                val contentType = ApiUtils.parseContentType(header)
                        ?: throw ParseException("Unable to parse $url content-type")

                var type = LocalRSSHelper.getRSSType(contentType)

                val bodyArray = response.peekBody(Long.MAX_VALUE).bytes()

                // if we can't guess type based on content-type header, we use the content
                if (type == LocalRSSHelper.RSSType.UNKNOWN)
                    type = LocalRSSHelper.getRSSContentType(ByteArrayInputStream(bodyArray))
                // if we can't guess type even with the content, we are unable to go further
                if (type == LocalRSSHelper.RSSType.UNKNOWN) throw UnknownFormatException("Unable to guess $url RSS type")

                val feed = parseFeed(ByteArrayInputStream(bodyArray), type, response)
                val items = parseItems(ByteArrayInputStream(bodyArray), type)

                response.body?.close()
                Pair(feed, items)
            }
            response.code == HttpURLConnection.HTTP_NOT_MODIFIED -> null
            else -> throw NetworkErrorException("$url returned ${response.code} code : ${response.message}")
        }
    }

    /**
     * Checks if the provided url is a RSS resource
     * @param url url to check
     * @return true if [url] is a RSS resource, false otherwise
     */
    @WorkerThread
    fun isUrlRSSResource(url: String): Boolean {
        val response = queryUrl(url, null)

        return if (response.isSuccessful) {
            val header = response.header(ApiUtils.CONTENT_TYPE_HEADER)
                    ?: return false

            val contentType = ApiUtils.parseContentType(header)
                    ?: return false

            var type = LocalRSSHelper.getRSSType(contentType)

            if (type == LocalRSSHelper.RSSType.UNKNOWN)
                type = LocalRSSHelper.getRSSContentType(response.body?.byteStream()!!) // stream is closed in helper method

            type != LocalRSSHelper.RSSType.UNKNOWN
        } else false
    }

    @Throws(IOException::class)
    private fun queryUrl(url: String, headers: Headers?): Response {
        val requestBuilder = Request.Builder().url(url)
        headers?.let { requestBuilder.headers(it) }

        return httpClient.newCall(requestBuilder.build()).execute()
    }

    private fun parseFeed(stream: InputStream, type: LocalRSSHelper.RSSType, response: Response): Feed {
        val feed = if (type != LocalRSSHelper.RSSType.JSONFEED) {
            val adapter = XmlAdapter.xmlFeedAdapterFactory(type)

            adapter.fromXml(stream)
        } else {
            val adapter = Moshi.Builder()
                    .add(JSONFeedAdapter())
                    .build()
                    .adapter(Feed::class.java)

            adapter.fromJson(Buffer().readFrom(stream))!!
        }

        handleSpecialCases(feed, type, response)

        feed.etag = response.header(ApiUtils.ETAG_HEADER)
        feed.lastModified = response.header(ApiUtils.LAST_MODIFIED_HEADER)

        return feed
    }

    private fun parseItems(stream: InputStream, type: LocalRSSHelper.RSSType): List<Item> {
        return if (type != LocalRSSHelper.RSSType.JSONFEED) {
            val adapter = XmlAdapter.xmlItemsAdapterFactory(type)

            adapter.fromXml(stream)
        } else {
            val adapter = Moshi.Builder()
                    .add(Types.newParameterizedType(MutableList::class.java, Item::class.java), JSONItemsAdapter())
                    .build()
                    .adapter<List<Item>>(Types.newParameterizedType(MutableList::class.java, Item::class.java))

            adapter.fromJson(Buffer().readFrom(stream))!!
        }
    }

    private fun handleSpecialCases(feed: Feed, type: LocalRSSHelper.RSSType, response: Response) {
        with(feed) {
            if (type == LocalRSSHelper.RSSType.RSS_2) {
                // if an atom:link element was parsed, we still replace its value as it is unreliable,
                // otherwise we just add the rss url
                url = response.request.url.toString()
            } else if (type == LocalRSSHelper.RSSType.ATOM || type == LocalRSSHelper.RSSType.RSS_1) {
                if (url == null) url = response.request.url.toString()
                if (siteUrl == null) siteUrl = response.request.url.scheme + "://" + response.request.url.host
            }
        }
    }
}