package com.vitorpamplona.amethyst.model

import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.actions.updated
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
import com.vitorpamplona.quartz.encoders.Nip19
import com.vitorpamplona.quartz.encoders.toNote
import com.vitorpamplona.quartz.events.*
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.coroutines.Dispatchers
import java.math.BigDecimal
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter

@Stable
class AddressableNote(val address: ATag) : Note(address.toTag()) {
    override fun idNote() = address.toNAddr()
    override fun toNEvent() = address.toNAddr()
    override fun idDisplayNote() = idNote().toShortenHex()
    override fun address() = address
    override fun createdAt(): Long? {
        if (event == null) return null

        val publishedAt = (event as? LongTextNoteEvent)?.publishedAt() ?: Long.MAX_VALUE
        val lastCreatedAt = event?.createdAt() ?: Long.MAX_VALUE

        return minOf(publishedAt, lastCreatedAt)
    }
}

@Stable
open class Note(val idHex: String) {
    // These fields are only available after the Text Note event is received.
    // They are immutable after that.
    var event: EventInterface? = null
    var author: User? = null
    var replyTo: List<Note>? = null

    // These fields are updated every time an event related to this note is received.
    var replies = listOf<Note>()
        private set
    var reactions = mapOf<String, List<Note>>()
        private set
    var boosts = listOf<Note>()
        private set
    var reports = mapOf<User, List<Note>>()
        private set
    var zaps = mapOf<Note, Note?>()
        private set
    var zapPayments = mapOf<Note, Note?>()
        private set

    var relays = listOf<String>()
        private set

    var lastReactionsDownloadTime: Map<String, EOSETime> = emptyMap()

    fun id() = Hex.decode(idHex)
    open fun idNote() = id().toNote()

    open fun toNEvent(): String {
        val myEvent = event
        return if (myEvent is WrappedEvent) {
            val host = myEvent.host
            if (host != null) {
                Nip19.createNEvent(
                    host.id,
                    host.pubKey,
                    host.kind(),
                    relays.firstOrNull()
                )
            } else {
                Nip19.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull())
            }
        } else {
            Nip19.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull())
        }
    }

    fun toNostrUri(): String {
        return "nostr:${toNEvent()}"
    }

    open fun idDisplayNote() = idNote().toShortenHex()

    fun channelHex(): HexKey? {
        return if (event is ChannelMessageEvent ||
            event is ChannelMetadataEvent ||
            event is ChannelCreateEvent ||
            event is LiveActivitiesChatMessageEvent ||
            event is LiveActivitiesEvent
        ) {
            (event as? ChannelMessageEvent)?.channel()
                ?: (event as? ChannelMetadataEvent)?.channel()
                ?: (event as? ChannelCreateEvent)?.id
                ?: (event as? LiveActivitiesChatMessageEvent)?.activity()?.toTag()
                ?: (event as? LiveActivitiesEvent)?.address()?.toTag()
        } else {
            null
        }
    }

    open fun address(): ATag? = null

    open fun createdAt() = event?.createdAt()

    fun loadEvent(event: Event, author: User, replyTo: List<Note>) {
        if (this.event?.id() != event.id()) {
            this.event = event
            this.author = author
            this.replyTo = replyTo

            liveSet?.metadata?.invalidateData()
        }
    }

    fun formattedDateTime(timestamp: Long): String {
        return Instant.ofEpochSecond(timestamp).atZone(ZoneId.systemDefault())
            .format(DateTimeFormatter.ofPattern("uuuu-MM-dd-HH:mm:ss"))
    }

    /**
     * This method caches signatures during each execution to avoid recalculation in longer threads
     */
    fun replyLevelSignature(
        eventsToConsider: Set<Note>,
        cachedSignatures: MutableMap<Note, String>
    ): String {
        val replyTo = replyTo
        if (event is RepostEvent || event is GenericRepostEvent || replyTo == null || replyTo.isEmpty()) {
            return "/" + formattedDateTime(createdAt() ?: 0) + ";"
        }

        val mySignature = (
            replyTo
                .filter { it in eventsToConsider } // This forces the signature to be based on a branch, avoiding two roots
                .map {
                    cachedSignatures[it] ?: it.replyLevelSignature(eventsToConsider, cachedSignatures).apply { cachedSignatures.put(it, this) }
                }
                .maxByOrNull { it.length }?.removeSuffix(";") ?: ""
            ) + "/" + formattedDateTime(createdAt() ?: 0) + ";"

        cachedSignatures[this] = mySignature
        return mySignature
    }

    fun replyLevel(cachedLevels: MutableMap<Note, Int> = mutableMapOf()): Int {
        val replyTo = replyTo
        if (event is RepostEvent || event is GenericRepostEvent || replyTo == null || replyTo.isEmpty()) {
            return 0
        }

        return replyTo.maxOf {
            cachedLevels[it] ?: it.replyLevel(cachedLevels).apply { cachedLevels.put(it, this) }
        } + 1
    }

    fun addReply(note: Note) {
        if (note !in replies) {
            replies = replies + note
            liveSet?.replies?.invalidateData()
        }
    }

    fun removeReply(note: Note) {
        if (note in replies) {
            replies = replies - note
            liveSet?.replies?.invalidateData()
        }
    }

    fun removeBoost(note: Note) {
        if (note in boosts) {
            boosts = boosts - note
            liveSet?.boosts?.invalidateData()
        }
    }

    fun removeAllChildNotes(): List<Note> {
        val toBeRemoved = replies +
            reactions.values.flatten() +
            boosts +
            reports.values.flatten() +
            zaps.keys +
            zaps.values.filterNotNull() +
            zapPayments.keys +
            zapPayments.values.filterNotNull()

        replies = listOf<Note>()
        reactions = mapOf<String, List<Note>>()
        boosts = listOf<Note>()
        reports = mapOf<User, List<Note>>()
        zaps = mapOf<Note, Note?>()
        zapPayments = mapOf<Note, Note?>()
        relays = listOf<String>()
        lastReactionsDownloadTime = emptyMap()

        liveSet?.replies?.invalidateData()
        liveSet?.reactions?.invalidateData()
        liveSet?.boosts?.invalidateData()
        liveSet?.reports?.invalidateData()
        liveSet?.zaps?.invalidateData()

        return toBeRemoved
    }

    fun removeReaction(note: Note) {
        val tags = note.event?.tags() ?: emptyList()
        val reaction = note.event?.content()?.firstFullCharOrEmoji(ImmutableListOfLists(tags)) ?: "+"

        if (reaction in reactions.keys && reactions[reaction]?.contains(note) == true) {
            reactions[reaction]?.let {
                if (note in it) {
                    val newList = it.minus(note)
                    if (newList.isEmpty()) {
                        reactions = reactions.minus(reaction)
                    } else {
                        reactions = reactions + Pair(reaction, newList)
                    }

                    liveSet?.reactions?.invalidateData()
                }
            }
        }
    }

    fun removeReport(deleteNote: Note) {
        val author = deleteNote.author ?: return

        if (author in reports.keys && reports[author]?.contains(deleteNote) == true) {
            reports[author]?.let {
                reports = reports + Pair(author, it.minus(deleteNote))
                liveSet?.reports?.invalidateData()
            }
        }
    }

    fun removeZap(note: Note) {
        if (zaps[note] != null) {
            zaps = zaps.minus(note)
            liveSet?.zaps?.invalidateData()
        } else if (zaps.containsValue(note)) {
            val toRemove = zaps.filterValues { it == note }
            zaps = zaps.minus(toRemove.keys)
            liveSet?.zaps?.invalidateData()
        }
    }

    fun removeZapPayment(note: Note) {
        if (zapPayments[note] != null) {
            zapPayments = zapPayments.minus(note)
            liveSet?.zaps?.invalidateData()
        } else if (zapPayments.containsValue(note)) {
            val toRemove = zapPayments.filterValues { it == note }
            zapPayments = zapPayments.minus(toRemove.keys)
            liveSet?.zaps?.invalidateData()
        }
    }

    fun addBoost(note: Note) {
        if (note !in boosts) {
            boosts = boosts + note
            liveSet?.boosts?.invalidateData()
        }
    }

    @Synchronized
    private fun innerAddZap(zapRequest: Note, zap: Note?): Boolean {
        if (zapRequest !in zaps.keys) {
            zaps = zaps + Pair(zapRequest, zap)
            return true
        } else if (zaps[zapRequest] == null) {
            zaps = zaps + Pair(zapRequest, zap)
            return true
        }

        return false
    }

    fun addZap(zapRequest: Note, zap: Note?) {
        checkNotInMainThread()
        if (zapRequest !in zaps.keys) {
            val inserted = innerAddZap(zapRequest, zap)
            if (inserted) {
                liveSet?.zaps?.invalidateData()
            }
        } else if (zaps[zapRequest] == null) {
            val inserted = innerAddZap(zapRequest, zap)
            if (inserted) {
                liveSet?.zaps?.invalidateData()
            }
        }
    }

    @Synchronized
    private fun innerAddZapPayment(zapPaymentRequest: Note, zapPayment: Note?): Boolean {
        if (zapPaymentRequest !in zapPayments.keys) {
            zapPayments = zapPayments + Pair(zapPaymentRequest, zapPayment)
            return true
        } else if (zapPayments[zapPaymentRequest] == null) {
            zapPayments = zapPayments + Pair(zapPaymentRequest, zapPayment)
            return true
        }

        return false
    }

    fun addZapPayment(zapPaymentRequest: Note, zapPayment: Note?) {
        checkNotInMainThread()
        if (zapPaymentRequest !in zapPayments.keys) {
            val inserted = innerAddZapPayment(zapPaymentRequest, zapPayment)
            if (inserted) {
                liveSet?.zaps?.invalidateData()
            }
        } else if (zapPayments[zapPaymentRequest] == null) {
            val inserted = innerAddZapPayment(zapPaymentRequest, zapPayment)
            if (inserted) {
                liveSet?.zaps?.invalidateData()
            }
        }
    }

    fun addReaction(note: Note) {
        val tags = note.event?.tags() ?: emptyList()
        val reaction = note.event?.content()?.firstFullCharOrEmoji(ImmutableListOfLists(tags)) ?: "+"

        if (reaction !in reactions.keys) {
            reactions = reactions + Pair(reaction, listOf(note))
            liveSet?.reactions?.invalidateData()
        } else if (reactions[reaction]?.contains(note) == false) {
            reactions = reactions + Pair(reaction, (reactions[reaction] ?: emptySet()) + note)
            liveSet?.reactions?.invalidateData()
        }
    }

    fun addReport(note: Note) {
        val author = note.author ?: return

        if (author !in reports.keys) {
            reports = reports + Pair(author, listOf(note))
            liveSet?.reports?.invalidateData()
        } else if (reports[author]?.contains(note) == false) {
            reports = reports + Pair(author, (reports[author] ?: emptySet()) + note)
            liveSet?.reports?.invalidateData()
        }
    }

    fun addRelay(relay: Relay) {
        if (relay.url !in relays) {
            relays = relays + relay.url
            liveSet?.relays?.invalidateData()
        }
    }

    fun isZappedBy(user: User, account: Account): Boolean {
        // Zaps who the requester was the user
        return zaps.any {
            it.key.author?.pubkeyHex == user.pubkeyHex || account.decryptZapContentAuthor(it.key)?.pubKey == user.pubkeyHex
        } || zapPayments.any {
            val zapResponseEvent = it.value?.event as? LnZapPaymentResponseEvent
            val response = if (zapResponseEvent != null) {
                account.decryptZapPaymentResponseEvent(zapResponseEvent)
            } else {
                null
            }
            response is PayInvoiceSuccessResponse && account.isNIP47Author(zapResponseEvent?.requestAuthor())
        }
    }

    fun publicZapAuthors(): Set<User> {
        // Zaps who the requester was the user
        return zaps.mapNotNull {
            it.key.author
        }.toSet()
    }

    fun publicZapAuthorHexes(): Set<HexKey> {
        // Zaps who the requester was the user
        return zaps.mapNotNull {
            it.key.author?.pubkeyHex
        }.toSet()
    }

    fun reactionAuthors(): Set<User> {
        // Zaps who the requester was the user
        return reactions.values.map {
            it.mapNotNull { it.author }
        }.flatten().toSet()
    }

    fun reactionAuthorHexes(): Set<HexKey> {
        // Zaps who the requester was the user
        return reactions.values.map {
            it.mapNotNull { it.author?.pubkeyHex }
        }.flatten().toSet()
    }

    fun replyAuthorHexes(): Set<HexKey> {
        // Zaps who the requester was the user
        return replies.mapNotNull {
            it.author?.pubkeyHex
        }.toSet()
    }

    fun replyAuthors(): Set<User> {
        // Zaps who the requester was the user
        return replies.mapNotNull {
            it.author
        }.toSet()
    }

    fun boostAuthors(): Set<User> {
        // Zaps who the requester was the user
        return boosts.mapNotNull {
            it.author
        }.toSet()
    }

    fun getReactionBy(user: User): String? {
        return reactions.firstNotNullOfOrNull {
            if (it.value.any { it.author?.pubkeyHex == user.pubkeyHex }) {
                it.key
            } else {
                null
            }
        }
    }

    fun isBoostedBy(user: User): Boolean {
        return boosts.any { it.author?.pubkeyHex == user.pubkeyHex }
    }

    fun hasReportsBy(user: User): Boolean {
        return reports[user]?.isNotEmpty() ?: false
    }

    fun reportAuthorsBy(users: Set<HexKey>): List<User> {
        return reports.keys.filter { it.pubkeyHex in users }
    }

    fun countReportAuthorsBy(users: Set<HexKey>): Int {
        return reports.keys.count { it.pubkeyHex in users }
    }

    fun reportsBy(users: Set<HexKey>): List<Note> {
        return reportAuthorsBy(users).mapNotNull {
            reports[it]
        }.flatten()
    }

    fun countReactions(): Int {
        return reactions.values.sumOf { it.size }
    }

    fun zappedAmount(privKey: ByteArray?, walletServicePubkey: ByteArray?): BigDecimal {
        // Regular Zap Receipts
        val completedZaps = zaps.asSequence()
            .mapNotNull { it.value?.event }
            .filterIsInstance<LnZapEvent>()
            .filter { it.amount != null }
            .associate {
                it.lnInvoice() to it.amount
            }
            .toMap()

        val completedPayments = if (privKey != null && walletServicePubkey != null) {
            // Payments confirmed by the User's Wallet
            zapPayments
                .asSequence()
                .filter {
                    val response = (it.value?.event as? LnZapPaymentResponseEvent)?.response(privKey, walletServicePubkey)
                    response is PayInvoiceSuccessResponse
                }
                .associate {
                    val lnInvoice = (it.key.event as? LnZapPaymentRequestEvent)?.lnInvoice(privKey, walletServicePubkey)
                    val amount = try {
                        if (lnInvoice == null) {
                            null
                        } else {
                            LnInvoiceUtil.getAmountInSats(lnInvoice)
                        }
                    } catch (e: java.lang.Exception) {
                        null
                    }
                    lnInvoice to amount
                }
                .toMap()
        } else {
            emptyMap()
        }

        return (completedZaps + completedPayments).values.filterNotNull().sumOf { it }
    }

    fun hasPledgeBy(user: User): Boolean {
        return replies
            .filter { it.event?.isTaggedHash("bounty-added-reward") ?: false }
            .any {
                val pledgeValue = try {
                    BigDecimal(it.event?.content())
                } catch (e: Exception) {
                    null
                    // do nothing if it can't convert to bigdecimal
                }

                pledgeValue != null && it.author == user
            }
    }

    fun pledgedAmountByOthers(): BigDecimal {
        return replies
            .filter { it.event?.isTaggedHash("bounty-added-reward") ?: false }
            .mapNotNull {
                try {
                    BigDecimal(it.event?.content())
                } catch (e: Exception) {
                    null
                    // do nothing if it can't convert to bigdecimal
                }
            }
            .sumOf { it }
    }

    fun hasAnyReports(): Boolean {
        val dayAgo = TimeUtils.oneDayAgo()
        return reports.isNotEmpty() ||
            (
                author?.reports?.values?.any {
                    it.firstOrNull { (it.createdAt() ?: 0) > dayAgo } != null
                } ?: false
                )
    }

    fun isNewThread(): Boolean {
        return (
            event is RepostEvent ||
                event is GenericRepostEvent ||
                replyTo == null ||
                replyTo?.size == 0
            ) &&
            event !is ChannelMessageEvent &&
            event !is LiveActivitiesChatMessageEvent
    }

    fun hasZapped(loggedIn: User): Boolean {
        return zaps.any { it.key.author == loggedIn }
    }

    fun hasReacted(loggedIn: User, content: String): Boolean {
        return reactedBy(loggedIn, content).isNotEmpty()
    }

    fun reactedBy(loggedIn: User, content: String): List<Note> {
        return reactions[content]?.filter { it.author == loggedIn } ?: emptyList()
    }

    fun reactedBy(loggedIn: User): List<String> {
        return reactions.filter { it.value.any { it.author == loggedIn } }.mapNotNull { it.key }
    }

    fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {
        return boosts.firstOrNull { it.author == loggedIn && (it.createdAt() ?: 0) > TimeUtils.fiveMinutesAgo() } != null // 5 minute protection
    }

    fun boostedBy(loggedIn: User): List<Note> {
        return boosts.filter { it.author == loggedIn }
    }

    fun moveAllReferencesTo(note: AddressableNote) {
        // migrates these comments to a new version
        replies.forEach {
            note.addReply(it)
            it.replyTo = it.replyTo?.updated(this, note)
        }
        reactions.forEach {
            it.value.forEach {
                note.addReaction(it)
                it.replyTo = it.replyTo?.updated(this, note)
            }
        }
        boosts.forEach {
            note.addBoost(it)
            it.replyTo = it.replyTo?.updated(this, note)
        }
        reports.forEach {
            it.value.forEach {
                note.addReport(it)
                it.replyTo = it.replyTo?.updated(this, note)
            }
        }
        zaps.forEach {
            note.addZap(it.key, it.value)
            it.key.replyTo = it.key.replyTo?.updated(this, note)
            it.value?.replyTo = it.value?.replyTo?.updated(this, note)
        }

        replyTo = null
        replies = emptyList()
        reactions = emptyMap()
        boosts = emptyList()
        reports = emptyMap()
        zaps = emptyMap()
    }

    fun clearEOSE() {
        lastReactionsDownloadTime = emptyMap()
    }

    var liveSet: NoteLiveSet? = null

    fun live(): NoteLiveSet {
        if (liveSet == null) {
            liveSet = NoteLiveSet(this)
        }
        return liveSet!!
    }

    fun clearLive() {
        if (liveSet != null && liveSet?.isInUse() == false) {
            liveSet?.destroy()
            liveSet = null
        }
    }

    fun isHiddenFor(accountChoices: Account.LiveHiddenUsers): Boolean {
        if (event == null) return false

        val isBoostedNoteHidden = if (event is GenericRepostEvent || event is RepostEvent || event is CommunityPostApprovalEvent) {
            replyTo?.lastOrNull()?.isHiddenFor(accountChoices) ?: false
        } else {
            false
        }

        val isSensitive = event?.isSensitive() ?: false
        return isBoostedNoteHidden ||
            accountChoices.hiddenUsers.contains(author?.pubkeyHex) ||
            accountChoices.spammers.contains(author?.pubkeyHex) ||
            (isSensitive && accountChoices.showSensitiveContent == false)
    }
}

class NoteLiveSet(u: Note) {
    // Observers line up here.
    val metadata: NoteLiveData = NoteLiveData(u)

    val reactions: NoteLiveData = NoteLiveData(u)
    val boosts: NoteLiveData = NoteLiveData(u)
    val replies: NoteLiveData = NoteLiveData(u)
    val reports: NoteLiveData = NoteLiveData(u)
    val relays: NoteLiveData = NoteLiveData(u)
    val zaps: NoteLiveData = NoteLiveData(u)

    fun isInUse(): Boolean {
        return metadata.hasObservers() ||
            reactions.hasObservers() ||
            boosts.hasObservers() ||
            replies.hasObservers() ||
            reports.hasObservers() ||
            relays.hasObservers() ||
            zaps.hasObservers()
    }

    fun destroy() {
        metadata.destroy()
        reactions.destroy()
        boosts.destroy()
        replies.destroy()
        reports.destroy()
        relays.destroy()
        zaps.destroy()
    }
}

class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
    // Refreshes observers in batches.
    private val bundler = BundledUpdate(500, Dispatchers.IO)

    fun destroy() {
        bundler.cancel()
    }

    fun invalidateData() {
        if (!hasObservers()) return

        checkNotInMainThread()

        bundler.invalidate() {
            checkNotInMainThread()

            if (hasActiveObservers()) {
                postValue(NoteState(note))
            }
        }
    }

    override fun onActive() {
        super.onActive()
        if (note is AddressableNote) {
            NostrSingleEventDataSource.addAddress(note)
        } else {
            NostrSingleEventDataSource.add(note)
        }
    }

    override fun onInactive() {
        super.onInactive()
        if (note is AddressableNote) {
            NostrSingleEventDataSource.removeAddress(note)
        } else {
            NostrSingleEventDataSource.remove(note)
        }
    }
}

@Immutable
class NoteState(val note: Note)
