import { normalizeURL } from '@nostr/tools/utils'
import { decode } from '@nostr/tools/nip19'
import { nostrUserFromEvent, loadNostrUser } from '@nostr/gadgets/metadata'

import { debounce, handleImageError, splitComma, transparentPixel } from './utils.js'
import { pool } from './nostr.js'

export default class NostrUserSearch extends HTMLElement {
  static observedAttributes = ['relays', 'limit', 'placeholder', 'value']

  root: HTMLDivElement
  limit = 10
  value: string | undefined
  relays: string[] = ['wss://antiprimal.net'].map(normalizeURL)

  constructor() {
    super()

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

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

  connectedCallback() {
    this.style.display = 'inline-block'
    this.style.width = 'fit-content'
    this.style.height = 'fit-content'

    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>
          .wrapper {
            position: relative;
          }
          ul {
            margin: 0;
            margin-top: -1px;
            padding: 0;
            background-color: white;
            position: absolute;
            left: 0;
            border-bottom: 0;
            display: none;
          }
          ul > li {
            display: flex;
            align-items: center;
            cursor: pointer;
          }
          ul > li:hover {
            background-color: rgba(0, 0, 0, 0.05);
          }
          ul > li > img {
            height: 2rem;
            width: 2rem;
          }
          ul > li > span {
            margin: 0 0.5rem;
            max-width: 120px;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
          }
        </style>

        <div class="wrapper" part="wrapper">
          <input type="search" part="input" />
          <ul part="results">
            <template>
              <li part="item">
                <img part="picture" />
                <span part="name"></span>
                <span part="nip05"></span>
              </li>
            </template>
          </ul>
        </div>
        `
      }

      for (let input of this.queryPart<HTMLInputElement>('input')) {
        // setup input listener
        input.oninput = ev => this.search((ev.target as HTMLInputElement).value.trim())

        // apply any attributes that were set before the template was loaded
        if (this.hasAttribute('placeholder')) {
          input.setAttribute('placeholder', this.getAttribute('placeholder')!)
        }
        if (this.hasAttribute('value')) {
          input.value = this.getAttribute('value')!
          this.search(input.value.trim())
        }
      }
    }, 1)
  }

  attributeChangedCallback(name: string, _old: string, value: string) {
    if (name === 'relays') {
      this.relays = splitComma(value).map(normalizeURL)
    } else if (name === 'limit') {
      this.limit = parseInt(value) || 10
    } else if (name === 'placeholder') {
      for (let input of this.queryPart<HTMLInputElement>('input')) {
        input.setAttribute('placeholder', value)
      }
    } else if (name === 'value') {
      for (let input of this.queryPart<HTMLInputElement>('input')) {
        input.value = value
      }
      this.search(value.trim())
    }
  }

  search: (_: string) => void = debounce(async (q: string) => {
    for (let results of this.queryPart('results')) {
      if (q.length < 2) {
        results.style.display = 'none'
        return
      }

      for (let i = 0; i < results.children.length; i++) {
        if (results.children[i].tagName === 'TEMPLATE') continue
        results.removeChild(results.children[i])
      }
      results.style.display = 'block'
    }

    // check if q is a valid npub, nprofile, or hex pubkey
    let pubkey: string | null = null
    try {
      if (q.startsWith('npub1') || q.startsWith('nprofile1')) {
        const decoded = decode(q)
        pubkey = decoded.data as string
      } else if (/^[0-9a-fA-F]{64}$/.test(q)) {
        pubkey = q.toLowerCase()
      }

      if (pubkey) {
        // valid pubkey found, fetch metadata and fire selected event
        this.value = pubkey
        this.dispatchEvent(
          new CustomEvent('selected', {
            bubbles: true,
            detail: loadNostrUser({ pubkey, relays: this.relays }),
          }),
        )
        return
      }
    } catch (e) {
      // invalid format, continue with normal search
    }

    pool.subscribeManyEose(
      this.relays,
      { search: q, limit: this.limit, kinds: [0] },
      {
        onevent: evt => {
          let nu = nostrUserFromEvent(evt)

          for (let results of this.queryPart('results')) {
            const template = results.querySelector('template') as HTMLTemplateElement
            if (!template) continue

            const item = template.content.cloneNode(true) as DocumentFragment
            const itemElement = item.querySelector('[part="item"]') as HTMLElement

            if (itemElement) {
              itemElement.title = nu.npub
              itemElement.onclick = this.handleItemClicked.bind(this)
              itemElement.dataset.event = JSON.stringify(evt)

              // set picture
              const pic = item.querySelector('[part="picture"]') as HTMLImageElement
              if (pic) {
                pic.onerror = handleImageError
                pic.src = nu.image || transparentPixel
              }

              // set name
              const name = item.querySelector('[part="name"]') as HTMLElement
              if (name) {
                name.textContent = nu.shortName
              }

              // Set nip05
              const nip05 = item.querySelector('[part="nip05"]') as HTMLElement
              if (nip05 && nu.metadata.nip05) {
                nip05.textContent = nu.metadata.nip05
              }

              results.appendChild(item)
            }
          }
        },
      },
    )
  }, 450)

  handleItemClicked(ev: MouseEvent) {
    let item = ev.currentTarget! as HTMLLIElement
    let npub = item.title
    let pubkey = decode(npub).data as string
    for (let results of this.queryPart('results')) {
      for (let i = 0; i < results.children.length; i++) {
        if (results.children[i].tagName === 'TEMPLATE') continue
        results.removeChild(results.children[i])
      }
      results.style.display = 'none'
    }
    this.value = pubkey
    this.dispatchEvent(
      new CustomEvent('selected', {
        bubbles: true,
        detail: nostrUserFromEvent(JSON.parse(item.dataset.event!)),
      }),
    )
  }

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

window.customElements.define('nostr-user-search', NostrUserSearch)
