package app.furmeet.events.system.schedule.managers

import android.content.Context
import android.os.FileObserver
import android.util.Log
import app.furmeet.events.system.helper.*
import app.furmeet.events.system.helper.observables.Observable
import app.furmeet.events.system.helper.observables.Observer
import app.furmeet.events.system.notification.NotificationFirer
import app.furmeet.events.system.schedule.abstractDataTypes.convention.*
import app.furmeet.events.system.schedule.abstractDataTypes.dataholder.FakeMap
import app.furmeet.events.system.schedule.abstractDataTypes.managed.AttendeeConFavorites
import app.furmeet.events.system.schedule.abstractDataTypes.managed.AttendeeConsFavorites
import com.google.gson.GsonBuilder
//import org.jetbrains.anko.doAsync
import java.io.File
import java.io.Serializable
import java.util.*

class ScheduleManager(context: Context) : UncomplicatedObservable<Any?>(), Observer<Any?>, Serializable {
    override fun update(observable: Observable<Any?>, args: Any?) = onAttendeeIntentionsChanged()

    private val uniqueIdentifierFile = File(context.filesDir, "rduid.txt")
    val uniqueIdentifier: String

    val cacheManager = CacheManager(context)
    private val cachedChoices = File(context.filesDir, "schedule_choices.json")
    private val cachedChoicesCanonicalPath = cachedChoices.canonicalPath

    private val fireNotificationsClosure = { NotificationFirer().fire(context) }

    private inner class InnerFileObserver(path: String, private val closure: () -> Any?) : FixedFileObserver(path) {
        override fun onEvent(event: Int, path: String) {
            val mask = FileObserver.CLOSE_WRITE
            if ((event and mask) != 0) {
                closure()
            }
        }
    }

    private var diskChangesToNotLoad: Int = 0
    private fun incrementDiskChangesToNotLoad(howMany: Int = 1) {
        synchronized(::diskChangesToNotLoad) {
            diskChangesToNotLoad += howMany
        }
    }

    private fun notifyDiskChanges() {
        synchronized(::diskChangesToNotLoad) {
            if (diskChangesToNotLoad > 0)
                diskChangesToNotLoad -= 1
            else
                loadFromDisk()
        }
        setChanged()
        notifyObservers()
    }

    private val fileObserver: InnerFileObserver
    private val scheduleObserver: InnerFileObserver

    val conventionTime get() = GregorianCalendar().apply { timeInMillis = System.currentTimeMillis() } // GregorianCalendar(2022,6,15,35,27)

    private val errorRegistrationTier = RegistrationTier(0, FakeMap("Error"), listOf())

    private val errorConventionEdition = Convention(
            listOf(),
            listOf(),
            listOf(errorRegistrationTier),
            errorRegistrationTier,
            listOf(),
            "",
            "Download error",
            "Download error",
            FakeMap("please close and re-open the app"),
            FakeMap(""),
            listOf(),
            conventionTime.dayRange,
            conventionTime,
            conventionTime.also { it.timeInMillis += 1000 },
            0,
            0,
            listOf(conventionTime.dayRange.start),
            24,
            SimpleTimeZone(0, "utc"),
            listOf(),
            0,
            false,
            true,
            listOf(),
            listOf(),
            null,
            null,
            null,
            null,
            null,
    )

    private val errorConventionSeries = ConventionSeries(
            FakeMap("Download error"),
            errorConventionEdition,
            listOf(errorConventionEdition),
            SimpleTimeZone(0, "utc"),
            listOf(),
            Language.EN,
            FakeMap(""),
            null,
            FakeMap(""),
            listOf(),
            1.0,
            listOf(),
            null,
            false
    )

    private fun updateConventionSeries() =
            cacheManager[RemoteAssetsHolder.remoteAssets.json.lastPathPart()]?.first
                    ?.let { nullAndLogIfException { ConventionJsonReader.readFromInputStream(it) } }
                    ?.also { conventionSeries = it }

    private fun safeConventionSeries() =
            cacheManager.fallbackGet(RemoteAssetsHolder.remoteAssets.json.lastPathPart())?.first
                    ?.let { nullAndLogIfException { ConventionJsonReader.readFromInputStream(it) } }
                    ?.also { conventionSeries = it }

    var conventionSeries: ConventionSeries = null
            ?: updateConventionSeries()
            ?: safeConventionSeries()
            ?: errorConventionSeries
    val convention get() = conventionSeries.featured
    val attendeeConventionsFavorites = AttendeeConsFavorites()
    val attendeeFavorites
        get(): AttendeeConFavorites {
            lateinit var favorites: AttendeeConFavorites
            synchronized(::attendeeConventionsFavorites) {
                favorites = attendeeConventionsFavorites[convention]
            }
            return favorites
        }

    val startupTerms get() = conventionSeries.startupTerms?.get(conventionSeries.language)
    private val lastAgreedStartupTermsFile = File(context.filesDir, "last_agreed_startup_terms.html")
    val needsToAcceptStartupTerms: Boolean
        get() = when {
            startupTerms == null -> false                                  // SKIP if there is no startup terms
            lastAgreedStartupTermsFile.exists().not() -> true              // ASK  if there was no consent
            else -> startupTerms != lastAgreedStartupTermsFile.readText()  // ASK  if policy has changed
        }

    fun acceptStartupTerms() {
        startupTerms?.let {
            lastAgreedStartupTermsFile.writeText(it)
        }
    }

    private val jsonSerializer
        get() = GsonBuilder()
                //.setExclusionStrategies(ObservableFieldsExclusionStrategy())
                .registerTypeAdapter(Convention::class.java, ConventionInstanceCreator(convention))
                .create()

    init {
        loadFromDisk()
        if (!cachedChoices.exists())
            saveToDisk(false)
        uniqueIdentifier = initUniqueIdentifierFile()
        fileObserver = (InnerFileObserver(cachedChoicesCanonicalPath) { notifyDiskChanges() }).apply { startWatching() }
        scheduleObserver = (InnerFileObserver(cacheManager.getFileFor(RemoteAssetsHolder.remoteAssets.json).absolutePath) { notifyScheduleChanged() }).apply { startWatching() }
    }

    private fun notifyScheduleChanged() {
        conventionSeries = updateConventionSeries()
                ?: safeConventionSeries()
                        ?: errorConventionSeries
        setChanged()
        notifyObservers()
        fireNotificationsClosure()
    }


    private val readAnnouncementsFile = File(context.filesDir, "announcements_read.txt")

    var readAnnouncements =
            if (readAnnouncementsFile.exists())
                readAnnouncementsFile.readLines()
            else
                conventionSeries.announcements.sortedBy { it.modified.timeInMillis }.map { it.uuid }

    val unreadAnnouncements: List<Announcement>
        get() =
            conventionSeries.announcements.filter {
                it.uuid !in readAnnouncements
            }.sortedByDescending { it.modified.timeInMillis }

    fun setAnnouncementsRead() {
        readAnnouncements = conventionSeries.announcements.sortedBy { it.modified.timeInMillis }.map { it.uuid }
        readAnnouncementsFile.writeText(readAnnouncements.joinToString(separator = "\n"))
    }


    private fun initUniqueIdentifierFile(): String {
        if (!uniqueIdentifierFile.exists())
            uniqueIdentifierFile.writeText(UUID.randomUUID().toString())
        return uniqueIdentifierFile.readText()
    }

    private fun readObject(ois: java.io.ObjectInputStream) {
        ois.defaultReadObject()
        fileObserver.stopWatching()
        fileObserver.startWatching()
        scheduleObserver.stopWatching()
        scheduleObserver.startWatching()
        loadFromDisk()
    }

    private fun loadFromDisk() {
        if (cachedChoices.exists()) {
            synchronized(::attendeeConventionsFavorites) {
                val json = cachedChoices.readText()
                val stateFromDisk = try {
                    jsonSerializer.fromJson(json, attendeeConventionsFavorites::class.java)
                } catch (e: Throwable) {
                    logException(e)
                    Log.e("Schedule manager", "Malformed JSON", e)
                    AttendeeConsFavorites()
                }
                if (stateFromDisk != null) {
                    stateFromDisk.recursivelyRemoveObservers()
                    attendeeConventionsFavorites.recursivelyRemoveObservers()
                    attendeeConventionsFavorites.clear()
                    attendeeConventionsFavorites.putAll(stateFromDisk)
                    attendeeConventionsFavorites.observeAllChildren()
                    attendeeConventionsFavorites.addObserver(this)
                }
            }
        }
    }


    private fun saveToDisk(async: Boolean = true) {
        synchronized(::attendeeConventionsFavorites) {
            attendeeConventionsFavorites.recursivelyRemoveObservers()
            val json = jsonSerializer.toJson(attendeeConventionsFavorites)
            attendeeConventionsFavorites.observeAllChildren()
            attendeeConventionsFavorites.addObserver(this)
            incrementDiskChangesToNotLoad()
            val closure = { cachedChoices.bufferedWriter().use { it.write(json) } }
            if (async)
                doAsync { closure() }
            else
                closure()
        }
    }

    fun onAttendeeIntentionsChanged() {
        saveToDisk()
    }

    fun clearIntentions() {
        attendeeConventionsFavorites.recursivelyRemoveObservers()
        attendeeConventionsFavorites.clear()
        attendeeConventionsFavorites.observeAllChildren()
        attendeeConventionsFavorites.addObserver(this)
        saveToDisk()
    }

    val currentSplashAsset: String
        get() = (
                conventionSeries.banners.firstOrNull { conventionTime in it.lifetime }?.banner
                        ?: conventionSeries.defaultBanner
                ).solved!!

    val editionHasStarted: Boolean get() = conventionTime.timeInMillis > convention.officialLifespan.start.timeInMillis
    val editionHasEnded: Boolean get() = conventionTime.timeInMillis > convention.officialLifespan.finish.timeInMillis

    val nextEditionStartTime: Long?
        get() {
            val currentTime = conventionTime.timeInMillis
            val timeUntilThis = convention.officialLifespan.start.timeInMillis - currentTime
            val timeUntilNext = (convention.nextEdition?.timeInMillis
                    ?: Long.MAX_VALUE) - currentTime
            val stillHappening = editionHasStarted and !editionHasEnded
            return when {
                timeUntilThis > 0 -> convention.officialLifespan.start.timeInMillis
                stillHappening -> null
                timeUntilNext > 0 -> convention.nextEdition?.timeInMillis
                else -> null
            }
        }

    val nextEditionStartCountdown: Triple<Int, Int, Int>?
        get() = nextEditionStartTime?.let { buildDayHourMinuteCountdownTriple(it - conventionTime.timeInMillis) }

    private fun buildDayHourMinuteCountdownTriple(millisUntil: Long): Triple<Int, Int, Int> {
        return (millisUntil / 1000).let {
            Triple((it / (3600 * 24)).toInt(), ((it / 3600) % 24).toInt(), ((it / 60) % 60).toInt())
        }
    }

    fun canAttend(event: Event) = attendeeFavorites.canAttend(event)

    var hasFinished = false

    fun finish() {
        hasFinished = true
        fileObserver.stopWatching()
        scheduleObserver.stopWatching()
        attendeeConventionsFavorites.recursivelyRemoveObservers()
    }
}
