import {
  MuteList,
  tryToMuteList,
  fromMuteListToHexSet,
  Pubkey,
  CannotMuteSelfError,
  MuteVisibility
} from '@/domain'
import client from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service'
import dayjs from 'dayjs'
import { Event } from 'nostr-tools'
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { z } from 'zod'
import { useNostr } from './NostrProvider'

type TMuteListContext = {
  mutePubkeySet: Set<string>
  muteList: MuteList | null
  changing: boolean
  getMutePubkeys: () => string[]
  getMuteType: (pubkey: string) => MuteVisibility | null
  mutePubkeyPublicly: (pubkey: string) => Promise<void>
  mutePubkeyPrivately: (pubkey: string) => Promise<void>
  unmutePubkey: (pubkey: string) => Promise<void>
  switchToPublicMute: (pubkey: string) => Promise<void>
  switchToPrivateMute: (pubkey: string) => Promise<void>
}

const MuteListContext = createContext<TMuteListContext | undefined>(undefined)

export const useMuteList = () => {
  const context = useContext(MuteListContext)
  if (!context) {
    throw new Error('useMuteList must be used within a MuteListProvider')
  }
  return context
}

export function MuteListProvider({ children }: { children: React.ReactNode }) {
  const { t } = useTranslation()
  const {
    pubkey: accountPubkey,
    muteListEvent,
    publish,
    updateMuteListEvent,
    nip04Decrypt,
    nip04Encrypt
  } = useNostr()
  const [privateTags, setPrivateTags] = useState<string[][]>([])
  const [changing, setChanging] = useState(false)

  // Decrypt private tags from mute list event
  const getPrivateTags = useCallback(
    async (event: Event) => {
      if (!event.content) return []

      try {
        const storedPlainText = await indexedDb.getDecryptedContent(event.id)

        let plainText: string
        if (storedPlainText) {
          plainText = storedPlainText
        } else {
          plainText = await nip04Decrypt(event.pubkey, event.content)
          await indexedDb.putDecryptedContent(event.id, plainText)
        }

        const tags = z.array(z.array(z.string())).parse(JSON.parse(plainText))
        return tags
      } catch (error) {
        console.error('Failed to decrypt mute list content', error)
        return []
      }
    },
    [nip04Decrypt]
  )

  // Update private tags when mute list event changes
  useEffect(() => {
    const updatePrivateTags = async () => {
      if (!muteListEvent) {
        setPrivateTags([])
        return
      }

      const tags = await getPrivateTags(muteListEvent).catch(() => [])
      setPrivateTags(tags)
    }
    updatePrivateTags()
  }, [muteListEvent, getPrivateTags])

  // Create domain MuteList from event and decrypted private tags
  const muteList = useMemo(
    () => tryToMuteList(muteListEvent, privateTags),
    [muteListEvent, privateTags]
  )

  // Legacy compatibility: expose as Set<string> for existing consumers
  const mutePubkeySet = useMemo(
    () => (muteList ? fromMuteListToHexSet(muteList) : new Set<string>()),
    [muteList]
  )

  const getMutePubkeys = useCallback(() => {
    return Array.from(mutePubkeySet)
  }, [mutePubkeySet])

  const getMuteType = useCallback(
    (pubkey: string): MuteVisibility | null => {
      if (!muteList) return null
      const pk = Pubkey.tryFromString(pubkey)
      return pk ? muteList.getMuteVisibility(pk) : null
    },
    [muteList]
  )

  // Publish updated mute list with rate limiting
  const publishMuteList = async (updatedMuteList: MuteList, encryptedContent: string) => {
    if (dayjs().unix() === muteListEvent?.created_at) {
      await new Promise((resolve) => setTimeout(resolve, 1000))
    }

    const draftEvent = updatedMuteList.toDraftEvent(encryptedContent)
    const event = await publish(draftEvent)
    toast.success(t('Successfully updated mute list'))
    return event
  }

  const mutePubkeyPublicly = async (pubkey: string) => {
    if (!accountPubkey || changing) return

    setChanging(true)
    try {
      const latestEvent = await client.fetchMuteListEvent(accountPubkey)
      if (!latestEvent) {
        const result = confirm(t('MuteListNotFoundConfirmation'))
        if (!result) return
      }

      const ownerPubkey = Pubkey.fromHex(accountPubkey)
      const decryptedPrivateTags = latestEvent ? await getPrivateTags(latestEvent) : []
      const currentMuteList = latestEvent
        ? MuteList.fromEvent(latestEvent, decryptedPrivateTags)
        : MuteList.empty(ownerPubkey)

      const targetPubkey = Pubkey.tryFromString(pubkey)
      if (!targetPubkey) return

      try {
        const change = currentMuteList.mutePublicly(targetPubkey)
        if (change.type === 'no_change') return

        // Encrypt private tags if there are any
        const encryptedContent = currentMuteList.hasPrivateMutes()
          ? await nip04Encrypt(accountPubkey, JSON.stringify(currentMuteList.toPrivateTags()))
          : ''

        const newEvent = await publishMuteList(currentMuteList, encryptedContent)
        await updateMuteListEvent(newEvent, currentMuteList.toPrivateTags())
      } catch (error) {
        if (error instanceof CannotMuteSelfError) return
        throw error
      }
    } catch (error) {
      toast.error(t('Failed to mute user publicly') + ': ' + (error as Error).message)
    } finally {
      setChanging(false)
    }
  }

  const mutePubkeyPrivately = async (pubkey: string) => {
    if (!accountPubkey || changing) return

    setChanging(true)
    try {
      const latestEvent = await client.fetchMuteListEvent(accountPubkey)
      if (!latestEvent) {
        const result = confirm(t('MuteListNotFoundConfirmation'))
        if (!result) return
      }

      const ownerPubkey = Pubkey.fromHex(accountPubkey)
      const decryptedPrivateTags = latestEvent ? await getPrivateTags(latestEvent) : []
      const currentMuteList = latestEvent
        ? MuteList.fromEvent(latestEvent, decryptedPrivateTags)
        : MuteList.empty(ownerPubkey)

      const targetPubkey = Pubkey.tryFromString(pubkey)
      if (!targetPubkey) return

      try {
        const change = currentMuteList.mutePrivately(targetPubkey)
        if (change.type === 'no_change') return

        // Always encrypt when adding private mutes
        const encryptedContent = await nip04Encrypt(
          accountPubkey,
          JSON.stringify(currentMuteList.toPrivateTags())
        )

        const newEvent = await publishMuteList(currentMuteList, encryptedContent)
        await updateMuteListEvent(newEvent, currentMuteList.toPrivateTags())
      } catch (error) {
        if (error instanceof CannotMuteSelfError) return
        throw error
      }
    } catch (error) {
      toast.error(t('Failed to mute user privately') + ': ' + (error as Error).message)
    } finally {
      setChanging(false)
    }
  }

  const unmutePubkey = async (pubkey: string) => {
    if (!accountPubkey || changing) return

    setChanging(true)
    try {
      const latestEvent = await client.fetchMuteListEvent(accountPubkey)
      if (!latestEvent) return

      const decryptedPrivateTags = await getPrivateTags(latestEvent)
      const currentMuteList = MuteList.fromEvent(latestEvent, decryptedPrivateTags)

      const targetPubkey = Pubkey.tryFromString(pubkey)
      if (!targetPubkey) return

      const change = currentMuteList.unmute(targetPubkey)
      if (change.type === 'no_change') return

      // Re-encrypt if there are still private mutes
      const encryptedContent = currentMuteList.hasPrivateMutes()
        ? await nip04Encrypt(accountPubkey, JSON.stringify(currentMuteList.toPrivateTags()))
        : ''

      const newEvent = await publishMuteList(currentMuteList, encryptedContent)
      await updateMuteListEvent(newEvent, currentMuteList.toPrivateTags())
    } finally {
      setChanging(false)
    }
  }

  const switchToPublicMute = async (pubkey: string) => {
    if (!accountPubkey || changing) return

    setChanging(true)
    try {
      const latestEvent = await client.fetchMuteListEvent(accountPubkey)
      if (!latestEvent) return

      const decryptedPrivateTags = await getPrivateTags(latestEvent)
      const currentMuteList = MuteList.fromEvent(latestEvent, decryptedPrivateTags)

      const targetPubkey = Pubkey.tryFromString(pubkey)
      if (!targetPubkey) return

      const change = currentMuteList.switchToPublic(targetPubkey)
      if (change.type === 'no_change') return

      // Re-encrypt private tags
      const encryptedContent = currentMuteList.hasPrivateMutes()
        ? await nip04Encrypt(accountPubkey, JSON.stringify(currentMuteList.toPrivateTags()))
        : ''

      const newEvent = await publishMuteList(currentMuteList, encryptedContent)
      await updateMuteListEvent(newEvent, currentMuteList.toPrivateTags())
    } finally {
      setChanging(false)
    }
  }

  const switchToPrivateMute = async (pubkey: string) => {
    if (!accountPubkey || changing) return

    setChanging(true)
    try {
      const latestEvent = await client.fetchMuteListEvent(accountPubkey)
      if (!latestEvent) return

      const decryptedPrivateTags = await getPrivateTags(latestEvent)
      const currentMuteList = MuteList.fromEvent(latestEvent, decryptedPrivateTags)

      const targetPubkey = Pubkey.tryFromString(pubkey)
      if (!targetPubkey) return

      const change = currentMuteList.switchToPrivate(targetPubkey)
      if (change.type === 'no_change') return

      // Encrypt the updated private tags
      const encryptedContent = await nip04Encrypt(
        accountPubkey,
        JSON.stringify(currentMuteList.toPrivateTags())
      )

      const newEvent = await publishMuteList(currentMuteList, encryptedContent)
      await updateMuteListEvent(newEvent, currentMuteList.toPrivateTags())
    } finally {
      setChanging(false)
    }
  }

  return (
    <MuteListContext.Provider
      value={{
        mutePubkeySet,
        muteList,
        changing,
        getMutePubkeys,
        getMuteType,
        mutePubkeyPublicly,
        mutePubkeyPrivately,
        unmutePubkey,
        switchToPublicMute,
        switchToPrivateMute
      }}
    >
      {children}
    </MuteListContext.Provider>
  )
}
