import * as IO from './IntersectionObserver.js'
import * as Messenger from './Messenger.js'
import type * as Remote from './Remote.js'
import type * as Store from './Store.js'
import * as TrustedHosts from './TrustedHosts.js'

/** Dialog interface — manages the iframe/popup lifecycle for cross-origin auth. */
export type Dialog = SetupFn & Meta

/** Static metadata attached to a dialog function. */
export type Meta = {
  /** Identifier for the dialog type (e.g. `'iframe'`, `'popup'`). */
  name?: string | undefined
}

export type Instance = {
  /** Close the dialog (hide iframe / close popup). */
  close: () => void
  /** Destroy the dialog (remove DOM elements, clean up). */
  destroy: () => void
  /** Open the dialog (show iframe / open popup). */
  open: () => void
  /** Sync the pending request queue to the remote auth app. */
  syncRequests: (sync: Remote.Sync) => Promise<void>
  /** Update the visual theme at runtime. */
  syncTheme: (theme: Theme | undefined) => void
}

/** The setup function a dialog must implement. */
export type SetupFn = (parameters: SetupFn.Parameters) => Instance

export declare namespace SetupFn {
  type Parameters = {
    /** URL of the Tempo Wallet app. */
    host: string
    /** Reactive state store. */
    store: Store.Store
    /** Called when the user rejects the currently displayed requests. */
    onReject: (ids: readonly number[]) => void
    /** Called when the remote UI responds to a request. */
    onResponse: (response: {
      id: number
      result?: unknown
      error?: { code: number; message: string } | undefined
    }) => void
    /** Visual theme overrides applied to the embed. */
    theme?: Theme | undefined
  }
}

/** Visual theme configuration for the dialog embed. */
export type Theme = {
  /** Accent color — a theme preset name or a CSS color value (e.g. `'#6366f1'`). */
  accent?: 'neutral' | 'blue' | 'red' | 'amber' | 'green' | 'purple' | (string & {}) | undefined
  /** Border radius preset. */
  radius?: 'none' | 'small' | 'medium' | 'large' | 'full' | undefined
  /** Color scheme — controls light/dark appearance. Defaults to `'light dark'` (follows OS). */
  scheme?: 'light' | 'dark' | undefined
}

/** Serializes theme options onto a URL's search params. */
function applyThemeParams(url: URL, theme: Theme | undefined) {
  if (!theme) return
  if (theme.accent) url.searchParams.set('accent', theme.accent)
  if (theme.radius) url.searchParams.set('radius', theme.radius)
  if (theme.scheme) url.searchParams.set('scheme', theme.scheme)
}

export const defaultSize = { height: 600, width: 404 }

/** Creates a dialog from metadata and a setup function. */
export function define(meta: Meta, fn: SetupFn): Dialog {
  const { name, ...rest } = meta
  Object.defineProperty(fn, 'name', { value: name, configurable: true })
  return Object.assign(fn, rest) as Dialog
}

/** Detects an insecure context (e.g. HTTP) where iframes lack WebAuthn support. */
export function isInsecureContext(): boolean {
  if (typeof window === 'undefined') return false
  // `http://localhost` is a secure context but WebAuthn still requires HTTPS.
  if (window.location.protocol === 'http:') return true
  return !window.isSecureContext
}

/** Detects Safari (which does not support WebAuthn in cross-origin iframes). */
export function isSafari(): boolean {
  if (typeof navigator === 'undefined') return false
  const ua = navigator.userAgent.toLowerCase()
  return ua.includes('safari') && !ua.includes('chrome')
}

type Cached = {
  host: string
  instance: Instance
  refs: {
    fallback: Instance
    store: Store.Store
  }
}

/** Cached iframe singleton — keyed by host, reused across setup calls. */
let cached: Cached | undefined

/** Creates an iframe dialog that embeds the auth app in a `<dialog>` element. */
export function iframe(): Dialog {
  if (typeof window === 'undefined') return noop()

  return define({ name: 'iframe' }, (parameters) => {
    const { host } = parameters

    // Reuse existing iframe if the host matches — just swap the store/fallback refs.
    if (cached && cached.host === host) {
      cached.refs.store = parameters.store

      cached.refs.fallback.destroy()
      cached.refs.fallback = popup()(parameters)
      cached.instance.syncTheme(parameters.theme)
      return cached.instance
    }

    // Different host — tear down old iframe and create fresh.
    cached?.instance.destroy()

    const refs = {
      fallback: popup()(parameters),
      store: parameters.store,
    }

    let open = false

    const referrer = getReferrer()

    const hostUrl = new URL(host)
    hostUrl.searchParams.set('chainId', String(refs.store.getState().chainId))
    hostUrl.searchParams.set('mode', 'iframe')
    if (referrer.icon) {
      if (typeof referrer.icon === 'string') hostUrl.searchParams.set('icon', referrer.icon)
      else {
        hostUrl.searchParams.set('icon', referrer.icon.light)
        hostUrl.searchParams.set('iconDark', referrer.icon.dark)
      }
    }
    applyThemeParams(hostUrl, parameters.theme)

    const root = document.createElement('dialog')
    root.dataset.tempoWallet = ''

    root.setAttribute('role', 'dialog')
    root.setAttribute('aria-closed', 'true')
    root.setAttribute('aria-label', 'Tempo Wallet')
    root.setAttribute('hidden', 'until-found')

    Object.assign(root.style, {
      background: 'transparent',
      border: '0',
      outline: '0',
      padding: '0',
      position: 'fixed',
    })

    const frame = document.createElement('iframe')
    frame.dataset.testid = 'tempo-wallet'
    frame.setAttribute(
      'allow',
      [
        `publickey-credentials-get ${hostUrl.origin}`,
        `publickey-credentials-create ${hostUrl.origin}`,
        'clipboard-write',
        'payment',
      ].join('; '),
    )
    frame.setAttribute('allowtransparency', 'true')
    frame.setAttribute('tabindex', '0')
    frame.setAttribute('title', 'Tempo Wallet')
    frame.src = hostUrl.toString()

    Object.assign(frame.style, {
      backgroundColor: 'transparent',
      border: '0',
      colorScheme: parameters.theme?.scheme ?? 'light dark',
      height: '100%',
      left: '0',
      position: 'fixed',
      top: '0',
      width: '100%',
    })

    const style = document.createElement('style')
    style.innerHTML = `
        dialog[data-tempo-wallet]::backdrop {
          background: transparent!important;
        }
      `

    root.appendChild(style)
    root.appendChild(frame)

    let readyResult: Messenger.ReadyOptions | undefined
    let sync_displayed: Remote.Sync | undefined
    let requests_displayed: readonly Remote.Request[] = []
    let switchedToPopup = false

    function rejectDisplayedRequests() {
      parameters.onReject(requests_displayed.map((x) => x.request.id))
    }

    function createMessenger() {
      readyResult = undefined

      const m = Messenger.bridge({
        from: Messenger.fromWindow(window, { targetOrigin: hostUrl.origin }),
        to: Messenger.fromWindow(frame.contentWindow!, {
          targetOrigin: hostUrl.origin,
        }),
        waitForReady: true,
      })
      m.on('rpc-response', (response) => parameters.onResponse(response))
      m.waitForReady().then((result) => {
        readyResult = result
        if (result.colorScheme) frame.style.colorScheme = result.colorScheme
        // Ask the wallet to verify the SDK's stored accounts are still valid.
        syncAccounts(m, refs.store)
      })
      m.on('sync', ({ valid }) => {
        if (valid === false) refs.store.disconnect()
      })
      m.on('switch-mode', () => {
        hideDialog()
        activatePage()
        open = false
        switchedToPopup = true

        if (sync_displayed && requests_displayed.length > 0)
          refs.fallback.syncRequests(sync_displayed)
      })
      return m
    }

    document.body.appendChild(root)
    let messenger = createMessenger()

    // Re-mount if removed (e.g. React hydration clears non-server-rendered elements).
    // The iframe reloads on re-append, so the messenger must be re-established.
    new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        for (const node of mutation.removedNodes) {
          if (node !== root) continue
          document.body.appendChild(root)
          messenger.destroy()
          messenger = createMessenger()
          return
        }
      }
    }).observe(document.body, { childList: true })

    let savedOverflow = ''
    let opener: HTMLElement | null = null

    const onBlur = () => rejectDisplayedRequests()

    // 1Password extension adds `inert` attribute to `dialog` rendering it unusable.
    const inertObserver = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        if (mutation.type !== 'attributes') continue
        if (mutation.attributeName !== 'inert') continue
        root.removeAttribute('inert')
      }
    })
    inertObserver.observe(root, { attributeOldValue: true, attributes: true })

    // dialog/page interactivity (no visibility change)
    let dialogActive = false
    const activatePage = () => {
      if (!dialogActive) return
      dialogActive = false

      root.removeEventListener('cancel', onBlur)
      root.removeEventListener('click', onBlur)
      root.style.pointerEvents = 'none'
      opener?.focus()
      opener = null

      document.body.style.overflow = savedOverflow
    }
    const activateDialog = () => {
      if (dialogActive) return
      dialogActive = true

      root.addEventListener('cancel', onBlur)
      root.addEventListener('click', onBlur)
      frame.focus()
      root.style.pointerEvents = 'auto'

      savedOverflow = document.body.style.overflow
      document.body.style.overflow = 'hidden'
    }

    // dialog visibility
    let visible = false
    const showDialog = () => {
      if (visible) return
      visible = true

      if (document.activeElement instanceof HTMLElement) opener = document.activeElement

      root.removeAttribute('hidden')
      root.removeAttribute('aria-closed')
      root.showModal()
    }
    const hideDialog = () => {
      if (!visible) return
      visible = false
      root.setAttribute('hidden', 'true')
      root.setAttribute('aria-closed', 'true')
      root.close()

      // 1Password extension sometimes adds `inert` to dialog siblings
      // and does not clean up when dialog closes.
      for (const sibling of root.parentNode ? Array.from(root.parentNode.children) : []) {
        if (sibling === root) continue
        if (!sibling.hasAttribute('inert')) continue
        sibling.removeAttribute('inert')
      }
    }

    const instance: Instance = {
      close() {
        refs.fallback.close()
        open = false

        hideDialog()
        activatePage()
      },
      destroy() {
        if (cached?.instance === instance) cached = undefined

        refs.fallback.close()
        open = false

        activatePage()
        hideDialog()

        refs.fallback.destroy()
        messenger.destroy()
        root.remove()
        inertObserver.disconnect()
      },
      open() {
        if (open) return
        open = true

        showDialog()
        activateDialog()
      },
      async syncRequests(sync) {
        const { requests } = sync
        sync_displayed = sync
        requests_displayed = requests
        if (switchedToPopup) {
          refs.fallback.syncRequests(sync)
          return
        }

        const { trustedHosts } = readyResult ?? (await messenger.waitForReady())

        // Safari does not support WebAuthn credential creation in iframes.
        if (
          isSafari() &&
          requests.some((x) => ['wallet_connect', 'eth_requestAccounts'].includes(x.request.method))
        ) {
          refs.fallback.syncRequests(sync)
          return
        }

        const ioSupported = IO.supported()
        const hostname = window.location.hostname.replace(/^www\./, '')
        const trusted = Boolean(
          trustedHosts && TrustedHosts.match(trustedHosts, hostname, hostUrl.hostname),
        )
        const secure = ioSupported || trusted

        if (!secure) {
          console.warn(
            [
              `[accounts] Browser does not support IntersectionObserver v2 and "${window.location.hostname}" is not a trusted host.`,
              'Falling back to popup dialog.',
              '',
              'To enable the iframe dialog, add your hostname to the trusted hosts list.',
            ].join('\n'),
          )
          refs.fallback.syncRequests(sync)
        } else {
          const requiresConfirm = requests.some((x) => x.status === 'pending')
          if (!open && requiresConfirm) this.open()
          messenger.send('rpc-requests', {
            account: sync.account,
            chainId: sync.chainId,
            requests,
          })
        }
      },
      syncTheme(theme) {
        frame.style.colorScheme = theme?.scheme ?? 'light dark'
        messenger.send('theme', theme ?? {})
      },
    }

    cached = { host, instance, refs }
    return instance
  })
}

/** Opens the auth app in a new browser window. */
export function popup(options: popup.Options = {}): Dialog {
  if (typeof window === 'undefined') return noop()

  const { size = defaultSize } = options

  return define({ name: 'popup' }, (parameters) => {
    const { host, store } = parameters

    let win: Window | null = null
    let requests_displayed: readonly Remote.Request[] = []

    function rejectDisplayedRequests() {
      parameters.onReject(requests_displayed.map((x) => x.request.id))
    }

    const offDetectClosed = (() => {
      const timer = setInterval(() => {
        if (win?.closed) rejectDisplayedRequests()
      }, 100)
      return () => clearInterval(timer)
    })()

    let messenger: Messenger.Bridge | undefined

    const overlay = document.createElement('div')
    Object.assign(overlay.style, {
      alignItems: 'center',
      background: 'rgba(0, 0, 0, 0.5)',
      color: 'white',
      display: 'none',
      flexDirection: 'column',
      fontFamily: 'system-ui, sans-serif',
      fontSize: '16px',
      gap: '12px',
      inset: '0',
      justifyContent: 'center',
      position: 'fixed',
      zIndex: '2147483647',
    })
    const overlayMessage = document.createElement('p')
    Object.assign(overlayMessage.style, { margin: '0' })
    overlayMessage.textContent = 'Continue in the popup window'
    const overlayClose = document.createElement('button')
    Object.assign(overlayClose.style, {
      background: 'none',
      border: 'none',
      color: 'white',
      cursor: 'pointer',
      font: 'inherit',
      padding: '0',
      textDecoration: 'underline',
    })
    overlayClose.textContent = 'Close'
    overlayClose.addEventListener('click', () => rejectDisplayedRequests())
    overlay.appendChild(overlayMessage)
    overlay.appendChild(overlayClose)
    document.body.appendChild(overlay)

    return {
      close() {
        overlay.style.display = 'none'
        if (!win) return
        win.close()
        win = null
      },
      destroy() {
        this.close()
        messenger?.destroy()
        offDetectClosed()
        overlay.remove()
      },
      open() {
        messenger?.destroy()
        win?.close()

        const referrer = getReferrer()

        const hostUrl = new URL(host)
        hostUrl.searchParams.set('chainId', String(store.getState().chainId))
        hostUrl.searchParams.set('mode', 'popup')
        if (referrer.icon) {
          if (typeof referrer.icon === 'string') hostUrl.searchParams.set('icon', referrer.icon)
          else {
            hostUrl.searchParams.set('icon', referrer.icon.light)
            hostUrl.searchParams.set('iconDark', referrer.icon.dark)
          }
        }
        applyThemeParams(hostUrl, parameters.theme)

        const left = (window.innerWidth - size.width) / 2 + window.screenX
        const top = (window.innerHeight - size.height) / 2 + window.screenY

        win = window.open(
          hostUrl.toString(),
          '_blank',
          `width=${size.width},height=${size.height},left=${left},top=${top}`,
        )
        if (!win) throw new Error('Failed to open popup')

        messenger = Messenger.bridge({
          from: Messenger.fromWindow(window, { targetOrigin: hostUrl.origin }),
          to: Messenger.fromWindow(win, { targetOrigin: hostUrl.origin }),
          waitForReady: true,
        })

        messenger.on('rpc-response', (response) => parameters.onResponse(response))

        overlay.style.display = 'flex'
      },
      async syncRequests(sync) {
        const { requests } = sync
        requests_displayed = requests
        const requiresConfirm = requests.some((x) => x.status === 'pending')
        if (requiresConfirm) {
          if (!win || win.closed) this.open()
          else win.focus()
        }
        messenger?.send('rpc-requests', {
          account: sync.account,
          chainId: sync.chainId,
          requests,
        })
      },
      syncTheme() {},
    }
  })
}

export declare namespace popup {
  type Options = {
    /** Popup window dimensions. @default `{ width: 360, height: 440 }` */
    size?: { width: number; height: number } | undefined
  }
}

/** Returns a no-op dialog for SSR environments. */
export function noop(): Dialog {
  return define({ name: 'noop' }, () => ({
    open() {},
    close() {},
    destroy() {},
    async syncRequests() {},
    syncTheme() {},
  }))
}

/** Sends stored account addresses to the wallet for session validation. */
function syncAccounts(messenger: Messenger.Bridge, store: Store.Store) {
  const { accounts } = store.getState()
  if (accounts.length === 0) return
  messenger.send('sync', { addresses: accounts.map((a) => a.address) })
}

/**
 * Extracts referrer metadata from the host page.
 * Must be called in the host page context (where `document` is accessible).
 */
function getReferrer(): getReferrer.ReturnType {
  const icon = (() => {
    const dark = document.querySelector(
      'link[rel~="icon"][media="(prefers-color-scheme: dark)"]',
    ) as HTMLLinkElement | null
    const light = (document.querySelector(
      'link[rel~="icon"][media="(prefers-color-scheme: light)"]',
    ) ?? document.querySelector('link[rel~="icon"]')) as HTMLLinkElement | null

    if (dark?.href && light?.href && dark.href !== light.href)
      return { dark: dark.href, light: light.href }

    const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches
    return (isDark ? dark?.href : light?.href) ?? light?.href
  })()

  return { icon, title: document.title }
}

declare namespace getReferrer {
  type ReturnType = {
    /** Favicon URL, or separate light/dark URLs. */
    icon: string | { light: string; dark: string } | undefined
    /** Document title of the host page. */
    title: string
  }
}
