import { logger } from '@bamboocss/logger'
import { capitalize, isBaseCondition, isObject, toRem, withoutSpace } from '@bamboocss/shared'
import type {
  ConditionDetails,
  ConditionQuery,
  Conditions as ConditionsConfig,
  ThemeVariantsMap,
} from '@bamboocss/types'
import { Breakpoints } from './breakpoints'
import { parseCondition } from './parse-condition'
import { expandRange, rangeQuery, sortScale } from './range-query'
import { compareAtRuleOrMixed } from './sort-style-rules'

/**
 * Checks if a condition is an at-rule type
 */
const isAtRule = (cond: ConditionDetails): boolean => cond.type === 'at-rule'

/**
 * Matches pseudo-element selectors (::before, ::after, ::placeholder, etc.)
 * Pseudo-elements must appear at the end of CSS selector chains per the CSS spec.
 */
const pseudoElementRegex = /::[\w-]/
const isPseudoElement = (cond: ConditionDetails): boolean =>
  typeof cond.raw === 'string' && pseudoElementRegex.test(cond.raw)

/**
 * Flattens a condition, extracting parts from mixed conditions.
 * Returns an array of { condition, originalIndex } to track source order.
 */
const flattenCondition = (
  cond: ConditionDetails,
  originalIndex: number,
): Array<{ cond: ConditionDetails; originalIndex: number }> => {
  if (cond.type === 'mixed') {
    // Extract parts from mixed condition, each inherits the original index
    const parts = cond.value as ConditionDetails[]
    return parts.map((part) => ({ cond: part, originalIndex }))
  }
  return [{ cond, originalIndex }]
}

interface Options {
  conditions?: ConditionsConfig
  breakpoints?: Record<string, string>
  containerNames?: string[]
  containerSizes?: Record<string, string>
  themes?: ThemeVariantsMap
}

const underscoreRegex = /^_/
const selectorRegex = /&|@/

export class Conditions {
  values: Record<string, ConditionDetails>

  breakpoints: Breakpoints

  constructor(private options: Options) {
    const { breakpoints: breakpointValues = {}, conditions = {} } = options

    const breakpoints = new Breakpoints(breakpointValues)
    this.breakpoints = breakpoints

    const entries = Object.entries(conditions).map(([key, value]) => [`_${key}`, parseCondition(value)])

    const containers = this.setupContainers()
    const themes = this.setupThemes()

    this.values = {
      ...Object.fromEntries(entries),
      ...breakpoints.conditions,
      ...containers,
      ...themes,
    }
  }

  /**
   * Container sizes, as the same range set breakpoints get.
   *
   * These used to be the `min` half only — one `@/md` per size, written as `(min-width: …)`.
   * Two things came of sharing the construction with breakpoints instead. Containers gain the
   * `@/mdOnly`, `@/mdDown` and `@/mdToXl` keys they never had, and the feature becomes
   * `inline-size`, which is what a container query is actually asking about: `width` and
   * `inline-size` diverge the moment a container is in a vertical writing mode.
   *
   * `params` now carries the query rather than the bare size. It fed the at-rule sorter, which
   * reads a bound out of it, and ` 40rem` has no bound to read — so container queries sorted by
   * length alone. A `Down` range would have inherited that and sorted among the `min` bounds.
   */
  private setupContainers = () => {
    const { containerNames = [], containerSizes = {} } = this.options

    const containers: Record<string, ConditionDetails> = {}
    containerNames.unshift('') // add empty container name for @/sm, @/md, etc.

    const sizes = new Map(sortScale(containerSizes).map(([size, value]) => [size, toRem(value) ?? value]))
    const ranges = expandRange([...sizes.keys()])

    containerNames.forEach((name) => {
      ranges.forEach(({ key, min, max }) => {
        const query = rangeQuery('inline-size', min && sizes.get(min), max && sizes.get(max))
        if (!query) return

        const params = `${name} ${query}`.trim()

        containers[`@${name}/${key}`] = {
          type: 'at-rule',
          name: 'container',
          value: query,
          raw: `@container ${params}`,
          params,
        }
      })
    })

    return containers
  }

  private setupThemes = () => {
    const { themes = {} } = this.options

    const themeVariants: Record<string, ConditionDetails> = {}
    Object.entries(themes).forEach(([theme, _themeVariant]) => {
      const condName = this.getThemeName(theme)
      const cond = parseCondition(this.getThemeSelector(theme) + ' &')
      if (!cond) return

      themeVariants[condName] = cond
    })

    return themeVariants
  }

  getThemeSelector = (name: string) => {
    return `[data-bamboo-theme=${name}]`
  }

  getThemeName = (theme: string) => {
    return '_theme' + capitalize(theme)
  }

  finalize = (paths: string[]) => {
    return paths.map((path) => {
      if (this.has(path)) {
        return path.replace(underscoreRegex, '')
      }

      if (selectorRegex.test(path)) {
        return `[${withoutSpace(path.trim())}]`
      }

      return path
    })
  }

  shift = (paths: string[]) => {
    return paths
      .map((path) => path.trim())
      .sort((a, b) => {
        const aIsCondition = this.isCondition(a)
        const bIsCondition = this.isCondition(b)
        if (aIsCondition && !bIsCondition) return 1
        if (!aIsCondition && bIsCondition) return -1
        if (!aIsCondition && !bIsCondition) return -1
        return 0
      })
  }

  segment = (paths: string[]): { condition: string[]; selector: string[] } => {
    const condition: string[] = []
    const selector: string[] = []

    for (const path of paths) {
      if (this.isCondition(path)) {
        condition.push(path)
      } else {
        selector.push(path)
      }
    }

    return { condition, selector }
  }

  has = (key: string) => {
    return Object.prototype.hasOwnProperty.call(this.values, key)
  }

  isCondition = (key: string) => {
    return this.has(key) || !!this.getRaw(key) || isBaseCondition(key)
  }

  isEmpty = () => {
    return Object.keys(this.values).length === 0
  }

  get = (key: string): ConditionQuery | undefined => {
    const details = this.values[key]
    return details?.raw as ConditionQuery | undefined
  }

  getRaw = (condNameOrQuery: ConditionQuery): ConditionDetails | undefined => {
    if (typeof condNameOrQuery === 'string' && this.values[condNameOrQuery]) return this.values[condNameOrQuery]

    try {
      return parseCondition(condNameOrQuery)
    } catch (error) {
      const query = typeof condNameOrQuery === 'string' ? condNameOrQuery : JSON.stringify(condNameOrQuery)
      const message = error instanceof Error ? error.message : String(error)
      logger.warn('core:condition', `Failed to parse condition "${query}": ${message}`)
    }
  }

  sort = (conditions: string[]): ConditionDetails[] => {
    const rawConditions = conditions.map(this.getRaw).filter(Boolean) as ConditionDetails[]

    // Flatten all conditions while tracking original index for stable sorting
    const flattened = rawConditions.flatMap((cond, index) => flattenCondition(cond, index))

    // Sort: at-rules first, pseudo-elements last, then selectors in original order
    flattened.sort((a, b) => {
      const aIsAtRule = isAtRule(a.cond)
      const bIsAtRule = isAtRule(b.cond)

      // At-rules come first
      if (aIsAtRule && !bIsAtRule) return -1
      if (!aIsAtRule && bIsAtRule) return 1

      // Pseudo-elements (::before, ::after, etc.) must come last per CSS spec
      const aIsPseudo = isPseudoElement(a.cond)
      const bIsPseudo = isPseudoElement(b.cond)
      if (aIsPseudo !== bIsPseudo) return aIsPseudo ? 1 : -1

      // Within same category, preserve original source order
      return a.originalIndex - b.originalIndex
    })

    return flattened.map((item) => item.cond)
  }

  normalize = (condition: ConditionQuery | ConditionDetails): ConditionDetails | undefined => {
    return isObject(condition) ? (condition as ConditionDetails) : this.getRaw(condition)
  }

  keys = () => {
    return Object.keys(this.values)
  }

  saveOne = (key: string, value: string) => {
    const parsed = parseCondition(value)
    if (!parsed) return

    this.values[`_${key}`] = parsed
  }

  remove(key: string) {
    delete this.values[`_${key}`]
  }

  getSortedKeys = () => {
    return Object.keys(this.values).sort((a, b) => {
      const aCondition = this.values[a]
      const bCondition = this.values[b]

      const score = compareAtRuleOrMixed(
        { entry: {} as any, conditions: [aCondition] },
        { entry: {} as any, conditions: [bCondition] },
      )
      return score
    })
  }
}
