import twColors from "tailwindcss/colors"
import {context as pomadeContext} from "@pomade/core"
import {Capacitor} from "@capacitor/core"
import {derived, readable, writable} from "svelte/store"
import * as nip19 from "nostr-tools/nip19"
import {Poll} from "nostr-tools/kinds"
import {
  on,
  gt,
  max,
  spec,
  call,
  first,
  uniqBy,
  sortBy,
  append,
  reject,
  sort,
  uniq,
  indexBy,
  partition,
  shuffle,
  parseJson,
  memoize,
  addToMapKey,
  identity,
  always,
  randomId,
  tryCatch,
  fromPairs,
  groupBy,
  remove,
  simpleCache,
  removeUndefined,
} from "@welshman/lib"
import type {Override} from "@welshman/lib"
import type {RepositoryUpdate} from "@welshman/net"
import {
  Pool,
  load,
  SocketStatus,
  AuthStateEvent,
  AuthStatus,
  SocketEvent,
  netContext,
} from "@welshman/net"
import {
  getter,
  throttled,
  withGetter,
  deriveArray,
  makeDeriveEvent,
  makeLoadItem,
  makeDeriveItem,
  deriveItems,
  deriveItemsByKey,
  deriveDeduplicated,
  deriveEventsById,
  deriveEventsByIdByUrl,
  deriveEventsByIdForUrl,
  getEventsByIdForUrl,
  deriveEventsAsc,
  deriveEventsDesc,
} from "@welshman/store"
import {
  FEED,
  FEEDS,
  APP_DATA,
  CLIENT_AUTH,
  COMMENT,
  DELETE,
  DIRECT_MESSAGE_FILE,
  DIRECT_MESSAGE,
  EVENT_TIME,
  MESSAGE,
  REACTION,
  RELAY_ADD_MEMBER,
  RELAY_JOIN,
  RELAY_LEAVE,
  RELAY_MEMBERS,
  RELAY_REMOVE_MEMBER,
  REPORT,
  ROOM_ADD_MEMBER,
  ROOM_CREATE_PERMISSION,
  ROOM_JOIN,
  ROOM_LEAVE,
  ROOM_MEMBERS,
  ROOM_ADMINS,
  ROOM_META,
  ROOM_DELETE,
  ROOM_REMOVE_MEMBER,
  ROOMS,
  THREAD,
  CLASSIFIED,
  WRAP,
  PROFILE,
  ZAP_GOAL,
  ZAP_REQUEST,
  ZAP_RESPONSE,
  REPOST,
  GENERIC_REPOST,
  asDecryptedEvent,
  getTagValue,
  getGroupTags,
  getListTags,
  getPubkeyTagValues,
  getRelayTagValues,
  getTagValues,
  isRelayUrl,
  normalizeRelayUrl,
  readList,
  verifyEvent,
  readRoomMeta,
  makeRoomMeta,
  ManagementMethod,
  sortEventsAsc,
  sortEventsDesc,
  getAddress,
  Address,
  getIdFilters,
  getEventTagValues,
  getAddressTagValues,
  getParentIds,
  getParentAddrs,
} from "@welshman/util"
import type {
  TrustedEvent,
  RelayProfile,
  PublishedList,
  PublishedRoomMeta,
  RoomMeta,
  List,
  Filter,
} from "@welshman/util"
import {routerContext, Router} from "@welshman/router"
import {
  pubkey,
  repository,
  tracker,
  createSearch,
  userMuteList,
  userFollowList,
  ensurePlaintext,
  makeOutboxLoader,
  appContext,
  deriveRelay,
  makeUserData,
  makeUserLoader,
  manageRelay,
  displayProfileByPubkey,
  getProfile,
} from "@welshman/app"
import {checkRelayHasLivekit} from "$lib/livekit"
import {readFeed} from "@lib/feeds"

export const fromCsv = (s: string) => (s || "").split(",").filter(identity)

export const ROOM = "h"

export const PROTECTED = ["-"]

export const IMAGE_CONTENT_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"]

export const VIDEO_CONTENT_TYPES = ["video/quicktime", "video/webm", "video/mp4"]

export const ENABLE_ZAPS = Capacitor.getPlatform() != "ios"

export const PUSH_SERVER = import.meta.env.VITE_PUSH_SERVER

export const PUSH_BRIDGE = normalizeRelayUrl(import.meta.env.VITE_PUSH_BRIDGE)

export const VAPID_PUBLIC_KEY = import.meta.env.VITE_VAPID_PUBLIC_KEY

export const SIGNER_RELAYS = fromCsv(import.meta.env.VITE_SIGNER_RELAYS).map(normalizeRelayUrl)

export const BLOCKED_RELAYS = fromCsv(import.meta.env.VITE_BLOCKED_RELAYS).map(normalizeRelayUrl)

export const INDEXER_RELAYS = fromCsv(import.meta.env.VITE_INDEXER_RELAYS).map(normalizeRelayUrl)

export const DEFAULT_RELAYS = fromCsv(import.meta.env.VITE_DEFAULT_RELAYS).map(normalizeRelayUrl)

export const DEFAULT_MESSAGING_RELAYS = fromCsv(import.meta.env.VITE_DEFAULT_MESSAGING_RELAYS).map(
  normalizeRelayUrl,
)

export const PLATFORM_RELAYS = fromCsv(import.meta.env.VITE_PLATFORM_RELAYS).map(normalizeRelayUrl)

export const PLATFORM_URL = import.meta.env.VITE_PLATFORM_URL

export const PLATFORM_TERMS = import.meta.env.VITE_PLATFORM_TERMS

export const PLATFORM_PRIVACY = import.meta.env.VITE_PLATFORM_PRIVACY

export const PLATFORM_LOGO = import.meta.env.PROD
  ? PLATFORM_URL + "/logo.png"
  : import.meta.env.VITE_PLATFORM_LOGO.replace(/^static/, "") || PLATFORM_URL + "/logo.png"

export const PLATFORM_NAME = import.meta.env.VITE_PLATFORM_NAME

export const PLATFORM_ACCENT = import.meta.env.VITE_PLATFORM_ACCENT

export const PLATFORM_DESCRIPTION = import.meta.env.VITE_PLATFORM_DESCRIPTION

export const POMADE_SIGNERS = fromCsv(import.meta.env.VITE_POMADE_SIGNERS)

export const DEFAULT_BLOSSOM_SERVERS = fromCsv(import.meta.env.VITE_DEFAULT_BLOSSOM_SERVERS)

export const DEFAULT_PUBKEYS = import.meta.env.VITE_DEFAULT_PUBKEYS

export const DUFFLEPUD_URL = "https://dufflepud.onrender.com"

export const THUMBNAIL_URL = import.meta.env.VITE_THUMBNAIL_URL

export const NIP46_PERMS =
  "nip44_encrypt,nip44_decrypt," +
  [
    CLIENT_AUTH,
    RELAY_JOIN,
    MESSAGE,
    THREAD,
    CLASSIFIED,
    COMMENT,
    ROOMS,
    WRAP,
    REACTION,
    ZAP_REQUEST,
  ]
    .map(k => `sign_event:${k}`)
    .join(",")

export const colors = [
  ["amber", twColors.amber[600]],
  ["blue", twColors.blue[600]],
  ["cyan", twColors.cyan[600]],
  ["emerald", twColors.emerald[600]],
  ["fuchsia", twColors.fuchsia[600]],
  ["green", twColors.green[600]],
  ["indigo", twColors.indigo[600]],
  ["sky", twColors.sky[600]],
  ["lime", twColors.lime[600]],
  ["orange", twColors.orange[600]],
  ["pink", twColors.pink[600]],
  ["purple", twColors.purple[600]],
  ["red", twColors.red[600]],
  ["rose", twColors.rose[600]],
  ["sky", twColors.sky[600]],
  ["teal", twColors.teal[600]],
  ["violet", twColors.violet[600]],
  ["yellow", twColors.yellow[600]],
  ["zinc", twColors.zinc[600]],
]

export const dufflepud = (path: string) => DUFFLEPUD_URL + "/" + path

export const entityLink = (entity: string) => `https://coracle.social/${entity}`

export const pubkeyLink = (pubkey: string, relays = Router.get().FromPubkeys([pubkey]).getUrls()) =>
  entityLink(nip19.nprofileEncode({pubkey, relays}))

export const bootstrapPubkeys = derived(userFollowList, $userFollowList => {
  const appPubkeys = DEFAULT_PUBKEYS.split(",")
  const userPubkeys = shuffle(getPubkeyTagValues(getListTags($userFollowList)))

  return userPubkeys.length > 5 ? userPubkeys : [...userPubkeys, ...appPubkeys]
})

export const deriveEvent = makeDeriveEvent({
  repository,
  includeDeleted: true,
  onDerive: (filters: Filter[], relays: string[]) => load({filters, relays}),
})

export const deriveEvents = (filters: Filter[] = [{}]) =>
  deriveEventsDesc(deriveEventsById({repository, filters}))

export const getEventsForUrl = (url: string, filters: Filter[] = [{}]) =>
  getEventsByIdForUrl({url, tracker, repository, filters}).values()

export const deriveEventsForUrl = (url: string, filters: Filter[] = [{}]) =>
  deriveArray(deriveEventsByIdForUrl({url, tracker, repository, filters}))

export const deriveEventsForUrlAsc = (url: string, filters: Filter[] = [{}]) =>
  deriveEventsAsc(deriveEventsByIdForUrl({url, tracker, repository, filters}))

export const deriveEventsForUrlDesc = (url: string, filters: Filter[] = [{}]) =>
  deriveEventsDesc(deriveEventsByIdForUrl({url, tracker, repository, filters}))

export const deriveLatestEventForUrl = (url: string, filters: Filter[] = [{}]) =>
  deriveDeduplicated(deriveEventsByIdForUrl({url, tracker, repository, filters}), $eventsById =>
    first(sortEventsDesc($eventsById.values())),
  )

export const deriveRelaySignedEvents = (url: string, filters: Filter[] = [{}]) =>
  derived(
    [deriveRelay(url), deriveEventsForUrl(url, filters)],
    ([relay, events]) => events,
    // TODO: khatru doesn't support relay.self, uncomment when it's ready
    // filter(spec({pubkey: relay.self}), events)
  )

// Context

pomadeContext.setSignerUrls(POMADE_SIGNERS)

pomadeContext.setArgonWorker(import("@pomade/core/argon-worker.js?worker"))

appContext.dufflepudUrl = DUFFLEPUD_URL

routerContext.getIndexerRelays = always(INDEXER_RELAYS)

netContext.isEventValid = (event: TrustedEvent, url: string) =>
  getSetting<string[]>("trusted_relays").includes(url) || verifyEvent(event)

// Filters

export const makeCommentFilter = (kinds: number[], extra: Filter = {}) => ({
  kinds: [COMMENT],
  "#K": kinds.map(String),
  ...extra,
})

export const REPOST_KINDS = [REPOST, GENERIC_REPOST]

export const REACTION_KINDS = [REPORT, DELETE, REACTION]

if (ENABLE_ZAPS) {
  REACTION_KINDS.push(ZAP_RESPONSE)
}

export const CONTENT_KINDS = [ZAP_GOAL, EVENT_TIME, THREAD, CLASSIFIED, Poll]

export const MESSAGE_KINDS = [...CONTENT_KINDS, MESSAGE]

export const DM_KINDS = [DIRECT_MESSAGE, DIRECT_MESSAGE_FILE]

// Settings

export const SETTINGS = "flotilla/settings"

export enum RelayAuthMode {
  Aggressive = "aggressive",
  Conservative = "conservative",
}

export type SpaceNotificationSettings = {
  url: string
  notify: boolean
  exceptions: string[]
}

export type SettingsValues = {
  show_media: boolean
  hide_sensitive: boolean
  trusted_relays: string[]
  report_usage: boolean
  report_errors: boolean
  relay_auth: RelayAuthMode
  send_delay: number
  font_size: number
  alerts: SpaceNotificationSettings[]
}

export type Settings = {
  event: TrustedEvent
  values: SettingsValues
}

export const defaultSettings: SettingsValues = {
  show_media: true,
  hide_sensitive: true,
  trusted_relays: [],
  report_usage: true,
  report_errors: true,
  relay_auth: RelayAuthMode.Conservative,
  send_delay: 0,
  font_size: 1.1,
  alerts: [],
}

export const settingsByPubkey = deriveItemsByKey({
  repository,
  getKey: settings => settings.event.pubkey,
  filters: [{kinds: [APP_DATA], "#d": [SETTINGS]}],
  eventToItem: async (event: TrustedEvent) => {
    const values = {...defaultSettings, ...parseJson(await ensurePlaintext(event))}

    return {event, values}
  },
})

export const getSettingsByPubkey = getter(settingsByPubkey)

export const loadSettings = makeLoadItem(
  makeOutboxLoader(APP_DATA, {"#d": [SETTINGS]}),
  (pubkey: string) => getSettingsByPubkey().get(pubkey),
)

export const userSettings = makeUserData(settingsByPubkey, loadSettings)

export const loadUserSettings = makeUserLoader(loadSettings)

export const userSettingsValues = derived(userSettings, $s => $s?.values || defaultSettings)

export const getSettings = getter(userSettingsValues)

export const getSetting = <T>(key: keyof Settings["values"]) => getSettings()[key] as T

// Relays sending events with empty signatures that the user has to choose to trust

export const relaysPendingTrust = writable<string[]>([])

// Relays that mostly send restricted responses to requests and events

export const relaysMostlyRestricted = writable<Record<string, string>>({})

// Push notifications

export const device = withGetter(writable(randomId()))

export const notificationSettings = withGetter(
  writable({
    push: false,
    sound: false,
    badge: false,
    spaces: true,
    mentions: true,
    messages: true,
  }),
)

export type PushSubscription = {
  key: string
  callback: string
}

export type PushState = {
  token?: string
  useFallback?: boolean
  subscription?: PushSubscription
}

export const pushState = withGetter(writable<PushState>({}))

// Chats

export type Chat = {
  id: string
  pubkeys: string[]
  messages: TrustedEvent[]
  last_activity: number
  search_text: string
}

export const getChatPubkeys = (pubkeys: string[]) => sort(uniq(append(pubkey.get()!, pubkeys)))

export const getChatPubkeysFromEvent = (event: TrustedEvent) =>
  getChatPubkeys(getPubkeyTagValues(event.tags).concat(event.pubkey))

export const makeChatId = (pubkeys: string[]) => {
  const userPubkey = pubkey.get()!
  const otherPubkeys = remove(userPubkey, uniq(pubkeys))
  const visiblePubkeys = otherPubkeys.length === 0 ? [userPubkey] : otherPubkeys

  return sort(visiblePubkeys).join(",")
}

export const splitChatId = (id: string) => getChatPubkeys(id.split(","))

export const chatsById = call(() => {
  const chatsById = new Map<string, Chat>()
  const chatsByPubkey = new Map<string, string[]>()

  const addSearchText = (chat: Override<Chat, {search_text?: string}>) => {
    chat.search_text =
      chat.pubkeys.length === 1
        ? displayProfileByPubkey(chat.pubkeys[0]) + " note to self"
        : remove(pubkey.get()!, chat.pubkeys).map(displayProfileByPubkey).join(" ")

    return chat as Chat
  }

  return readable(chatsById, set => {
    const indexChatByPubkeys = (chat: Chat) => {
      for (const pubkey of chat.pubkeys) {
        chatsByPubkey.set(pubkey, uniq(append(chat.id, chatsByPubkey.get(pubkey) || [])))
      }
    }

    const addEvents = (events: TrustedEvent[]) => {
      let dirty = false
      for (const event of events) {
        if (DM_KINDS.includes(event.kind)) {
          const pubkeys = getChatPubkeysFromEvent(event)
          const id = makeChatId(pubkeys)
          const chat = chatsById.get(id)
          const messages = sortBy(
            e => -e.created_at,
            uniqBy(e => e.id, append(event, chat?.messages || [])),
          )
          const last_activity = Math.max(chat?.last_activity || 0, event.created_at)
          const updatedChat = addSearchText({id, pubkeys, messages, last_activity})

          chatsById.set(id, updatedChat)
          indexChatByPubkeys(updatedChat)

          dirty = true
        }

        if (event.kind === PROFILE) {
          for (const chatId of chatsByPubkey.get(event.pubkey) || []) {
            const chat = chatsById.get(chatId)

            if (chat) {
              addSearchText(chat)
              dirty = true
            }
          }
        }
      }

      if (dirty) {
        set(chatsById)
      }
    }

    const removeEvents = (removed: Set<string>) => {
      let dirty = false

      for (const id of removed) {
        const event = repository.getEvent(id)

        if (event && DM_KINDS.includes(event.kind)) {
          for (const chatId of chatsByPubkey.get(event.pubkey) || []) {
            const chat = chatsById.get(chatId)

            if (chat) {
              chat.messages = reject(spec({id: event.id}), chat.messages)
              dirty = true
            }
          }
        }
      }

      if (dirty) {
        set(chatsById)
      }
    }

    addEvents(repository.query([{kinds: [...DM_KINDS, DELETE, PROFILE]}]))

    const unsubscribers = [
      on(repository, "update", ({added, removed}: RepositoryUpdate) => {
        // Do this async so that profiles are populated
        setTimeout(() => {
          addEvents(added)
          removeEvents(removed)
        }, 200)
      }),
    ]

    return () => unsubscribers.forEach(call)
  })
})

export const deriveChat = call(() => {
  const _deriveChat = makeDeriveItem(chatsById)

  return (pubkeys: string[]) => _deriveChat(makeChatId(pubkeys))
})

export const chatSearch = derived(throttled(1500, chatsById), $chatsByPubkey => {
  return createSearch(
    sortBy(c => -c.last_activity, Array.from($chatsByPubkey.values())),
    {
      getValue: (chat: Chat) => chat.id,
      fuseOptions: {keys: ["search_text"]},
    },
  )
})

// Rooms

export enum RoomType {
  Text = "text",
  Voice = "voice",
}

export type Room = PublishedRoomMeta & {
  id: string
  url: string
}

export const getRoomType = (room: RoomMeta): RoomType =>
  room.livekit ? RoomType.Voice : RoomType.Text

export const makeRoomId = (url: string, h: string) => `${url}'${h}`

export const splitRoomId = (id: string) => id.split("'")

export const hasNip29 = (relay?: RelayProfile) =>
  Boolean(relay?.supported_nips?.map?.(String)?.includes?.("29"))

export const roomMetaEventsByIdByUrl = deriveEventsByIdByUrl({
  tracker,
  repository,
  filters: [{kinds: [ROOM_META, ROOM_DELETE]}],
})

export const roomsByUrl = derived(roomMetaEventsByIdByUrl, roomMetaEventsByIdByUrl => {
  const result = new Map<string, Room[]>()

  for (const [url, events] of roomMetaEventsByIdByUrl.entries()) {
    const [metaEvents, deleteEvents] = partition(spec({kind: ROOM_META}), events.values())
    const deletedByH = new Map<string, number>()

    for (const event of deleteEvents) {
      for (const h of getTagValues("h", event.tags)) {
        deletedByH.set(h, max([deletedByH.get(h), event.created_at]))
      }
    }

    const metaById = new Map<string, Room>()

    for (const event of metaEvents) {
      const meta = tryCatch(() => readRoomMeta(event))

      if (!meta || gt(deletedByH.get(meta.h), meta.event.created_at)) {
        continue
      }

      const id = makeRoomId(url, meta.h)

      metaById.set(id, {...meta, url, id})
    }

    if (metaById.size > 0) {
      result.set(url, Array.from(metaById.values()))
    }
  }

  return result
})

export const roomsById = derived(roomsByUrl, roomsByUrl =>
  indexBy(room => room.id, Array.from(roomsByUrl.values()).flatMap(identity)),
)

export const getRoomsById = getter(roomsById)

export const getRoom = (id: string) => getRoomsById().get(id)

export const loadRoom = call(() => {
  const _fetchRoom = async (id: string) => {
    const [url, h] = splitRoomId(id)

    await load({
      relays: [url],
      filters: [{kinds: [ROOM_META], "#d": [h]}],
    })
  }

  const _loadRoom = makeLoadItem(_fetchRoom, getRoom)

  return (url: string, h: string) => _loadRoom(makeRoomId(url, h))
})

export const deriveRoom = call(() => {
  const _deriveRoom = makeDeriveItem(roomsById, loadRoom)

  return (url: string, h: string) =>
    derived(
      _deriveRoom(makeRoomId(url, h)),
      room => (room || {url, id: makeRoomId(url, h), ...makeRoomMeta({h})}) as Room,
    )
})

export const displayRoom = (url: string, h: string) => getRoom(makeRoomId(url, h))?.name || h

export const roomComparator = (url: string) => (h: string) => displayRoom(url, h).toLowerCase()

export const deriveVoiceRooms = (url: string) =>
  derived(roomsById, $roomsById => {
    const set = new Set<string>()
    for (const room of $roomsById.values()) {
      if (room.url === url && room.livekit) {
        set.add(room.h)
      }
    }
    return set
  })

export const deriveOtherVoiceRooms = (url: string) =>
  derived([deriveVoiceRooms(url), deriveUserRooms(url)], ([$roomsWithLivekit, $userRooms]) => {
    const rooms: string[] = []

    for (const h of $roomsWithLivekit) {
      if (!$userRooms.includes(h)) {
        rooms.push(h)
      }
    }

    return sortBy(roomComparator(url), uniq(rooms))
  })

// User space/room lists

export const groupListsByPubkey = deriveItemsByKey({
  repository,
  filters: [{kinds: [ROOMS]}],
  getKey: list => list.event.pubkey,
  eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
})

export const getGroupListsByPubkey = getter(groupListsByPubkey)

export const getGroupList = (pubkey: string) => getGroupListsByPubkey().get(pubkey)

export const loadGroupList = makeLoadItem(makeOutboxLoader(ROOMS), getGroupList)

export const deriveGroupList = makeDeriveItem(groupListsByPubkey, loadGroupList)

export const groupListPubkeysByUrl = derived(groupListsByPubkey, $groupListsByPubkey => {
  const result = new Map<string, Set<string>>()

  for (const list of $groupListsByPubkey.values()) {
    const tags = getListTags(list)

    for (const url of getRelayTagValues(tags)) {
      addToMapKey(result, url, list.event.pubkey)
    }

    for (const tag of getGroupTags(tags)) {
      const url = tag[2] || ""

      if (isRelayUrl(url)) {
        addToMapKey(result, url, list.event.pubkey)
      }
    }
  }

  return result
})

export const deriveGroupListPubkeys = (url: string) =>
  derived(groupListPubkeysByUrl, $groupListPubkeysByUrl => new Set($groupListPubkeysByUrl.get(url)))

export const getSpaceUrlsFromGroupList = (groupList: List | undefined) => {
  const tags = getListTags(groupList)
  const urls = getRelayTagValues(tags)

  for (const tag of getGroupTags(tags)) {
    const url = tag[2] || ""

    if (isRelayUrl(url)) {
      urls.push(url)
    }
  }

  return uniq(urls.map(normalizeRelayUrl))
}

export const getSpaceRoomsFromGroupList = (url: string, groupList: List | undefined) => {
  const rooms: string[] = []

  for (const [_, h, relay] of getGroupTags(getListTags(groupList))) {
    if (url === relay) {
      rooms.push(h)
    }
  }

  return sortBy(roomComparator(url), uniq(rooms))
}

export const userGroupList = makeUserData(groupListsByPubkey, loadGroupList)

export const loadUserGroupList = makeUserLoader(loadGroupList)

export const userSpaceUrls = derived(userGroupList, getSpaceUrlsFromGroupList)

export const deriveUserRooms = (url: string) =>
  derived([userGroupList, roomsById], ([$userGroupList, $roomsById]) => {
    const rooms: string[] = []

    for (const h of getSpaceRoomsFromGroupList(url, $userGroupList)) {
      if ($roomsById.has(makeRoomId(url, h))) {
        rooms.push(h)
      }
    }

    return sortBy(roomComparator(url), rooms)
  })

export const deriveOtherRooms = (url: string) =>
  derived(
    [deriveUserRooms(url), deriveVoiceRooms(url), roomsByUrl],
    ([$userRooms, voiceRooms, $roomsByUrl]) => {
      const rooms: string[] = []

      for (const {h} of $roomsByUrl.get(url) || []) {
        if (!$userRooms.includes(h) && !voiceRooms.has(h)) {
          rooms.push(h)
        }
      }

      return sortBy(roomComparator(url), uniq(rooms))
    },
  )

// Space/room memberships

const getSpaceMembers = (_url: string, events: TrustedEvent[]) => {
  const members = new Set<string>()

  for (const event of sortEventsAsc(events)) {
    if (event.kind === RELAY_MEMBERS) {
      members.clear()

      for (const pubkey of uniq(getTagValues("member", event.tags))) {
        members.add(pubkey)
      }

      continue
    }

    const pubkeys = getPubkeyTagValues(event.tags)

    if (event.kind === RELAY_ADD_MEMBER) {
      for (const pubkey of pubkeys) {
        members.add(pubkey)
      }
    }

    if (event.kind === RELAY_REMOVE_MEMBER) {
      for (const pubkey of pubkeys) {
        members.delete(pubkey)
      }
    }
  }

  return Array.from(members)
}

const getRoomMembers = (_url: string, h: string, events: TrustedEvent[]) => {
  const members = new Set<string>()

  for (const event of sortEventsAsc(events)) {
    if (event.kind === ROOM_MEMBERS && getTagValue("d", event.tags) === h) {
      members.clear()

      for (const pubkey of uniq(getPubkeyTagValues(event.tags))) {
        members.add(pubkey)
      }

      continue
    }

    if (getTagValue("h", event.tags) !== h) {
      continue
    }

    const pubkeys = getPubkeyTagValues(event.tags)

    if (event.kind === ROOM_ADD_MEMBER) {
      for (const pubkey of pubkeys) {
        members.add(pubkey)
      }
    }

    if (event.kind === ROOM_REMOVE_MEMBER) {
      for (const pubkey of pubkeys) {
        members.delete(pubkey)
      }
    }
  }

  return Array.from(members)
}

export const deriveSpaceMembers = (url: string) =>
  derived(
    deriveRelaySignedEvents(url, [{kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, RELAY_MEMBERS]}]),
    $events => getSpaceMembers(url, $events),
  )

export type BannedPubkeyItem = {
  pubkey: string
  reason: string
}

export const spaceBannedPubkeyItems = new Map<string, BannedPubkeyItem[]>()

export const deriveSpaceBannedPubkeyItems = (url: string) => {
  const store = writable(spaceBannedPubkeyItems.get(url) || [])

  manageRelay(url, {method: ManagementMethod.ListBannedPubkeys, params: []}).then(res => {
    spaceBannedPubkeyItems.set(url, res.result)
    store.set(res.result)
  })

  return store
}

export const deriveRoomMembers = (url: string, h: string) => {
  const filters: Filter[] = [
    {kinds: [ROOM_MEMBERS], "#d": [h]},
    {kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [h]},
  ]

  return derived(deriveEventsForUrl(url, filters), $events => getRoomMembers(url, h, $events))
}

export const deriveRoomAdmins = (url: string, h: string) => {
  const filters: Filter[] = [{kinds: [ROOM_ADMINS], "#d": [h]}]

  return derived(deriveEventsForUrl(url, filters), $events => {
    const adminsEvent = first($events)

    if (adminsEvent) {
      return getPubkeyTagValues(adminsEvent.tags)
    }

    return []
  })
}

// Action items (admin review queue)
// const pendingJoins: TrustedEvent[] = []

export const deriveSpaceActionItems = (url: string) =>
  derived(
    deriveEventsForUrl(url, [
      {
        kinds: [REPORT, ROOM_JOIN, ROOM_LEAVE, ROOM_MEMBERS, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER],
      },
    ]),
    $events => {
      const getRoomId = (e: TrustedEvent) =>
        getTagValue(e.kind === ROOM_MEMBERS ? "d" : "h", e.tags)
      const reports = $events.filter(e => e.kind === REPORT)
      const pendingJoins: TrustedEvent[] = []

      // Room-level join requests — most recent per pubkey+h
      for (const [h, roomEvents] of groupBy(getRoomId, $events)) {
        if (!h) continue

        const roomJoins: TrustedEvent[] = []
        const roomLeaves: TrustedEvent[] = []
        const roomMembershipEvents: TrustedEvent[] = []

        for (const event of roomEvents) {
          switch (event.kind) {
            case ROOM_JOIN:
              roomJoins.push(event)
              break
            case ROOM_LEAVE:
              roomLeaves.push(event)
              break
            case ROOM_MEMBERS:
            case ROOM_ADD_MEMBER:
            case ROOM_REMOVE_MEMBER:
              roomMembershipEvents.push(event)
              break
          }
        }

        const roomMembers = new Set(getRoomMembers(url, h, roomMembershipEvents))

        pendingJoins.push(
          ...removeUndefined(
            Array.from(groupBy(e => e.pubkey, roomJoins).values()).map(events =>
              first(sortEventsDesc(events)),
            ),
          ).filter(({pubkey, created_at}) => {
            if (roomMembers.has(pubkey)) return false
            if (
              roomMembershipEvents.some(event => {
                if (event.created_at <= created_at) {
                  return false
                }

                if (event.kind === ROOM_MEMBERS) {
                  return true
                }

                return getPubkeyTagValues(event.tags).includes(pubkey)
              })
            ) {
              return false
            }
            if (roomLeaves.some(e => e.pubkey === pubkey && e.created_at > created_at)) return false

            return true
          }),
        )
      }

      return sortEventsDesc([...reports, ...pendingJoins])
    },
  )

// User membership status

export enum MembershipStatus {
  Initial,
  Pending,
  Granted,
}

export const deriveUserIsSpaceAdmin = memoize((url?: string) => {
  const store = writable(false)

  if (url) {
    manageRelay(url, {method: ManagementMethod.SupportedMethods, params: []}).then(res =>
      store.set(Boolean(res.result?.length)),
    )
  }

  return store
})

export const deriveUserSpaceMembershipStatus = (url: string) => {
  const filters: Filter[] = [{kinds: [RELAY_JOIN, RELAY_LEAVE]}]

  return derived(
    [
      pubkey,
      deriveSpaceMembers(url),
      deriveEventsForUrl(url, filters),
      deriveUserIsSpaceAdmin(url),
    ],
    ([$pubkey, $members, $events, $isAdmin]) => {
      const isMember = $members.includes($pubkey!) || $isAdmin

      for (const event of $events) {
        if (event.pubkey !== $pubkey) {
          continue
        }

        if (event.kind === RELAY_JOIN) {
          return isMember ? MembershipStatus.Granted : MembershipStatus.Pending
        }

        if (event.kind === RELAY_LEAVE) {
          return MembershipStatus.Initial
        }
      }

      return isMember ? MembershipStatus.Granted : MembershipStatus.Initial
    },
  )
}

export const deriveUserIsRoomAdmin = (url: string, h: string) =>
  derived(
    [pubkey, deriveRoomAdmins(url, h), deriveUserIsSpaceAdmin(url)],
    ([$pubkey, $admins, $isSpaceAdmin]) => $isSpaceAdmin || $admins.includes($pubkey!),
  )

export const deriveUserRoomMembershipStatus = (url: string, h: string) => {
  const filters: Filter[] = [{kinds: [ROOM_JOIN, ROOM_LEAVE], "#h": [h]}]

  return derived(
    [
      pubkey,
      deriveRoomMembers(url, h),
      deriveEventsForUrl(url, filters),
      deriveUserIsRoomAdmin(url, h),
    ],
    ([$pubkey, $members, $events, $isAdmin]) => {
      const isMember = $members.includes($pubkey!) || $isAdmin

      for (const event of $events) {
        if (event.pubkey !== $pubkey) {
          continue
        }

        if (event.kind === ROOM_JOIN) {
          return isMember ? MembershipStatus.Granted : MembershipStatus.Pending
        }

        if (event.kind === ROOM_LEAVE) {
          return MembershipStatus.Initial
        }
      }

      return isMember ? MembershipStatus.Granted : MembershipStatus.Initial
    },
  )
}

export const deriveUserCanCreateRoom = (url: string) => {
  const filters: Filter[] = [{kinds: [ROOM_CREATE_PERMISSION]}]

  return derived(
    [pubkey, deriveEventsForUrl(url, filters), deriveUserIsSpaceAdmin(url)],
    ([$pubkey, $events, $isAdmin]) => {
      for (const event of $events) {
        if (getPubkeyTagValues(event.tags).includes($pubkey!)) {
          return true
        }
      }

      return $isAdmin
    },
  )
}

// Feeds

export const feedsByAddress = deriveItemsByKey({
  repository,
  getKey: feed => getAddress(feed.event),
  filters: [{kinds: [FEED]}],
  eventToItem: readFeed,
})

export const getFeedsByAddress = getter(feedsByAddress)

export const feeds = deriveItems(feedsByAddress)

export const getFeeds = getter(feeds)

export const getFeed = (address: string) => getFeedsByAddress().get(address)

export const fetchFeed = (address: string) => {
  const {pubkey} = Address.from(address)

  return load({
    relays: Router.get().FromPubkey(pubkey).getUrls(),
    filters: getIdFilters([address]),
  })
}

export const loadFeed = makeLoadItem(fetchFeed, getFeed)

export const deriveFeed = makeDeriveItem(feedsByAddress, loadFeed)

// Feeds by pubkey

export const feedsByPubkey = derived(feeds, $feeds => groupBy(f => f.event.pubkey, $feeds))

export const getFeedsByPubkey = getter(feedsByPubkey)

export const getFeedsForPubkey = (pubkey: string) => getFeedsByPubkey().get(pubkey)

export const loadFeedsForPubkey = makeLoadItem(makeOutboxLoader(FEED), getFeedsForPubkey)

export const userFeeds = makeUserData(feedsByPubkey, loadFeedsForPubkey)

export const loadUserFeeds = makeUserLoader(loadFeedsForPubkey)

// Feed favorites

export const feedFavoritesByPubkey = deriveItemsByKey<PublishedList>({
  repository,
  getKey: list => list.event.pubkey,
  filters: [{kinds: [FEEDS]}],
  eventToItem: async event =>
    readList(
      asDecryptedEvent(event, {
        content: await ensurePlaintext(event),
      }),
    ),
})

export const getFeedFavoritesByPubkey = getter(feedFavoritesByPubkey)

export const getFeedFavorites = (pubkey: string) => getFeedFavoritesByPubkey().get(pubkey)

export const loadFeedFavorites = makeLoadItem(makeOutboxLoader(FEEDS), getFeedFavorites)

export const userFeedFavorites = makeUserData(feedFavoritesByPubkey, loadFeedFavorites)

export const loadUserFeedFavorites = makeUserLoader(loadFeedFavorites)

// Mutes

export const isEventMuted = withGetter(
  derived(userMuteList, $userMuteList => {
    const pubkey = $userMuteList?.event.pubkey
    const tags = getListTags($userMuteList)
    const mutedEvents = new Set(getEventTagValues(tags))
    const mutedPubkeys = new Set(getPubkeyTagValues(tags))
    const mutedAddresses = new Set(getAddressTagValues(tags))
    const mutedTopics = new Set(getTagValues("t", tags))
    const mutedWords = getTagValues("word", tags)
    const regex =
      mutedWords.length > 0
        ? new RegExp(`\\b(${mutedWords.map(w => w.toLowerCase().trim()).join("|")})\\b`)
        : null

    return (e: TrustedEvent) => {
      if (!pubkey) return false
      if (pubkey === e.pubkey) return false
      if (mutedPubkeys.has(e.pubkey)) return true
      if (mutedEvents.has(e.id)) return true
      if (mutedAddresses.has(getAddress(e))) return true
      if (getParentIds(e).some(id => mutedEvents.has(id))) return true
      if (getParentAddrs(e).some(address => mutedAddresses.has(address))) return true
      if (getTagValues("t", e.tags).some(t => mutedTopics.has(t))) return true

      if (regex) {
        if (e.content?.toLowerCase().match(regex)) return true
        if (displayProfileByPubkey(e.pubkey).toLowerCase().match(regex)) return true
        if (tryCatch(() => getProfile(e.pubkey)?.nip05?.match(regex))) return true
      }

      return false
    }
  }),
)

// Other utils

export const encodeRelay = (url: string) =>
  encodeURIComponent(
    normalizeRelayUrl(url)
      .replace(/^wss:\/\//, "")
      .replace(/\/$/, ""),
  )

export const decodeRelay = (url: string) => normalizeRelayUrl(decodeURIComponent(url))

export const displayReaction = (content: string) => {
  if (!content || content === "+") return "❤️"
  if (content === "-") return "👎"
  return content
}

export const deriveSocket = (url: string) => {
  const socket = Pool.get().get(url)

  return readable(socket, set => {
    const subs = [
      on(socket, SocketEvent.Error, () => set(socket)),
      on(socket, SocketEvent.Status, () => set(socket)),
      on(socket.auth, AuthStateEvent.Status, () => set(socket)),
    ]

    return () => subs.forEach(call)
  })
}

export const deriveSocketStatus = (url: string) =>
  throttled(
    800,
    derived([deriveSocket(url), relaysMostlyRestricted], ([$socket, $relaysMostlyRestricted]) => {
      if ($socket.status === SocketStatus.Opening) {
        return {theme: "warning", title: "Connecting"}
      }

      if ($socket.status === SocketStatus.Closing) {
        return {theme: "gray-500", title: "Not Connected"}
      }

      if ($socket.status === SocketStatus.Closed) {
        return {theme: "gray-500", title: "Not Connected"}
      }

      if ($socket.status === SocketStatus.Error) {
        return {theme: "error", title: "Failed to Connect"}
      }

      if ($socket.auth.status === AuthStatus.Requested) {
        return {theme: "warning", title: "Authenticating"}
      }

      if ($socket.auth.status === AuthStatus.PendingSignature) {
        return {theme: "warning", title: "Authenticating"}
      }

      if ($socket.auth.status === AuthStatus.DeniedSignature) {
        return {theme: "error", title: "Failed to Authenticate"}
      }

      if ($socket.auth.status === AuthStatus.PendingResponse) {
        return {theme: "warning", title: "Authenticating"}
      }

      if ($socket.auth.status === AuthStatus.Forbidden) {
        return {theme: "error", title: "Access Denied"}
      }

      if ($relaysMostlyRestricted[url]) {
        return {theme: "error", title: "Access Denied"}
      }

      return {theme: "success", title: "Connected"}
    }),
  )

export const deriveSupportedMethods = simpleCache(([url]: [string]) => {
  return readable<ManagementMethod[]>([], set => {
    manageRelay(url, {
      method: ManagementMethod.SupportedMethods,
      params: [],
    }).then(({result = []}) => set(result))
  })
})

export const deriveHasLivekit = simpleCache(([url]: [string]) =>
  readable<boolean | undefined>(undefined, set => {
    checkRelayHasLivekit(url).then(has => set(has))
  }),
)

export const deriveTimeout = (timeout: number) => {
  const store = writable<boolean>(false)

  setTimeout(() => store.set(true), timeout)

  return derived(store, identity)
}

export const shouldIgnoreError = (error: string) => {
  const isIgnored = error.startsWith("mute: ")
  const isAborted = error.includes("Signing was aborted")
  const isStrictNip29Relay = error.includes("missing group (`h`) tag")

  return isIgnored || isAborted || isStrictNip29Relay
}

export const stripPrefix = (m: string) => m.replace(/^\w+: /, "")

export type InviteData = {url: string; claim: string}

export const parseInviteLink = (invite: string): InviteData | undefined => {
  if (invite.length < 3 || !invite.includes(".")) {
    return
  }

  return (
    tryCatch(() => {
      const {r: relay = "", c: claim = ""} = fromPairs(Array.from(new URL(invite).searchParams))
      const url = normalizeRelayUrl(relay)

      if (isRelayUrl(url)) {
        return {url, claim}
      }
    }) ||
    tryCatch(() => {
      const url = normalizeRelayUrl(invite)

      if (isRelayUrl(url)) {
        return {url, claim: ""}
      }
    })
  )
}

// Hierarchical notification helpers

export const getShouldNotify = ({alerts}: SettingsValues, url: string, h?: string) => {
  const pref = alerts.find(spec({url}))

  if (!pref) return true
  if (!h) return pref.notify
  if (pref.notify) return !pref.exceptions.includes(h)
  if (!pref.notify) return pref.exceptions.includes(h)
}

export const shouldNotify = (url: string, h?: string) => getShouldNotify(getSettings(), url, h)

export const deriveShouldNotify = (url: string, h?: string) =>
  derived(userSettingsValues, $settings => getShouldNotify($settings, url, h))

// Whatever who cares

export const hasNip50 = (relay?: RelayProfile) =>
  Boolean(relay?.supported_nips?.map?.(String)?.includes?.("50"))
