// import "./picker";
import browser from "webextension-polyfill"
import type { GetCompiledStyleDocument } from "../generated/graphql"
import { graphql } from "~/shared/messaging"

const BLACKLIST = ["docs.google.com"]
const REAPPLY = ["wikia.com"]
const PREVENT_REMOVAL = ["tanstack.com"]

const gql = String.raw
const isFirefox = navigator.userAgent.includes("Gecko/")

// Global states
let isExtensionEnabled = false
let lastIDs = new Set<string>()
const styleNodes = new WeakSet<Element>()
const styleNodesMap = new Map<string, HTMLStyleElement>()

const isXMLViewer = () => document.head?.firstElementChild?.id === "xml-viewer-style"

function toggleEnableStyles(state: boolean) {
  if (state === isExtensionEnabled) return

  isExtensionEnabled = state

  Array.from(document.styleSheets)
    .filter(({ ownerNode }) => styleNodes.has(ownerNode as Element))
    .forEach(stylesheet => {
      stylesheet.disabled = !isExtensionEnabled
    })
}

function applyStyles(enabled: boolean, files: { id: string; code: string }[]) {
  toggleEnableStyles(enabled)
  if (!enabled) return

  const ids = new Set(files.map(_ => _.id))
  for (const id of lastIDs) {
    if (!ids.has(id)) {
      const node = styleNodesMap.get(id)
      styleNodesMap.delete(id)
      node?.remove()
    }
  }

  lastIDs = ids

  for (const { id, code } of files) {
    applyStyle(id, code)
  }

  if (styleNodesMap.size) {
    if (!isXMLViewer() && !isFirefox) {
      function migrate() {
        // Move <body> before all the attached styles so they can take precedence.
        const style = Array.from(
          document.documentElement.querySelectorAll(`:scope > style`),
        ).find(style => styleNodes.has(style))
        if (!style) return
        if (document.body) {
          document.documentElement.insertBefore(document.body, style)
        } else {
          setTimeout(migrate, 1000)
        }
      }
      document.addEventListener("DOMContentLoaded", migrate)
    }
  }
}

function applyStyle(styleId: string, code: string) {
  const current = styleNodesMap.get(styleId)
  if (current) {
    ;(current.childNodes[0] as Text).textContent = code
    return
  }

  const namespace = document.documentElement.namespaceURI
  const node: HTMLStyleElement = document.createElementNS(namespace, "style") as any
  if (isPatched) {
    node.dataset.keep = ""
  }
  node.appendChild(document.createTextNode(code))

  node.disabled = !isExtensionEnabled

  const clonedNode = document.adoptNode(node)
  styleNodes.add(clonedNode)
  if (isFirefox) {
    setTimeout(() => {
      document.body.appendChild(clonedNode)
    }, 29)
  } else if (isXMLViewer()) {
    /**
     * When site response is `application/xml`, Chrome displays our style elements
     * under document.documentElement as plain text so we need to move them into <head>
     * (which already is auto-generated at this moment for the XML response).
     */
    document.head.appendChild(clonedNode)
  } else {
    document.documentElement.appendChild(clonedNode)
  }

  styleNodes.add(node)
  styleNodesMap.set(String(styleId), node)
}

const GET_COMPILED_STYLE = gql`
  query GetCompiledStyle($url: String!) {
    preferences {
      enabled
    }
    files(
      filter: { url: $url, enabled: true, state: None, isGlobal: false }
      logQuery: true
    ) {
      id
      sections {
        compiledCode
      }
    }
  }
` as any as GetCompiledStyleDocument

async function applyCompiledStyle() {
  const {
    data: { preferences, files },
  } = await graphql(GET_COMPILED_STYLE, { url: location.href })

  applyStyles(
    preferences.enabled ?? true,
    files.map(file => ({
      id: file.id,
      code: file.sections.map(section => section.compiledCode).join("\n"),
    })),
  )
}

function endsWith(text: string, list: string[]) {
  return list.some(item => text.endsWith(item))
}

if (!endsWith(location.hostname, BLACKLIST)) {
  void applyCompiledStyle()

  browser.runtime.onMessage.addListener(req => {
    if (req.method === "fileUpdated") {
      void applyCompiledStyle()
    }
  })
}

const isPatched = endsWith(location.hostname, PREVENT_REMOVAL)

// Some React sites remove <style> elements from DOM, so we need to patch them
if (isPatched) {
  function patch() {
    // eslint-disable-next-line @typescript-eslint/unbound-method
    const { removeChild } = Element.prototype
    Object.defineProperty(document.documentElement, "removeChild", {
      value(this: Element, child: HTMLElement) {
        if (child.tagName === "STYLE" && child.dataset.keep != null) {
          return
        }
        return removeChild.call(this, child)
      },
    })
  }

  const script = `(${patch.toString()})()`
  const scriptNode = document.createElement("script")
  scriptNode.appendChild(document.createTextNode(script))
  document.documentElement.appendChild(scriptNode)
  scriptNode.remove()
}

// Fix naughty websites
if (endsWith(location.hostname, REAPPLY)) {
  setTimeout(applyCompiledStyle, 2000)
  setTimeout(applyCompiledStyle, 10000)
}
