/**
 * ProjectLaogai
 *
 * Copyright 2019-2020  <seil0@mosad.xyz>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 *
 */

package org.mosad.seil0.projectlaogai.controller

import android.content.Context
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.jsoup.HttpStatusException
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import org.mosad.seil0.projectlaogai.R
import org.mosad.seil0.projectlaogai.controller.preferences.EncryptedPreferences
import org.mosad.seil0.projectlaogai.util.GradeSubject
import java.security.KeyStore
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.util.*
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.TrustManagerFactory
import kotlin.collections.ArrayList
import kotlin.collections.HashMap

/**
 * Parse the qispos site the get all needed data for the grades fragment
 */
class QISPOSParser(val context: Context) {

    private val className = this.javaClass.name
    private val baseURL = "https://notenverwaltung.hs-offenburg.de"
    private val loginPath = "/qispos/rds?state=user&type=1&category=auth.login&startpage=portal.vm&breadCrumbSource=portal"

    /**
     * check if qispos is available
     * @return a http status code, 999 if no HttpStatusException supplied
     */
    fun checkQISPOSStatus(): Int {
        return runBlocking {
            withContext(Dispatchers.IO) {
                val socketFactory = createSSLSocketFactory()

                try {
                    val res = Jsoup.connect(baseURL + loginPath)
                        .sslSocketFactory(socketFactory)
                        .followRedirects(true)
                        .referrer("https://notenverwaltung.hs-offenburg.de/qispos/rds?state=user&type=0")
                        .execute()

                    Log.i(className, "Qispos status is: ${res.statusCode()}")
                    res.statusCode()
                } catch (exHttp: HttpStatusException) {
                    Log.w(className, "Qispos status is: ${exHttp.statusCode}")
                    exHttp.statusCode
                } catch (ex: Exception) {
                    Log.e(className, "Error while checking status", ex)
                    999
                }

            }
        }
    }

    /**
     * parse the html from readGrades()
     * @return a SortedMap, each entry is a semester, each semester has a ArrayList with subjects
     */
    fun parseGrades(): SortedMap<String, ArrayList<GradeSubject>> {
        val gradesMap = HashMap<String, ArrayList<GradeSubject>>()
        val gradesListHtml = readGrades()

        gradesListHtml?.select("table > tbody > tr")?.forEach {
            val row = it.select("td.tabelle1_alignleft,td.tabelle1_aligncenter,td.tabelle1_alignright")

            // only real subjects will be selected
            if(row.size >= 6 && row[0].text().length >=7) {
                val subject = GradeSubject(
                    id = row[0].text(),
                    name = row[1].text(),
                    semester = row[2].text(),
                    grade = if (row[3].text().isNotEmpty()) row[3].text() else row[4].text(),
                    credits = row[5].text()
                )

                if (gradesMap.containsKey(subject.semester)) {
                    gradesMap[subject.semester]!!.add(subject)
                } else {
                    gradesMap[subject.semester] = arrayListOf(subject)
                }
            }

        }

        return gradesMap.toSortedMap(compareBy<String>{
            val oText = it.substringAfter(" ")

            // if WS, substring before / and add 0.5 to compareBy
            if (oText.contains("/")) {
                oText.substringBefore("/").toInt() + 0.5
            } else {
                oText.toDouble()
            }
        }.thenBy { it })
    }

    /**
     * read the grades html from qispos
     * @return the grades list as html element or null
     */
    private fun readGrades(): Element?{

        val credentials = EncryptedPreferences.readCredentials(context)
        val username = credentials.first.substringBefore("@")
        val password = credentials.second

        return runBlocking {
            withContext(Dispatchers.IO) {
                try {
                    val socketFactory = createSSLSocketFactory()

                    // login, asdf = username, fdsa = password, wtf
                    val list = mapOf(
                        Pair("asdf", username),
                        Pair("fdsa", password),
                        Pair("submit", "Anmelden")
                    )

                    // login and get session cookies
                    val res = Jsoup.connect(baseURL + loginPath)
                        .sslSocketFactory(socketFactory)
                        .followRedirects(true)
                        .referrer("https://notenverwaltung.hs-offenburg.de/qispos/rds?state=user&type=0")
                        .data(list)
                        .postDataCharset("UTF-8")
                        .execute()
                    Log.i(className, "Login status is: ${res.statusCode()} (${res.statusMessage()})")

                    val loginCookies = res.cookies()

                    // grades root document and url
                    val rootHtml =Jsoup.parse(res.body())
                    val gradesRootLink = rootHtml.select("li.menueListStyle > a.auflistung").last().attr("abs:href")

                    // parse grades url
                    val gradesHtml = Jsoup.connect(gradesRootLink)
                        .sslSocketFactory(socketFactory)
                        .followRedirects(true)
                        .cookies(loginCookies)
                        .referrer("https://notenverwaltung.hs-offenburg.de/qispos/rds?state=user&type=0")
                        .get()

                    val gradesNextLink = gradesHtml.select("li.treelist > a.regular").attr("abs:href")

                    val gradesNextHtml = Jsoup.connect(gradesNextLink)
                        .sslSocketFactory(socketFactory)
                        .followRedirects(true)
                        .cookies(loginCookies)
                        .referrer("https://notenverwaltung.hs-offenburg.de/qispos/rds?state=user&type=0")
                        .get()

                    val gradesListLink = gradesNextHtml.selectFirst("li.treelist > ul > li").selectFirst("a").attr("abs:href")

                    // get the grades list
                    val gradesListHtml = Jsoup.connect(gradesListLink)
                        .sslSocketFactory(socketFactory)
                        .followRedirects(true)
                        .cookies(loginCookies)
                        .referrer("https://notenverwaltung.hs-offenburg.de/qispos/rds?state=user&type=0")
                        .get()

                    Log.i(className, "Read html length: ${gradesListHtml.body().html().length}")
                    gradesListHtml.body()
                } catch (ex: Exception) {
                    Log.e(className, "Error while loading Qispos", ex)
                    null
                }
            }
        }

    }

    /**
     * since the HS has a fucked up tls setup we need to work around that
     */
    private fun createSSLSocketFactory(): SSLSocketFactory {
        // Load CAs from an InputStream
        // (could be from a resource or ByteArrayInputStream or ...)
        val cf: CertificateFactory = CertificateFactory.getInstance("X.509")
        val caInput = context.resources.openRawResource(R.raw.notenverwaltung_hs_offenburg_de)
        val ca = caInput.use {
            cf.generateCertificate(it) as X509Certificate
        }

        // Create a KeyStore containing our trusted CAs
        val keyStoreType = KeyStore.getDefaultType()
        val keyStore = KeyStore.getInstance(keyStoreType).apply {
            load(null, null)
            setCertificateEntry("ca", ca)
        }

        // Create a TrustManager that trusts the CAs inputStream our KeyStore
        val tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm()
        val tmf = TrustManagerFactory.getInstance(tmfAlgorithm).apply {
            init(keyStore)
        }

        // Create an SSLContext that uses our TrustManager
        val sslContext = SSLContext.getInstance("TLS").apply {
            init(null, tmf.trustManagers, null)
        }

        return sslContext.socketFactory
    }




}