'use client'

import classNames from 'classnames'
import { forwardRef, HTMLAttributes, MutableRefObject, useEffect, useLayoutEffect, useRef } from 'react'
import type { Booleanish } from 'shared-types'

const defaultPath = 'https://punkt-cdn.oslo.kommune.no/latest/icons/'

if (typeof window !== 'undefined') {
  window.pktFetch = window.pktFetch === undefined ? (fetch as unknown as typeof window.pktFetch) : window.pktFetch
  window.pktIconPath = window.pktIconPath || defaultPath
}

const errorSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"></svg>'
const dlCache: { [key: string]: Promise<string> } = {}
const MAX_RETRIES = 2
const RETRY_DELAY = 1500

const isSessionStorageAvailable = typeof Storage !== 'undefined' && typeof sessionStorage !== 'undefined'

const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect

const getCachedIconSync = (name: string, path: string | undefined): string => {
  if (!name || !isSessionStorageAvailable) return ''
  return sessionStorage.getItem(path + name + '.svg') ?? ''
}

const fetchIcon = (url: string): Promise<string> =>
  (window.pktFetch as unknown as typeof fetch)(url).then((response) => {
    if (!response.ok) {
      throw new Error('Missing icon: ' + url)
    }
    return response.text()
  })

const fetchIconWithRetry = async (url: string, retries = MAX_RETRIES): Promise<string> => {
  try {
    return await fetchIcon(url)
    // eslint-disable-next-line @typescript-eslint/no-unused-vars -- we want to catch all errors, including from fetchIcon
  } catch (error) {
    if (retries > 0) {
      await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY))
      return fetchIconWithRetry(url, retries - 1)
    }
    // eslint-disable-next-line no-console -- we want to log the error when all retries have been exhausted
    console.error('Failed to fetch icon: ' + url)
    return errorSvg
  }
}

const downloadIconOrGetFromCache = async (name: string, path: string | undefined): Promise<string | null> => {
  const key = path + name + '.svg'

  if (isSessionStorageAvailable && sessionStorage.getItem(key)) {
    return sessionStorage.getItem(key)
  }

  if (key in dlCache) {
    return dlCache[key]
  }

  if (typeof window !== 'undefined' && typeof window.pktFetch === 'function') {
    dlCache[key] = fetchIconWithRetry(key).then((text) => {
      if (text !== errorSvg && isSessionStorageAvailable) {
        sessionStorage.setItem(key, text)
      }
      delete dlCache[key]
      return text
    })
    return dlCache[key]
  }
  return errorSvg
}

interface IPktIcon extends HTMLAttributes<HTMLElement> {
  name?: string
  path?: string
  ariaHidden?: Booleanish
  ariaLabel?: string
}

const toBoolean = (v: Booleanish | undefined): boolean | undefined => {
  if (v === undefined) return undefined
  return v === true || v === 'true'
}

export const PktIcon = forwardRef<HTMLSpanElement, IPktIcon>(function PktIcon(
  {
    name = '',
    path,
    className,
    ariaHidden,
    ariaLabel,
    'aria-hidden': ariaHiddenAttr,
    'aria-label': ariaLabelAttr,
    ...rest
  },
  forwardedRef,
) {
  const resolvedPath = path ?? (typeof window !== 'undefined' ? window.pktIconPath : defaultPath)
  const elementRef = useRef<HTMLSpanElement | null>(null)

  const explicitAriaHidden = toBoolean(ariaHidden) ?? toBoolean(ariaHiddenAttr)
  const explicitAriaLabel = ariaLabel ?? ariaLabelAttr
  const effectiveAriaHidden = explicitAriaHidden !== undefined ? explicitAriaHidden : !explicitAriaLabel
  const effectiveAriaLabel = effectiveAriaHidden ? undefined : explicitAriaLabel || name

  useIsomorphicLayoutEffect(() => {
    const el = elementRef.current
    if (!el) return

    const applyAria = (root: HTMLElement) => {
      const svg = root.querySelector('svg')
      if (!svg) return
      svg.setAttribute('aria-hidden', String(effectiveAriaHidden))
      if (effectiveAriaLabel) {
        svg.setAttribute('aria-label', effectiveAriaLabel)
      } else {
        svg.removeAttribute('aria-label')
      }
    }

    if (!name) {
      el.innerHTML = ''
      return
    }

    const cached = getCachedIconSync(name, resolvedPath)
    if (cached) {
      el.innerHTML = cached
      applyAria(el)
      return
    }

    let cancelled = false
    downloadIconOrGetFromCache(name, resolvedPath).then((text) => {
      if (cancelled) return
      const current = elementRef.current
      if (current) {
        current.innerHTML = text || errorSvg
        applyAria(current)
      }
    })
    return () => {
      cancelled = true
    }
  }, [name, resolvedPath, effectiveAriaHidden, effectiveAriaLabel])

  const setRef = (el: HTMLSpanElement | null) => {
    elementRef.current = el
    if (typeof forwardedRef === 'function') {
      forwardedRef(el)
    } else if (forwardedRef) {
      ;(forwardedRef as MutableRefObject<HTMLSpanElement | null>).current = el
    }
  }

  return (
    <span
      {...rest}
      ref={setRef}
      className={classNames('pkt-icon', className)}
      {...(name ? { name } : {})}
      data-path={resolvedPath}
    />
  )
})

PktIcon.displayName = 'PktIcon'
