import { LitElement, html } from 'lit'
import { customElement, property, queryAsync } from 'lit/decorators.js'
import { ComponentStatesMixin } from '../ComponentStatesMixin'
import { EventEmitterMixin } from '../EventEmitterMixin'
import onFind from '../dom/onFind'
import onRemove from '../dom/onRemove'
import { v1 as uuid } from 'uuid'
import { focusRegionManager } from '../FocusRegions'

@customElement('btu-dialog')
export default class Dialog extends ComponentStatesMixin(
  EventEmitterMixin(LitElement),
) {
  static get tagName() {
    return 'BTU-DIALOG'
  }

  @queryAsync(':scope > dialog')
  private _dialog?: Promise<HTMLDialogElement>

  @property({ type: Boolean })
  private _unsavedChanges = false

  private _contextObserver?: MutationObserver
  private _popupElement?: HTMLElement

  /*
    Adding this functionality to our WC since it's not recommended
    to open native <dialog>s directly using their `open` attribute.
    See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/open
  */
  @property({ attribute: 'auto-open', type: Boolean })
  public autoOpen = true

  @property({ attribute: 'remove-on-close', type: Boolean })
  public removeOnClose = true

  @property({ type: String, reflect: true, attribute: 'enter-from' })
  public enterFrom: 'left' | 'right' | 'center' = 'center'

  @property({ type: Boolean, reflect: true })
  public loaded = false

  private set unsavedChanges(value: boolean) {
    const oldValue = this._unsavedChanges
    this._unsavedChanges = value
    if (oldValue !== value) {
      this.dispatchEvent(
        new CustomEvent('btu-dialog-unsaved-changes-state-changed', {
          detail: value,
          bubbles: true,
          composed: true,
        }),
      )
    }
  }

  private async _setupChangeDetection(form: HTMLFormElement) {
    // Check for changes...
    const hasStateChanges = () => {
      // First check the form itself
      if (form.classList.contains('state-changed')) {
        return true
      }

      // Then check for any descendants with state-changed
      if (form.querySelector('.state-changed')) {
        return true
      }

      return false
    }

    // Check overall state
    const updateChangeState = () => {
      const hasChanges = hasStateChanges()
      const isProcessing = form.classList.contains('is-processingFiles')

      this.unsavedChanges = hasChanges || isProcessing
    }

    // Watch for class changes that might indicate state changes
    const observer = new MutationObserver(() => {
      requestAnimationFrame(updateChangeState)
    })

    observer.observe(form, {
      attributes: true,
      attributeFilter: ['class'],
      subtree: true,
    })

    // Handle form removal
    // false positives can happen on post-back when a frame reloads without its original <form>.
    onRemove(form, () => {
      observer.disconnect()
      this.unsavedChanges = false
    })

    // Run initial state check
    updateChangeState()

    // Clean up when dialog is closed
    this.addEventListener(
      'btu-dialog-closed',
      () => {
        observer.disconnect()
      },
      { once: true },
    )
  }

  /**
   * Caches the Popup element reference for performance
   */
  private _cachePopupElement(): void {
    if (!this._popupElement) {
      this._popupElement =
        this.querySelector<HTMLElement>(':scope > dialog > .Popup') || undefined
    }
  }

  /**
   * Detects the Popup's enter direction and sets enterFrom property
   */
  private _detectEnterFrom(): void {
    if (!this._popupElement) return

    if (this._popupElement.classList.contains('is-fromRight')) {
      this.enterFrom = 'right'
    } else if (this._popupElement.classList.contains('is-fromLeft')) {
      this.enterFrom = 'left'
    } else {
      this.enterFrom = 'center'
    }
  }

  /**
   * Detects if the Popup is fully loaded and ready
   */
  private _detectLoadedState(): void {
    if (!this._popupElement) return

    // Check if Popup has all three classes indicating it's loaded
    const isLoaded =
      this._popupElement.classList.contains('loaded') &&
      this._popupElement.classList.contains('is-ready') &&
      this._popupElement.classList.contains('is-visible')

    this.loaded = isLoaded
  }

  /**
   * Checks if an element is relevant for context annotation
   */
  private _isRelevantElement(element: Element): boolean {
    const relevantSelectors =
      '.ActionBar, .Message, .Admin, form, .TC, .AIChat-wrapper, .fixedScrollable'

    return (
      element.matches(relevantSelectors) ||
      element.querySelector(relevantSelectors) !== null
    )
  }

  /**
   * Helper to find elements within a scope (either specific element or entire dialog)
   */
  private _findInScope(scope: Element | Document, selector: string): Element[] {
    const elements: Element[] = []

    if (scope instanceof Element && scope.matches(selector)) {
      elements.push(scope)
    }

    elements.push(...Array.from(scope.querySelectorAll(selector)))
    return elements
  }

  /**
   * Sets up MutationObserver to watch for dynamically added content
   */
  private _setupContextObserver(dialog: HTMLDialogElement): void {
    let annotationFrame: number
    const pendingTargets = new Set<Element>()

    this._contextObserver = new MutationObserver((mutations) => {
      let shouldCheckEnterFrom = false
      let shouldCheckLoadedState = false

      // Collect newly added elements that need annotation
      for (const mutation of mutations) {
        // Handle childList mutations (elements added/removed)
        if (mutation.type === 'childList') {
          for (const node of mutation.addedNodes) {
            if (node instanceof Element) {
              // Check if Popup was added
              if (
                node.matches('.Popup') ||
                node.querySelector('.Popup') !== null
              ) {
                // Clear cache so it gets refreshed
                this._popupElement = undefined
                this._cachePopupElement()
                shouldCheckEnterFrom = true
                shouldCheckLoadedState = true
              }

              if (this._isRelevantElement(node)) {
                pendingTargets.add(node)
              }
            }
          }
        }

        // Handle attribute mutations (class changes on Popup)
        if (
          mutation.type === 'attributes' &&
          mutation.attributeName === 'class'
        ) {
          const target = mutation.target
          if (target instanceof Element && target.matches('.Popup')) {
            shouldCheckLoadedState = true
          }
        }
      }

      // Re-detect enterFrom if Popup was added
      if (shouldCheckEnterFrom) {
        this._detectEnterFrom()
      }

      // Re-detect loaded state if Popup classes changed
      if (shouldCheckLoadedState) {
        this._detectLoadedState()
      }

      // Bail if no relevant changes
      if (pendingTargets.size === 0) return

      // Debounce annotation updates
      if (!annotationFrame) {
        annotationFrame = window.requestAnimationFrame(() => {
          pendingTargets.forEach((target) =>
            this._annotateDialogContext(target),
          )
          pendingTargets.clear()
          annotationFrame = 0
        })
      }
    })

    // Watch for new elements and class changes on Popup
    this._contextObserver.observe(dialog, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ['class'],
    })
  }

  /**
   * Marks ancestors with a class, stopping at boundary or if class already exists
   */
  private _markAncestors(
    element: Element,
    className: string,
    boundary: Element,
  ): void {
    let ancestor = element.parentElement
    while (ancestor && ancestor !== boundary) {
      if (ancestor.classList.contains(className)) break
      ancestor.classList.add(className)
      ancestor = ancestor.parentElement
    }
  }

  /**
   * Annotates dialog DOM with context classes to replace expensive :has() CSS selectors
   */
  private _annotateDialogContext(targetElement?: Element): void {
    const dialog = this.querySelector(':scope > dialog')
    if (!dialog) return

    const popupContent = dialog.querySelector('.Popup-content')
    if (!popupContent) return

    // Skip if already fully annotated
    if (!targetElement && popupContent.classList.contains('has-ActionBar')) {
      return
    }

    const scope = targetElement || dialog
    const isEditing = popupContent.classList.contains('is-editing')

    // Mark if Admin nav exists
    if (dialog.querySelector('.Admin .Admin-nav')) {
      popupContent.classList.add('has-AdminNav')
    }

    // Only proceed if not in editing mode and no Admin nav
    const hasAdminNav = popupContent.classList.contains('has-AdminNav')
    if (isEditing || hasAdminNav) return

    // Find and process ActionBars
    const actionBars = this._findInScope(scope, '.ActionBar')
    if (actionBars.length === 0) return

    popupContent.classList.add('has-ActionBar')

    // Mark ancestors of ActionBars that have substantive content
    actionBars.forEach((actionBar) => {
      const hasContent = actionBar.querySelector(
        ':scope > *:not(.ImageEditor-closeButton)',
      )
      if (hasContent) {
        this._markAncestors(actionBar, 'has-ActionBarContent', popupContent)
      }
    })

    // Mark ancestors of status messages
    const messages = this._findInScope(
      scope,
      '.Message.is-success, .Widget-title + .Message.is-error, .Widget-title + .Message.message-info, .PostPublish-form .Message.is-error, form[action*="translation/request"] > .Message',
    )
    messages.forEach((message) => {
      this._markAncestors(message, 'has-StatusMessage', dialog)
    })

    // Mark elements that appear immediately before an ActionBar
    const candidates = this._findInScope(
      scope,
      'form, .TC, .AIChat-wrapper, .fixedScrollable',
    )
    candidates.forEach((element) => {
      const isBeforeActionBar =
        element.nextElementSibling?.classList.contains('ActionBar')
      const isPrePublishScrollable =
        element.classList.contains('fixedScrollable') &&
        element.closest('.Widget.PrePublish')

      if (isBeforeActionBar && !isPrePublishScrollable) {
        element.classList.add('before-ActionBar')
      }
    })
  }

  private async _handleClose(e?: Event): Promise<void> {
    if (e?.defaultPrevented) return

    const dialog = await this._dialog
    if (!dialog) return

    // Check if we should prevent closing
    if (this._unsavedChanges) {
      if (!confirm(window.BRIGHTSPOT?.ui.tooltips.confirmPopupUnsaved)) {
        e?.preventDefault()
        return
      }
    }

    // Close dialog and perform cleanup
    dialog.close()
    this.emit('btu-dialog-closed')

    // remove focus regions associated with this dialog
    focusRegionManager.destroyLayer(dialog)

    // Support for legacy jquery popups
    if (this._popupElement) {
      window.jQuery?.(this._popupElement).trigger('close.popup')
    }

    // Remove this host element from the DOM if configured
    if (this.removeOnClose) {
      this.remove()
    }
  }

  async connectedCallback() {
    super.connectedCallback()

    this.id = `i${uuid()}`
  }

  async disconnectedCallback() {
    super.disconnectedCallback()

    // Clean up the context observer
    this._contextObserver?.disconnect()
    this._contextObserver = undefined

    // Clear cached popup element
    this._popupElement = undefined

    await this._dialog?.then((d) => {
      d.removeEventListener('cancel', this._handleClose)
      d.close()
    })
  }

  async firstUpdated() {
    // Wait for a form to be present in the dialog before setting up change detection
    onFind(`#${this.id} form`, (form: HTMLFormElement) => {
      this._setupChangeDetection(form)
    })

    await this._dialog?.then((d) => {
      /* Look for children of the dialog when it is attached and move them into the dialog. */
      const children = this.querySelector(
        ':scope > .Popup, :scope > .QueryField-frame, :scope > .ContentEdit-workflow, :scope > *:not(dialog)',
      )

      if (!children) return

      d.addEventListener('cancel', (e: Event) => {
        if (e.defaultPrevented) return
        this._handleClose(e)
      })

      d.appendChild(children)

      // Cache Popup element reference and detect its properties
      this._cachePopupElement()
      this._detectEnterFrom()
      this._annotateDialogContext()
      this._detectLoadedState()
      // Set up observer to handle dynamically added content
      this._setupContextObserver(d)
    })

    if (this.autoOpen) {
      // Special case to open global search as a non-modal because we don't want a scrim.
      if (this.querySelector(':scope > dialog > .Popup[name="miscSearch"')) {
        await this._dialog?.then((d) => d.show())
      } else {
        await this._dialog?.then((d) => d.showModal())
      }
    }

    this.emit('btu-dialog-ready')
  }

  createRenderRoot() {
    return this
  }

  public close(e?: Event): void {
    this._handleClose(e)
  }

  public show(): void {
    this._dialog?.then((d) => d.showModal())
  }

  render() {
    return html`<dialog></dialog>`
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'btu-dialog': Dialog
  }
}
