import { Component, CssRule, Editor, Selector } from 'grapesjs'
import { ComplexSelector, fromString, noPseudo, toString } from './ComplexSelector'
import { AttributeOperatorType, AttributeSelector, ClassSelector, IdSelector, isSameSelector, SimpleSelector, SimpleSelectorType, TAGS, TagSelector } from './SimpleSelector'
import { OperatorType } from './Operator'
import { CompoundSelector } from './CompoundSelector'

////////////////
// Canvas body remap
// In GrapesJS, the iframe DOM is <body> → <div.wrapper> → [components]. The
// wrapper has a class (set by the silex-lib body-remap plugin) and CSS rules
// targeting `body` are rewritten to target that class so they visually apply.
// For .matches() to also recognize selectors like `body > .x`, we apply the
// same rewrite here before testing against the DOM.
// Kept in sync with silex-lib/src/ts/client/grapesjs/body-remap.ts
const CANVAS_BODY_WRAPPER_CLASS = 'silex-page-body'
const BODY_TAG_SELECTOR_RE = /(^|[,\s>+~])body(?![a-zA-Z0-9_-])/g
function normalizeSelectorForCanvas(selector: string): string {
  if (selector.includes(CANVAS_BODY_WRAPPER_CLASS)) return selector
  return selector.replace(BODY_TAG_SELECTOR_RE, `$1.${CANVAS_BODY_WRAPPER_CLASS}`)
}

////////////////
// GrapeJs functions
const untranslatedKeys = new Set<string>()
export function getTranslation(editor: Editor, key: string): string {
  if(!key) return ''
  const translated = editor?.I18n?.t(key)
  if (!translated) {
    untranslatedKeys.add(key)
    console.info(`Untranslated key "${ key }", call editor.runCommand("i18n:info") to see all untranslated keys`)
  }
  return translated || key
}

export function getUntranslatedKeys(): string[] {
  return Array.from(untranslatedKeys)
}

////////////////
// Stye Manager functions
/**
 * Get all selectors that match the selected component
 * Don't take pseudo class into account
 */
export function getSelectors(editor: Editor): ComplexSelector[] {
  return editor.getSelectedAll()
    .flatMap((component: Component) => {
      return editor
        .CssComposer
        .getRules()
        .reduce<ComplexSelector[]>((acc, _rule: CssRule) => {
          const rule = _rule.clone()

          // Check if the rule has a style applied
          if (Object.keys(rule.getStyle()).length === 0) {
            // No style, this is just a selector
            return acc
          }
          const selectorString = rule.getSelectorsString()
          if (!selectorString) {
            // Empty selector, this must be being edited
            console.warn('Empty selector for rule', rule.get('state'), { rule })
            return acc
          }

          try {
            // Get a complex selector for this rule
            const complex = fromString(selectorString, rule.getAtRule())

            // Remove the pseudo class
            const complexNoPseudo: ComplexSelector = noPseudo(complex)

            // Back to a string
            const selectorNoPseudo = toString(complexNoPseudo)

            // Add the matching selectors
            if (component.view?.el.matches(normalizeSelectorForCanvas(selectorNoPseudo))) {
              acc.push(complex)
            } else if (component.tagName.toLowerCase() === 'body' && selectorNoPseudo.toLowerCase().startsWith('html')) {
              // `html` selector: not present in canvas DOM, match on body component
              acc.push(complex)
            }
          } catch (e) {
            console.warn('Error matching selector', selectorString, e)
          }

          return acc
        }, [])
    })
}

/**
 * Function to edit or add style based on the selector
 */
export function editStyle(editor: Editor, selector: string) {
  const currentWidth = editor.DeviceManager.getSelected()?.get('widthMedia')

  const opts = {
    atRuleType: currentWidth ? 'media' : '',
    atRuleParams: currentWidth ? `(max-width: ${currentWidth})` : '',
  }
  const old = editor.CssComposer.getRule(selector, opts)
  const rule = editor.CssComposer.setRule(selector, old?.getStyle(), {
    addStyles: !!old?.getStyle(),
    ...opts,
  })
  editor.StyleManager.select(rule)
}

/**
 * Check if all the selected components are selected by the provided selector
 */
export function matchSelectorAll(selector: string, components: Component[]): boolean {
  try {
    const normalized = normalizeSelectorForCanvas(selector)
    return components.some((component) => {
      // `html` selector: not present in canvas DOM, match on body component
      if (component.tagName?.toLowerCase() === 'body' && selector.toLowerCase().startsWith('html')) {
        return true
      }
      return component.view?.el.matches(normalized)
    })
  } catch {
    return false
  }
}

/**
 * Check if some of the selected components are selected by the provided selector
 */
export function matchSelectorSome(selector: string, components: Component[]): boolean {
  try {
    const normalized = normalizeSelectorForCanvas(selector)
    return components.every((component) => {
      // `html` selector: not present in canvas DOM, match on body component
      if (component.tagName?.toLowerCase() === 'body' && selector.toLowerCase().startsWith('html')) {
        return true
      }
      return component.view?.el.matches(normalized)
    })
  } catch {
    return false
  }
}

/**
 * Remove the style (rules) for the current selector (edited selector)
 */
export function clearStyle(editor: Editor) {
  // Remove the selected rule from the CSS Composer
  const rule = editor.StyleManager.getSelected()
  if (!rule) {
    console.warn('No rule selected')
    return
  }
  editor.CssComposer.remove(rule as CssRule)
}

export function getSelectedStyle(editor: Editor): any {
  return editor.StyleManager.getSelected()
    ?.getStyle()
}

export function setSelectedStyle(editor: Editor, style: any) {
  editor.StyleManager.getSelected()
    ?.setStyle(style)
}

export function renameSelector(editor: Editor, oldSelector: SimpleSelector, newSelector: SimpleSelector) {
  // Rename the selector in the Style manager
  if (oldSelector.type !== newSelector.type) {
    // Cannot rename to a different type
    console.warn('Cannot rename to a different types', oldSelector, newSelector)
    return
  }
  if (isSameSelector(oldSelector, newSelector)) {
    // No change
    console.warn('No change', oldSelector, newSelector)
    return
  }
  switch (oldSelector.type) {
  case SimpleSelectorType.ID:
    // Will never reach here as a different ID is not allowed (readonly + no suggestion)
    // const id = (newSelector as IdSelector).value
    // const components = editor.getSelectedAll()
    // if (components.length !== 1) {
    //   console.error('Cannot rename ID selector for multiple components', { oldSelector, newSelector }, 'Only one component will ave the ID', id)
    // }
    // components[0].setId(id)
    throw new Error('Cannot rename ID selector')
  case SimpleSelectorType.CLASS:
    renameCssClass(editor, (oldSelector as ClassSelector).value, (newSelector as ClassSelector).value)
    break
  case SimpleSelectorType.TAG:
    // Readonly
    // const components = editor.getSelectedAll()
    // components.forEach((component) => component.set('tagName', (newSelector as ClassSelector).value))
    throw new Error('Cannot rename ID selector')
  default:
    console.error('Cannot rename selector: Unknown Type', { oldSelector, newSelector })
  }
}

/**
 * Removes the specified CSS class from all selected components in the editor.
 * Similar to renameSelector, but instead of renaming, it deletes the class from the components.
 */
export function removeClass(editor: Editor, selector: SimpleSelector) {
  if (selector.type !== SimpleSelectorType.CLASS) {
    return
  }
  const className = (selector as ClassSelector).value
  if (!className) {
    console.warn('Invalid class name provided.')
    return
  }

  // Get all selected components
  const components = editor.getSelectedAll()
  components.forEach(component => {
    // Remove the class from the component
    const classes = component.getClasses()
    const newClasses = classes.filter((cls: string) => cls !== className)
    component.setClass(newClasses)
  })
}

/**
 * Renames a CSS class in the style manager and updates all components using that class.
 * - Updates the selector name in the SelectorManager.
 * - Finds all components with the old class name and replaces it with the new class name.
 * - Handles undo logic by updating the selector before modifying components.
 */
function renameCssClass(editor: Editor, oldClassName: string, newClassName: string) {
  if (!oldClassName || !newClassName || oldClassName === newClassName) {
    console.warn('Invalid class names or same names provided.')
    return
  }

  const sm = editor.SelectorManager
  const oldSelector = sm.get(oldClassName)
  if (!oldSelector) {
    console.warn(`Class "${oldClassName}" not found.`)
    return
  }

  // TODO: handle the related selectors
  // IMPORTANT: Find components BEFORE renaming the selector
  // Because oldSelector.set('name') will automatically update component.getClasses()

  // Helper function to recursively find all components with a class in their selector
  const findComponentsWithClass = (component: Component, className: string): Component[] => {
    const results: Component[] = []

    // Check if this component has the class in its selector
    const selector = component.get('selector')
    if (selector?.mainSelector?.selectors) {
      const hasClass = selector.mainSelector.selectors.some(
        (sel: any) => sel.type === SimpleSelectorType.CLASS && sel.value === className
      )
      if (hasClass) {
        results.push(component)
      }
    }

    // Recursively check children
    component.components().forEach((child: Component) => {
      results.push(...findComponentsWithClass(child, className))
    })

    return results
  }

  const toUpdate = editor.Pages.getAll()
    .flatMap(page => findComponentsWithClass(page.getMainComponent(), oldClassName))

  // This has to be before we update component selectors (but after finding them!)
  // Otherwise it messes up the undo
  oldSelector.set('name', newClassName)

  // Rename the class in the components
  // TODO: handle the related selectors
  // NOTE: No need to manually remove the old class from component.getClasses()
  // because oldSelector.set('name', newClassName) already updated it automatically
  toUpdate.forEach(component => {
    // Update the selector stored in component attributes
    const selector = getComponentSelector(component)
    const updatedSelector = {
      ...selector,
      mainSelector: {
        ...selector.mainSelector,
        selectors: selector.mainSelector.selectors
          // Remove the new class to avoid duplicates
          .filter(sel => sel.type !== SimpleSelectorType.CLASS || (sel as ClassSelector).value !== newClassName)
          .map(sel =>
            sel.type === SimpleSelectorType.CLASS && (sel as ClassSelector).value === oldClassName
              ? { ...sel, value: newClassName }
              : sel
          ),
      },
    }
    setComponentSelector(component, updatedSelector)
  })
}

////////////////
// Component functions
/**
 * Stores a selector in a component's attributes.
 */
export function setComponentSelector(component: Component, selector: ComplexSelector) {
  component.set('selector', {
    ...selector,
  })
  const classes: string[] = []
  selector.mainSelector.selectors.forEach((simpleSelector) => {
    switch (simpleSelector.type) {
    case 'id':
      component.setId((simpleSelector as IdSelector).value)
      break
    case 'class':
      classes.push((simpleSelector as ClassSelector).value)
      break
    case 'tag':
      break // Tags are inherent to the component, no model update needed
    default:
      console.warn('Unhandled selector type', simpleSelector)
    }
  })
  // FIXME: Add back the protected classes
  // Replace the classes with the ones from the selector
  const existing = component.getClasses()

  // Keep existing classes that are not in the selector + add all classes from the selector
  const finalClasses = [
    ...existing.filter((c: string) => !classes.includes(c)),
    ...classes
  ]

  component.setClass(finalClasses)
}

export function getComponentSelector(component: Component): ComplexSelector {
  // FIXME: Backward compatibility should be handled in the app
  const oldClasses: string[] = component.getClasses()
  const selector = component.get('selector') ?? {
    mainSelector: {
      selectors: oldClasses
        .map(className => ({ type: SimpleSelectorType.CLASS, value: className, active: false })),
    },
  }
  return {
    ...selector,
  }
}

////////////////
// Selector functions
/**
 * Get the suggestions for the selected components which have the provided selector
 * We want to suggest the main selector:
 * - The component tag name and ID
 * - Any classes that are in the current website (will be added to the component if selected) but no classes that are already in the component
 */
export function getSuggestionsMain(editor: Editor, components: Component[], selector: ComplexSelector): SimpleSelector[] {
  const suggestions: SimpleSelector[] = []

  components.forEach(component => {
    // Add the component tag name and ID
    addTagNames(component, suggestions, selector.mainSelector)

    // Add the website classes
    const classes = component.getClasses()
    editor.CssComposer.getAll().forEach((rule) => {
      rule.getSelectors().forEach((sel: Selector) => {
        const s = {
          type: SimpleSelectorType.CLASS,
          value: sel.get('name'),
          active: true,
        } as ClassSelector
        if (sel.get('type') === 1 &&
          !sel.get('private') &&
          !classes.includes(sel.get('name')) &&
          canIadd(s, selector.mainSelector, suggestions)
        ) {
          suggestions.push(s)
        }
      })
    })
    // Add attributes (excluding 'id' since it should use ID selector instead)
    const attributes = component.getAttributes({ noStyle: true, noClass: true })
    Object.keys(attributes).forEach((attr) => {
      // Skip 'id' attribute - it should use ID selector instead of attribute selector
      if (attr === 'id') return

      const s = {
        type: SimpleSelectorType.ATTRIBUTE,
        value: attr,
        active: true,
      } as AttributeSelector
      if (attributes[attr]) {
        s.operator = AttributeOperatorType.EQUALS
        s.attributeValue = attributes[attr]
      }
      if (canIadd(s, selector.mainSelector, suggestions)) {
        suggestions.push(s)
      }
    })
  })
  return suggestions
}

/**
 * Get the suggestions for the related selectors. Depending on the operator (> => look for children, '+' => look for siblings, ' ' => look for parents):
 * - Any classes that are in the parents/children/siblings of the selected components
 * - Add the classes that are on each parent/child/sibling but not on the selected components
 * - Same for IDs, tag names
 */
export function getSuggestionsRelated(editor: Editor, components: Component[], selector: ComplexSelector): SimpleSelector[] {
  if (!selector.operator) {
    return []
  }
  const suggestions: SimpleSelector[] = []
  switch (selector.operator?.type) {
  case OperatorType.HAS:
    // Add the children classes, tag names, and IDs inside the selected components
    components
      .forEach((component) => {
        suggestions.push(...getSuggestions(getChildren(component), selector.relatedSelector || { selectors: [] }))
      })
    break
  case OperatorType.NOT:
    suggestions.push(...TAGS.map((tag) => ({
      type: SimpleSelectorType.TAG,
      value: tag,
      active: true,
    } as TagSelector)))
    components.forEach((component) => {
      const classes = component.getClasses()

      editor.CssComposer.getAll().forEach((rule) => {
        rule.getSelectors().forEach((sel: Selector) => {
          if (sel.get('private')) {
            return
          }
          if (sel.get('type') === 1 && !classes.includes(sel.get('name'))) {
            const classSelector = {
              type: SimpleSelectorType.CLASS,
              value: sel.get('name'),
              active: true,
            } as ClassSelector
            if (canIadd(classSelector, selector.relatedSelector!, suggestions)) {
              suggestions.push(classSelector)
            }
          } else if (sel.get('type') === 2) {
            const idSelector = {
              type: SimpleSelectorType.ID,
              value: sel.get('name'),
              active: true,
            } as IdSelector
            if (canIadd(idSelector, selector.relatedSelector!, suggestions)) {
              suggestions.push(idSelector)
            }
          }
        })
      })
    })
    break
  case OperatorType.IS:
  case OperatorType.WHERE:
    suggestions.push(...getSuggestions(components, selector.relatedSelector || { selectors: [] }))
    break
  case OperatorType.CHILD:
    components.forEach((component) => {
      const parent = component.parent()
      if (parent) {
        // Add the parent tag names, IDs, and classes
        suggestions.push(...getSuggestions([parent], selector.relatedSelector || { selectors: [] }))
      }
    })
    break
  case OperatorType.DESCENDANT:
    components.forEach((component) => {
      suggestions.push(...getSuggestions(getParents(component), selector.relatedSelector || { selectors: [] }))
    })
    break
  case OperatorType.ADJACENT:
    // Only the sibling immediately before each component
    const siblingsAdjacent: Component[] = components
      .map((comp) => getSiblingsBefore(comp)[0])
      .filter((c): c is Component => !!c)
    suggestions.push(...getSuggestions(siblingsAdjacent, selector.relatedSelector || { selectors: [] }))
    break
  case OperatorType.GENERAL_SIBLING:
    // All siblings before each component
    const siblingsGeneral: Component[] = components
      .flatMap(getSiblingsBefore)
      .filter((c): c is Component => !!c)
    suggestions.push(...getSuggestions(siblingsGeneral, selector.relatedSelector || { selectors: [] }))
    break
  default:
    console.error('Unhandled operator', selector.operator)
    throw new Error(`Unhandled operator: ${selector.operator}`)
  }
  return suggestions
}

/**
 * Add classes, tag names to the suggestions
 */
function getSuggestions(components: Component[], selector: CompoundSelector): SimpleSelector[] {
  const suggestions: SimpleSelector[] = []

  // Tag names
  components.forEach((component) => {
    addTagNames(component, suggestions, selector)
  })

  // Classes
  components
    .flatMap((comp) => comp.getClasses())
    .forEach((cl: string) => {
      // Only classes not in the suggestions yet
      if (suggestions.some((s) => s.type === SimpleSelectorType.CLASS && (s as ClassSelector).value === cl)) {
        return
      }
      // Only classes not in the related selector yet
      if (selector.selectors.some((s) => s.active && s.type === SimpleSelectorType.CLASS && (s as ClassSelector).value === cl)) {
        return
      }
      // Add the class
      suggestions.push({
        type: SimpleSelectorType.CLASS,
        value: cl,
        active: true,
      } as ClassSelector)
    })

  // Attributes
  components.forEach(component => {
    // Check if getAttributes method exists (for compatibility with mock components in tests)
    if (typeof component.getAttributes === 'function') {
      const attributes = component.getAttributes({ noStyle: true, noClass: true })
      Object.keys(attributes).forEach((attr) => {
        // Skip 'id' attribute - it should use ID selector instead of attribute selector
        if (attr === 'id') return

        // Only attributes not in the suggestions yet
        if (suggestions.some((s) => s.type === SimpleSelectorType.ATTRIBUTE && (s as AttributeSelector).value === attr)) {
          return
        }
        // Only attributes not in the related selector yet
        if (selector.selectors.some((s) => s.active && s.type === SimpleSelectorType.ATTRIBUTE && (s as AttributeSelector).value === attr)) {
          return
        }

        const s = {
          type: SimpleSelectorType.ATTRIBUTE,
          value: attr,
          active: true,
        } as AttributeSelector
        if (attributes[attr]) {
          s.operator = AttributeOperatorType.EQUALS
          s.attributeValue = attributes[attr]
        }
        suggestions.push(s)
      })
    }
  })

  return suggestions
}

// Add the component tag name
function addTagNames(component: Component, suggestions: SimpleSelector[], selector: CompoundSelector) {
  // Tag names - normalize to lowercase for CSS consistency
  const tagName = component.tagName.toLowerCase()
  const tagSelector = {
    type: SimpleSelectorType.TAG,
    value: tagName,
    active: true,
  }
  if (canIadd(tagSelector, selector, suggestions)) {
    suggestions.push(tagSelector)
  }
  // Special case of the HTML container
  if(tagName === 'body') {
    const htmlSelector = {
      type: SimpleSelectorType.TAG,
      value: 'html',
      active: true,
    }
    if (canIadd(htmlSelector, selector, suggestions)) {
      suggestions.push(htmlSelector)
    }
  }
  // // IDs
  // const id = component.getId()
  // const idSelector = {
  //   type: SimpleSelectorType.ID,
  //   value: id,
  //   active: true,
  // }
  // if (canIadd(idSelector, selector, suggestions)) {
  //   suggestions.push(idSelector as IdSelector)
  // }
}

// function sameType(gjs: Selector, sel: SimpleSelector): boolean {
//   return (gjs.get('type') === 1 && sel.type === SimpleSelectorType.CLASS) || (gjs.get('type') === 2 && sel.type === SimpleSelectorType.ID)
// }
//
// function sameValue(gjs: Selector, sel: SimpleSelector): boolean {
//   if(sel.type !== SimpleSelectorType.CLASS && sel.type !== SimpleSelectorType.ID) return false // Only classes and IDs are supported by grapesjs
//   return (gjs.get('name') === (sel as ClassSelector | IdSelector).value)
// }

function canIadd(sel: SimpleSelector, selector: CompoundSelector, suggestions: SimpleSelector[]): boolean {
  if(selector.selectors.some((s) => isSameSelector(s, sel))) {
    return false
  }
  if(suggestions.some((s) => isSameSelector(s, sel))) {
    return false
  }
  return true
}

function getChildren(component: Component): Component[] {
  return component
    .components()
    .reduce((acc, child) => [
      ...acc,
      child,
      ...getChildren(child)
    ], [] as Component[])
}

function getParents(component: Component): Component[] {
  const parents = []
  let parent = component.parent()
  while (parent) {
    parents.push(parent)
    parent = parent.parent()
  }
  return parents
}

function getSiblingsBefore(component: Component): Component[] {
  return component
    .parent()
    ?.components()
    .filter(c => c !== component) ?? []
}
