import type { ParsedDevRule } from '@twind/core'
import type { ColorInformation, Diagnostics, DocumentationAt } from '../types'
import type { IntellisenseContext, Boundary } from '../internal/types'

import csstreeParse from 'css-tree/parser'
import csstreeWalk from 'css-tree/walker'
import csstreeGenerate from 'css-tree/generator'

import { parse } from '@twind/core'
import { fixClassList, parseHTML } from '../../../core/src/internal/parse-html'
import { toClassName } from '../../../core/src/internal/to-class-name'

import { editabelColorRe, parseColor } from '../internal/color'
import { adjustRuleLocation } from '../internal/adjust-rule-location'

export function documentationAt(
  content: string,
  offset: number,
  { isIgnored }: IntellisenseContext,
): DocumentationAt | null {
  let result: DocumentationAt | null = null

  parseHTML(content, (startIndex, endIndex, quote) => {
    if (startIndex <= offset && offset < endIndex) {
      // offset is within this classList
      const token = content.slice(startIndex, endIndex)

      // TODO: after fixClassList the positions maybe invalid
      const rules = parse(fixClassList(token, quote)) as ParsedDevRule[]

      for (const rule of rules) {
        const start = startIndex + rule.l[0]
        const end = startIndex + rule.l[1]

        if (start <= offset && offset < end) {
          // found our rule
          if (!isIgnored(rule.n)) {
            result = {
              ...adjustRuleLocation(token, rule, startIndex),
              value: toClassName(rule),
            }
          }

          return false
        }

        if (offset < end) {
          return false
        }
      }

      return false
    }

    if (offset < startIndex) {
      return false
    }
  })

  return result
}

export function collectColors(
  content: string,
  { classes, isIgnored }: IntellisenseContext,
): ColorInformation[] {
  const colors: ColorInformation[] = []

  parseHTML(content, (startIndex, endIndex, quote) => {
    const token = content.slice(startIndex, endIndex)
    const rules = parse(fixClassList(token, quote)) as ParsedDevRule[]

    for (const rule of rules) {
      if (isIgnored(rule.n)) continue

      const completion = classes.get(rule.n)

      if (completion?.color) {
        const color = parseColor(completion.color)

        if (color) {
          colors.push({
            ...adjustRuleLocation(token, rule, startIndex),
            value: completion.color,
            rgba: color,
          })

          continue
        }
      }

      const editableMatch = rule.n.match(editabelColorRe)

      if (editableMatch) {
        const { 1: currentColor } = editableMatch
        const color = parseColor(currentColor)

        if (color) {
          colors.push({
            ...adjustRuleLocation(token, rule, startIndex),
            value: currentColor,
            rgba: color,
            editable: true,
          })

          continue
        }
      }
    }
  })

  return colors
}

export function validate(
  content: string,
  { variants, classes, isIgnored, generateCSS }: IntellisenseContext,
): Diagnostics[] {
  const diagnostics: Diagnostics[] = []

  parseHTML(content, (startIndex, endIndex, quote) => {
    const token = content.slice(startIndex, endIndex)
    const rules = parse(fixClassList(token, quote)) as ParsedDevRule[]

    for (const rule of rules) {
      if (isIgnored(rule.n)) continue

      const css = generateCSS(rule.n)

      const ast = csstreeParse(css, {
        positions: false,
        parseAtrulePrelude: false,
        parseRulePrelude: false,
        parseValue: false,
        parseCustomProperty: false,
        onParseError(error) {
          diagnostics.push({
            ...adjustRuleLocation(token, rule, startIndex),
            code: 'invalidCSS',
            message: `Failed to parse CSS of class ${JSON.stringify(rule.n)}: ${error.message}`,
            severity: 'error',
            value: rule.n,
          })
        },
      })

      if (ast) {
        // TODO: csstree-validator uses createRequire to fetch mdn-data -> this does not work in the browser
        // if (typeof document !== 'object') {
        //   const cssValidator = await import('csstree-validator')

        //   // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
        //   for (const error of cssValidator.validate(ast)) {
        //     diagnostics.push({
        //       ...adjustRuleLocation(token, rule, startIndex),
        //       code: 'invalidCSS',
        //       // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
        //       message: error.message,
        //       severity: 'warning',
        //       value: rule.n,
        //     })
        //   }
        // }

        if (typeof document == 'object') {
          csstreeWalk(ast, {
            visit: 'SelectorList',
            enter(node) {
              const selector = csstreeGenerate(node)
              try {
                document.querySelector(selector)
              } catch (error) {
                // Some thrown errors are because of specific pseudo classes
                // lets filter them to prevent unnecessary warnings
                // ::-moz-focus-inner
                // :-moz-focusring
                if (/:(-webkit-|-moz-|-ms-)/.test(selector)) {
                  diagnostics.push({
                    ...adjustRuleLocation(token, rule, startIndex),
                    code: 'invalidCSS',
                    message: `Vendor specific selector ${JSON.stringify(
                      selector,
                    )} for class ${JSON.stringify(rule.n)}`,
                    severity: 'hint',
                    value: rule.n,
                  })
                } else {
                  diagnostics.push({
                    ...adjustRuleLocation(token, rule, startIndex),
                    code: 'invalidCSS',
                    message: `Invalid selector ${JSON.stringify(
                      selector,
                    )} for class ${JSON.stringify(rule.n)}`,
                    severity: 'warning',
                    value: rule.n,
                  })
                }
              }
            },
          })
        }
      }

      if (!(classes.has(rule.n) || css)) {
        diagnostics.push({
          ...adjustRuleLocation(token, rule, startIndex),
          code: 'invalidClass',
          message: `Invalid class ${JSON.stringify(rule.n)}`,
          severity: 'error',
          value: rule.n,
        })
      }

      for (const variant of rule.v) {
        const className = variant + ':' + rule.n
        if (isIgnored(className)) continue

        const css = generateCSS(className)

        const ast = csstreeParse(css, {
          positions: false,
          parseAtrulePrelude: false,
          parseRulePrelude: false,
          parseValue: false,
          parseCustomProperty: false,
          onParseError(error) {
            diagnostics.push({
              ...adjustRuleLocation(token, rule, startIndex),
              code: 'invalidCSS',
              message: `Failed to parse CSS of variant ${JSON.stringify(variant)}: ${
                error.message
              }`,
              severity: 'error',
              value: rule.n,
            })
          },
        })

        if (ast) {
          if (typeof document == 'object') {
            csstreeWalk(ast, {
              visit: 'SelectorList',
              enter(node) {
                const selector = csstreeGenerate(node)
                try {
                  document.querySelector(selector)
                } catch {
                  // Some thrown errors are because of specific pseudo classes
                  // lets filter them to prevent unnecessary warnings
                  // ::-moz-focus-inner
                  // :-moz-focusring
                  if (/:(-webkit-|-moz-|-ms-)/.test(selector)) {
                    diagnostics.push({
                      ...adjustRuleLocation(token, rule, startIndex),
                      code: 'invalidCSS',
                      message: `Vendor specific selector ${JSON.stringify(
                        selector,
                      )} for variant ${JSON.stringify(variant)}`,
                      severity: 'hint',
                      value: rule.n,
                    })
                  } else {
                    diagnostics.push({
                      ...adjustRuleLocation(token, rule, startIndex),
                      code: 'invalidCSS',
                      message: `Invalid selector ${JSON.stringify(
                        selector,
                      )} for variant ${JSON.stringify(variant)}`,
                      severity: 'warning',
                      value: rule.n,
                    })
                  }
                }
              },
            })
          }
        }

        if (!(variants.has(variant + ':') || css)) {
          diagnostics.push({
            ...adjustRuleLocation(token, rule, startIndex),
            code: 'invalidVariant',
            message: `Invalid variant ${JSON.stringify(variant)}`,
            severity: 'error',
            value: variant,
          })
        }
      }
    }
  })

  return diagnostics
}

export function extractBoundary(content: string, position: number): Boundary | null {
  return (
    find(`class="`, /[^\\]"/) ||
    find(`class='`, /[^\\]'/) ||
    find(`class=`, /[\s"'`=;>]/) ||
    // svelte class toggle
    // 'class:...',
    find(`class:`, /[\s"'/=]/)
  )

  function find(search: string, invalid: RegExp, before = /\s/): Boundary | null {
    const startIndex = content.lastIndexOf(search, position)

    // found and the char before is a white space
    if (startIndex !== -1 && before.test(content[startIndex - 1])) {
      const boundary = content.slice(startIndex + search.length, position)

      // maybe an expression like class="{...}"
      // TODO: for now ignore expression
      if (/{/.test(boundary[0])) {
        return null
      }

      if (invalid.test(boundary)) {
        return null
      }

      return { start: startIndex + search.length, end: position, content: boundary }
    }

    return null
  }
}
