import { decodeNostrURI } from '@nostr/tools/nip19'
import { normalizeURL } from '@nostr/tools/utils'
import { SubCloser } from '@nostr/tools/abstract-pool'

import { pool, getOutboxRelaysFor } from './nostr.js'
import { debounce } from './utils.js'

export default class NostrLivestream extends HTMLElement {
  static observedAttributes = ['ref', 'autoplay', 'muted']

  root: HTMLDivElement
  subc: SubCloser | undefined

  constructor() {
    super()

    this.attachShadow({ mode: 'open' })
    const { shadowRoot } = this

    this.root = document.createElement('div')
    shadowRoot!.appendChild(this.root)
  }

  connectedCallback() {
    setTimeout(() => {
      let template = this.getAttribute('template')
        ? (document.getElementById(this.getAttribute('template')!) as HTMLTemplateElement)
        : null
      if (template) {
        this.root.appendChild(template.content.cloneNode(true))
      } else {
        this.root.innerHTML = `
        <style>
          header {
            display: flex;
            align-items: center;
          }
          header img {
            height: 2em;
          }
          video {
            width: 100%;
          }
        </style>

        <slot name="header">
          <header part="header">
            <img part="image" />
            <h3 part="title"></h3>
            <p part="status"></p>
          </header >
        </slot>

        <p part="time"></p>
        <p part="summary"></p>

        <section part="stats">
          <slot name="stats">
            <span part="current" style="display: none;">
              current participants: <span part="current-value" />
            </span>
            <span part="total" style="display: none;">
              total participants: <span part="total-value" />
            </span>
          </slot>
        </section>

        <video part="video" controls style="display: none;">
          <source type="application/x-mpegURL">
          your browser does not support HLS video playback
        </video>

        <section part="participants" style="display: none;">
          <slot name="participants">
            <h4 part="participants-title">
              <slot name="participants-title"></slot>
            </h4>
            <ul part="participants-list">
              <template>
                <li part="participant-entry">
                  <nostr-name part="participant-pubkey" />
                  <span part="participant-role"></span>
                </li>
              </template>
            </ul>
          </slot>
        </section>
        `
      }
      this.set()
    }, 1)
  }

  *queryPart(name: string): Generator<HTMLElement> {
    let slotted = this.querySelectorAll(`[part="${name}"]`)
    for (let i = 0; i < slotted.length; i++) {
      yield slotted[i] as HTMLElement
    }
    let templated = this.root.querySelectorAll(`[part="${name}"]`)
    for (let i = 0; i < templated.length; i++) {
      yield templated[i] as HTMLElement
    }
  }

  disconnectedCallback() {
    this.subc?.close?.()
  }

  attributeChangedCallback() {
    this.set()
  }

  set: () => void = debounce(async () => {
    if (this.subc) {
      this.subc.close()
    }

    let input = this.getAttribute('ref')
    if (!input) return

    let { type, data } = decodeNostrURI(input)
    let relays: string[] = []
    let author: string | undefined
    let filter: any

    if (type === 'nevent') {
      let d = data as any
      relays = d.relays || []
      author = d.author
      filter = { ids: [d.id] }
    } else if (type === 'naddr') {
      let d = data as any
      relays = d.relays || []
      author = d.pubkey
      filter = {
        authors: [d.pubkey],
        kinds: [d.kind],
        '#d': [d.identifier],
      }
    } else if (input.match(/[0-9a-f]{64}/)) {
      filter = { ids: [input] }
    } else {
      this.root.innerHTML = '<p part="error"><slot name="error-pointer">not a valid livestream event pointer</slot></p>'
      return
    }

    relays = relays.map(normalizeURL)
    if (author) {
      let authorRelays = await getOutboxRelaysFor(author)
      relays.push(...authorRelays)
    }

    let found = false
    let loadedHLS = false
    let hls: any

    this.subc = pool.subscribeMany(relays, filter, {
      label: 'nostr-livestream',
      onevent: async evt => {
        found = true
        if (evt.kind !== 30311) {
          this.root.innerHTML = '<p part="error"><slot name="error-invalid">event is not a livestream</slot></p>'
          return
        }

        // get metadata from tags and replace in the template
        const title = evt.tags.find((t: string[]) => t[0] === 'title')?.[1]
        if (title) {
          for (let node of this.queryPart('title')) {
            node.textContent = title
          }
        }

        const summary = evt.tags.find((t: string[]) => t[0] === 'summary')?.[1]
        if (summary) {
          for (let node of this.queryPart('summary')) {
            node.textContent = summary
          }
        }

        const image = evt.tags.find((t: string[]) => t[0] === 'image')?.[1]
        if (image) {
          for (let node of this.queryPart('image')) {
            ;(node as HTMLImageElement).src = image
          }
        }

        const status = evt.tags.find((t: string[]) => t[0] === 'status')?.[1] || 'unknown'
        if (status) {
          for (let node of this.queryPart('status')) {
            node.textContent = status
          }
        }

        const current = evt.tags.find((t: string[]) => t[0] === 'current_participants')?.[1]
        if (current) {
          for (let node of this.queryPart('current')) {
            node.style.display = 'block'
          }
          for (let dataEl of this.queryPart('current-value')) {
            dataEl.textContent = current
          }
        }

        const total = evt.tags.find((t: string[]) => t[0] === 'total_participants')?.[1]
        if (total) {
          for (let node of this.queryPart('total')) {
            ;(node as HTMLElement).style.display = 'block'
          }
          for (let dataEl of this.queryPart('total-value')) {
            dataEl.textContent = total
          }
        }

        // get start/end times
        const starts = evt.tags.find((t: string[]) => t[0] === 'starts')?.[1]
        const ends = evt.tags.find((t: string[]) => t[0] === 'ends')?.[1]
        let timeStr = ''
        if (starts) {
          const startDate = new Date(parseInt(starts) * 1000)
          timeStr = startDate.toLocaleString()
          if (ends) {
            const endDate = new Date(parseInt(ends) * 1000)
            timeStr += ` to ${endDate.toLocaleString()}`
          }
          for (let node of this.queryPart('time')) {
            node.textContent = timeStr
          }
        }

        // get participants
        let participantTags = evt.tags.filter((t: string[]) => t[0] === 'p')
        if (participantTags.length) {
          for (let container of this.queryPart('participants')) {
            let templateEl = container.querySelector('template')
            if (templateEl) {
              let template = (templateEl as HTMLTemplateElement).content
              participantTags.forEach((t: string[]) => {
                let entry = template.cloneNode() as HTMLElement

                let pubkeyEl = entry.querySelector('[part="participant-pubkey"]')
                if (pubkeyEl) {
                  if (pubkeyEl.tagName === 'NOSTR-NAME') {
                    pubkeyEl.setAttribute('pubkey', t[1])
                  } else {
                    pubkeyEl.textContent = t[1]
                  }
                }

                entry.querySelectorAll('[part="participant-role"]').forEach(roleEl => {
                  roleEl.textContent = t[3] || 'participant'
                })
              })
            }
          }
        }

        const streamUrl = evt.tags.find((t: string[]) => t[0] === 'streaming')?.[1]
        if (status === 'live' && streamUrl?.endsWith('.m3u8')) {
          let videoEl = this.root.querySelector('video')!
          videoEl.autoplay = this.hasAttribute('autoplay')
          videoEl.muted = this.hasAttribute('muted')
          videoEl.style.display = 'block'
          let sourceEl = videoEl.querySelector('source')!
          if (sourceEl.src !== streamUrl) {
            sourceEl.src = streamUrl

            if (!videoEl.canPlayType('application/vnd.apple.mpegurl') && !loadedHLS) {
              loadedHLS = true
              const script = document.createElement('script')
              script.src = 'https://cdn.jsdelivr.net/npm/hls.js@latest'
              script.onload = () => {
                if ((window as any).Hls.isSupported()) {
                  hls = new (window as any).Hls()
                  hls.loadSource(streamUrl)
                  hls.attachMedia(videoEl)
                }
              }
              document.head.appendChild(script)
            } else if (hls) {
              hls.loadSource(streamUrl)
            }
          }
        }
      },
      oneose: () => {
        if (!found) {
          this.root.innerHTML = '<p part="error"><slot name="error-fetch">failed to fetch event</slot></p>'
          this.subc?.close?.()
        }
      },
    })
  }, 200)
}

window.customElements.define('nostr-livestream', NostrLivestream)
