import { cloneCandidate, type Candidate, type Variant } from '../../../../tailwindcss/src/candidate'
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
import type { Writable } from '../../../../tailwindcss/src/types'
import * as ValueParser from '../../../../tailwindcss/src/value-parser'
import { walk, WalkAction } from '../../../../tailwindcss/src/walk'

export function migrateAutomaticVarInjection(
  designSystem: DesignSystem,
  _userConfig: Config | null,
  rawCandidate: string,
): string {
  for (let readonlyCandidate of designSystem.parseCandidate(rawCandidate)) {
    // The below logic makes extended use of mutation. Since candidates in the
    // DesignSystem are cached, we can't mutate them directly.
    let candidate = cloneCandidate(readonlyCandidate) as Writable<Candidate>

    let didChange = false

    // Add `var(…)` in modifier position, e.g.:
    //
    // `bg-red-500/[--my-opacity]` => `bg-red-500/[var(--my-opacity)]`
    if (
      'modifier' in candidate &&
      candidate.modifier?.kind === 'arbitrary' &&
      !isAutomaticVarInjectionException(designSystem, candidate, candidate.modifier.value)
    ) {
      let { value, didChange: modifierDidChange } = injectVar(candidate.modifier.value)
      candidate.modifier.value = value
      didChange ||= modifierDidChange
    }

    // Add `var(…)` to all variants, e.g.:
    //
    // `supports-[--test]:flex'` => `supports-[var(--test)]:flex`
    for (let variant of candidate.variants) {
      let didChangeVariant = injectVarIntoVariant(designSystem, variant)
      if (didChangeVariant) {
        didChange = true
      }
    }

    // Add `var(…)` to arbitrary candidates, e.g.:
    //
    // `[color:--my-color]` => `[color:var(--my-color)]`
    if (
      candidate.kind === 'arbitrary' &&
      !isAutomaticVarInjectionException(designSystem, candidate, candidate.value)
    ) {
      let { value, didChange: valueDidChange } = injectVar(candidate.value)
      candidate.value = value
      didChange ||= valueDidChange
    }

    // Add `var(…)` to arbitrary values for functional candidates, e.g.:
    //
    // `bg-[--my-color]` => `bg-[var(--my-color)]`
    if (
      candidate.kind === 'functional' &&
      candidate.value &&
      candidate.value.kind === 'arbitrary' &&
      !isAutomaticVarInjectionException(designSystem, candidate, candidate.value.value)
    ) {
      let { value, didChange: valueDidChange } = injectVar(candidate.value.value)
      candidate.value.value = value
      didChange ||= valueDidChange
    }

    if (didChange) {
      return designSystem.printCandidate(candidate)
    }
  }
  return rawCandidate
}

function injectVar(value: string): { value: string; didChange: boolean } {
  let didChange = false
  if (value.startsWith('--')) {
    // E.g.:
    //
    // - `--my-color` → `var(--my-color)`             Convert variable
    // - `--my-color,red` → `var(--my-color,red)`     Convert variable with fallback
    // - `--theme(color.red)` → `--theme(color.red)`  Do not convert functions
    //
    if (
      // No `(` definitely means there is no function
      !value.includes('(') ||
      // There could be a function call in the fallback value, but it cannot be
      // top-level, so we can safely check the first part
      ValueParser.parse(value)[0]?.kind !== 'function'
    ) {
      value = `var(${value})`
      didChange = true
    }
  } else if (value.startsWith(' --')) {
    value = value.slice(1)
    didChange = true
  }

  return { value, didChange }
}

function injectVarIntoVariant(designSystem: DesignSystem, variant: Variant): boolean {
  let didChange = false
  if (
    variant.kind === 'functional' &&
    variant.value &&
    variant.value.kind === 'arbitrary' &&
    !isAutomaticVarInjectionException(
      designSystem,
      createEmptyCandidate(variant),
      variant.value.value,
    )
  ) {
    let { value, didChange: valueDidChange } = injectVar(variant.value.value)
    variant.value.value = value
    didChange ||= valueDidChange
  }

  if (variant.kind === 'compound') {
    let compoundDidChange = injectVarIntoVariant(designSystem, variant.variant)
    if (compoundDidChange) {
      didChange = true
    }
  }

  return didChange
}

function createEmptyCandidate(variant: Variant) {
  return {
    kind: 'arbitrary' as const,
    property: 'color',
    value: 'red',
    modifier: null,
    variants: [variant],
    important: false,
    raw: 'candidate',
  } satisfies Candidate
}

const AUTO_VAR_INJECTION_EXCEPTIONS = new Set([
  // Concrete properties
  'scroll-timeline-name',
  'timeline-scope',
  'view-timeline-name',
  'font-palette',
  'anchor-name',
  'anchor-scope',
  'position-anchor',
  'position-try-options',

  // Shorthand properties
  'scroll-timeline',
  'animation-timeline',
  'view-timeline',
  'position-try',
])
// Some properties never had var() injection in v3. We need to convert the candidate to CSS
// so we can check the properties used by the utility.
function isAutomaticVarInjectionException(
  designSystem: DesignSystem,
  candidate: Candidate,
  value: string,
): boolean {
  let ast = designSystem.compileAstNodes(candidate).map((n) => n.node)

  let isException = false
  walk(ast, (node) => {
    if (
      node.kind === 'declaration' &&
      AUTO_VAR_INJECTION_EXCEPTIONS.has(node.property) &&
      node.value == value
    ) {
      isException = true
      return WalkAction.Stop
    }
  })
  return isException
}