import { Component, Editor } from 'grapesjs'
import { html, render } from 'lit'
import { editStyle, getComponentSelector, getSelectedStyle, getSelectors, getSuggestionsMain, getSuggestionsRelated, getTranslation, getUntranslatedKeys, matchSelectorAll, matchSelectorSome, removeClass, renameSelector, setComponentSelector, setSelectedStyle } from './model/GrapesJsSelectors'
import { activateSelectors, ComplexSelector, EMPTY_SELECTOR, fromString, merge, toString, getSelector, noPseudo } from './model/ComplexSelector'
import './components/complex-selector'
import './components/current-selector-display'

// Debounce utility (no semicolons, matches code style)
function debounce(fn: (...args: any[]) => void, delay: number = 100) {
  let timer: ReturnType<typeof setTimeout>
  return function(this: any, ...args: any[]) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

////////////////
// Types
export type AdvancedSelectorOptions = {
  i18n: {
    [key: string]: string
  }
  helpLinks: {
    actionBar: string
  }
}

type CustomSelectorEventProps = {
  container: HTMLElement
}

////////////////
// Globals
const container = document.createElement('div')
container.id = 'asm-container'

////////////////
// Plugin functions
export function initListeners(editor: Editor, options: AdvancedSelectorOptions) {
  editor.Commands.add('i18n:info', () => {
    console.log('i18n', getUntranslatedKeys())
  })
  editor.on('undo redo', () => {
    requestAnimationFrame(() => updateUi(editor, options))
  })
  // editor.on('component:selected', () => {
  //   console.log('============> component:selected')
  //   requestAnimationFrame(() => updateUi(editor, options))
  // })
  // // When a class changes
  // editor.on('selector', (...args) => {
  //   console.log('============> component:classes', args)
  //   requestAnimationFrame(() => updateUi(editor, options))
  // })
  // editor.on('component:update:classes', (...args) => {
  //   console.log('============ component:update:classes', args)
  //   requestAnimationFrame(() => updateUi(editor, options)) // Delay to avoid flicker of warning
  //   // console.log("========== Component class list updated:", component, component.getC);
  //   // // Get the class that changed
  //   // const changedClass = component
  //   //   .getClasses()
  //   //   .filter((cls) => component.previous('classes') !== component.get(cls))[0];
  //   // const classNames = component.getClasses()
  //   // console.log("============ Updated class names:", classNames);
  // })
}

export function initASM(editor: Editor, options: AdvancedSelectorOptions, props?: CustomSelectorEventProps) {
  if (props && props.container) {
    props.container.appendChild(container)
    editor.on('selector:custom component:update', debounce(() => updateUi(editor, options)))
  } else {
    // Keep listening
    editor.once('selector:custom', (props) => initASM(editor, options, props))
  }
}

function updateUi(editor: Editor, options: AdvancedSelectorOptions) {
  const components: Component[] = editor.getSelectedAll()
  const widthMedia = editor.DeviceManager.getSelected()?.get('widthMedia')
  const selectorInitial = getSelector(components)
  if(selectorInitial) {
    const selector = {
      ...selectorInitial,
      atRule: widthMedia ? `@media (max-width: ${widthMedia})` : undefined,
    }
    const [errors, warnings] = getErrorsAndWarnings(selector, components)
    requestAnimationFrame(() => editStyle(editor, toString(selector)))
    render(html`
      <current-selector-display
        .t=${(key: string) => getTranslation(editor, key)}
        .value=${selector || undefined}
        .selectors=${getSelectors(editor)}
        .helpLink=${options.helpLinks.actionBar}
        .error=${getTranslation(editor, errors || '')}
        .warning=${getTranslation(editor, warnings || '')}
        @change=${(event: CustomEvent) => mergeSelector(event.detail as ComplexSelector, editor, components)}
        @delete=${() => deleteSelector(editor, selector)}
        @copy=${() => copyStyle(editor)}
        @paste=${() => pasteStyle(editor)}
      ></current-selector-display>
      <complex-selector
        .t=${(key: string) => getTranslation(editor, key)}
        .hasBody=${components.some(c => c.tagName.toLocaleLowerCase() === 'body')}
        .value=${selector}
        .suggestions=${getSuggestionsMain(editor, components, selector)}
        .relations=${getSuggestionsRelated(editor, components, selector)}
        @change=${(event: CustomEvent) => changedSelector(event.detail as ComplexSelector, editor, components)}
        @rename=${(event: CustomEvent) => renameSelector(editor, event.detail.oldValue, event.detail.value) }
        @removeClass=${(event: CustomEvent) => removeClass(editor, event.detail) }
      ></complex-selector>
    `, container)
  } else {
    render(html`
      <p>Select a component to edit its selector</p>
    `, container)
  }
}

function changedSelector(selector: ComplexSelector, editor: Editor, components: Component[]) {
  const isBody = components.some(c => c.tagName?.toLowerCase() === 'body')
  // For body: body tag and ID are mutually exclusive
  if (isBody) {
    const bodyTagActive = selector.mainSelector.selectors
      .some(s => s.type === 'tag' && (s as any).value === 'body' && s.active)
    if (bodyTagActive) {
      selector = {
        ...selector,
        mainSelector: {
          ...selector.mainSelector,
          selectors: selector.mainSelector.selectors.map(s =>
            s.type === 'id' ? { ...s, active: false } : s
          ),
        },
      }
    }
  }
  components.forEach((component) => {
    setComponentSelector(component, selector)
  })
  editStyle(editor, toString(selector))
}

/**
 * Merge the selector with the current selector of the components
 * If a simple selector is missing, just deactivate it
 * Handles the atRule and pseudoClass
 */
function mergeSelector(selector: ComplexSelector, editor: Editor, components: Component[]) {
  components.forEach((component) => {
    const oldSelector = getComponentSelector(component) || EMPTY_SELECTOR
    const newSelector = merge(oldSelector, selector)
    const activated = activateSelectors(newSelector, selector)
    activated.atRule = selector.atRule // We merged but want to keep the atRule
    activated.mainSelector.pseudoClass = selector.mainSelector.pseudoClass // We merged but want to keep the pseudoClass
    if (activated.relatedSelector) activated.relatedSelector.pseudoClass = selector.relatedSelector?.pseudoClass // We merged but want to keep the pseudoClass
    setComponentSelector(component, activated)
  })
  // Select the device if the selector contains the device
  if(selector.atRule) {
    const device = editor.DeviceManager.getAll().find(device => {
      const width = device.get('widthMedia')
      return !!width && selector.atRule!.includes(width)
    })?.get('id')
    if(device) editor.DeviceManager.select(device)
  } else {
    editor.DeviceManager.select(editor.DeviceManager.getAll().first())
  }
  editStyle(editor, toString(selector))
}

/**
 * Deletes all styles associated with the current selector from the editor.
 * Removes rules for the base selector as well as any pseudo-class or
 * relation variants (e.g. `.foo:hover`, `.foo:has(.bar)`), across all
 * media queries.
 */
function deleteSelector(editor: Editor, selector: ComplexSelector) {
  const selectorString = toString(selector)
  if (!selectorString) return

  // Find the rule that exactly matches the current selector (including pseudo-class/relation)
  const toRemove = editor.CssComposer.getRules().filter((rule: any) => {
    const selStr = rule.getSelectorsString()
    if (!selStr) return false
    try {
      const complex = fromString(selStr, rule.getAtRule())
      const ruleString = toString(complex)
      return ruleString === selectorString
    } catch {
      return false
    }
  })

  toRemove.forEach((rule: any) => {
    // Clear styles before removing — getRule() keeps a stale cached reference
    // after remove(), so editStyle() would re-select the ghost rule with old styles
    rule.setStyle({})
    editor.CssComposer.remove(rule)
  })

  editStyle(editor, toString(selector))
}

function copyStyle(editor: Editor) {
  localStorage?.setItem('asm:clipboard', JSON.stringify(getSelectedStyle(editor)))
}

function pasteStyle(editor: Editor) {
  const clipboard = localStorage?.getItem('asm:clipboard')
  if(clipboard) {
    const style = JSON.parse(clipboard)
    setSelectedStyle(editor, style)
  }
}

function getErrorsAndWarnings(selector: ComplexSelector, components: Component[]): [string | null, string | null] {
  const selectorNoPseudo: ComplexSelector = noPseudo(selector)
  if (!matchSelectorAll(toString(selectorNoPseudo, true), components)) {
    return ['Current selector does not match the selected elements', null]
  } else if (!matchSelectorSome(toString(selectorNoPseudo, true), components)) {
    return [null, 'Current selector does not match all the selected elements']
  }
  return [null, null]
}
