import { html, LitElement, PropertyValues, TemplateResult, nothing } from 'lit'
import { property, query, state } from 'lit/decorators.js'
import { classMap } from 'lit/directives/class-map.js'
import { ifDefined } from 'lit/directives/if-defined.js'
import reactive from '../../../decorators/reactive.js'
import '../../../md/chip/ui-chip.js'

export interface MentionSuggestion {
  /** Unique identifier for the suggestion */
  id: string
  /** Main label/headline displayed */
  label: string
  /** Supporting description text */
  description?: string
  /** Suffix text (e.g., role, department) */
  suffix?: string
  /** Additional data associated with the suggestion */
  data?: Record<string, unknown>
}

export interface MentionInsertEvent {
  /** The inserted mention suggestion */
  suggestion: MentionSuggestion
  /** The text that triggered the mention (e.g., "@john") */
  trigger: string
  /** The position where the mention was inserted */
  position: number
}

export interface MentionRemoveEvent {
  /** The removed mention suggestion */
  suggestion: MentionSuggestion
  /** The position where the mention was removed from */
  position: number
}

interface Fragment {
  type: 'text' | 'mention'
  content: string
  mentionId?: string
}

/**
 * A NodeFilter mask that includes text nodes and elements.
 * It is used with the NodeWalker
 */
const ShowMask = NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT

/**
 * A material design textarea component that supports @mentions with suggestions.
 *
 * Features:
 * - Material Design styling
 * - @mention triggers with customizable suggestions
 * - Inline pill/chip rendering for mentions
 * - Proper caret management
 * - Keyboard navigation for suggestions
 * - Overflow container support
 * - Generic design for extensibility
 * - Accessibility support
 *
 * @fires mention-insert - When a mention is inserted
 * @fires mention-remove - When a mention is removed
 * @fires input - When the input value changes
 * @fires change - When the input loses focus and value has changed
 */
export default class MentionTextArea extends LitElement {
  /**
   * Shadow root configuration for the component.
   * Uses 'open' mode for accessibility and delegates focus to enable proper focus management.
   */
  static override shadowRootOptions: ShadowRootInit = {
    mode: 'open',
    delegatesFocus: true,
  }

  /**
   * The label text displayed as placeholder/floating label
   */
  @property({ type: String })
  accessor label = ''

  /**
   * Supporting text displayed below the input
   */
  @property({ type: String, attribute: 'supporting-text' })
  accessor supportingText = ''

  /**
   * Whether the component is disabled
   */
  @property({ type: Boolean, reflect: true })
  accessor disabled = false

  /**
   * Whether the component is in an invalid state
   */
  @property({ type: Boolean, reflect: true })
  accessor invalid = false

  /**
   * The name attribute for form integration
   */
  @property({ type: String })
  accessor name = ''

  /**
   * Whether the input is required
   */
  @property({ type: Boolean })
  accessor required = false

  /**
   * Placeholder text shown when input is empty
   */
  @property({ type: String })
  accessor placeholder = ''

  get value(): string {
    return this._value
  }

  @property({ type: String })
  set value(newValue: string) {
    const oldValue = this._value
    if (newValue === this._value) return // No change, skip update
    this._value = newValue
    this.requestUpdate('value', oldValue)
    // Only sync editor from value when set externally and element is ready
    if (this.editorElement) {
      this.syncEditorFromValue()
    }
  }

  /**
   * Available suggestions for mentions
   */
  @property({ type: Array, attribute: false })
  accessor suggestions: MentionSuggestion[] = []

  /**
   * Character that triggers mention suggestions (default: '@')
   */
  @property({ type: String, attribute: 'mention-trigger' })
  accessor mentionTrigger = '@'

  /**
   * Minimum characters after trigger to show suggestions
   */
  @property({ type: Number, attribute: 'min-query-length' })
  accessor minQueryLength = 0

  @query('.editor')
  private accessor editorElement!: HTMLDivElement

  @query('.suggestions-popover')
  private accessor suggestionsPopover!: HTMLElement

  @state()
  @reactive()
  private accessor isShowingSuggestions = false

  @reactive()
  private accessor filteredSuggestions: MentionSuggestion[] = []

  @state()
  private accessor selectedSuggestionIndex = 0

  @state()
  private accessor hasContent = false

  @state()
  private accessor isLabelFloating = false

  @state()
  private accessor isEditorFocus = false

  private currentMentionQuery = ''
  private currentMentionStart = -1
  private mentionMap = new Map<string, MentionSuggestion>()
  private mutationObserver?: MutationObserver
  private _value = '' // Internal value storage

  /**
   * A read-only list of the names of the fields currently mentioned in the textarea.
   */
  get mentions(): string[] {
    const dependencies = new Set<string>()
    if (!this.value) {
      return []
    }
    const regex = /@\{([^}]+)\}/g
    let match: RegExpExecArray | null
    while ((match = regex.exec(this.value)) !== null) {
      dependencies.add(match[1])
    }
    return Array.from(dependencies)
  }

  override connectedCallback(): void {
    super.connectedCallback()

    // Setup mutation observer to watch for changes in the editor
    this.mutationObserver = new MutationObserver(() => {
      this.syncValueFromEditor()
    })
  }

  override disconnectedCallback(): void {
    super.disconnectedCallback()
    this.mutationObserver?.disconnect()
  }

  override firstUpdated(): void {
    // Start observing mutations in the editor
    if (this.editorElement && this.mutationObserver) {
      this.mutationObserver.observe(this.editorElement, {
        childList: true,
        subtree: true,
        characterData: true,
      })
    }

    // Setup initial content
    this.updateComplete.then(() => {
      this.syncEditorFromValue()
    })
  }

  override willUpdate(changedProperties: PropertyValues): void {
    super.willUpdate(changedProperties)

    // No need to sync editor from value here since setter handles it

    if (changedProperties.has('suggestions')) {
      this.updateFilteredSuggestions()
    }
    if (changedProperties.has('selectedSuggestionIndex')) {
      const index = this.selectedSuggestionIndex
      if (index >= 0) {
        this.suggestionsPopover?.querySelector(`.suggestion-item:nth-child(${index + 1})`)?.scrollIntoView({
          behavior: 'smooth',
          block: 'nearest',
        })
      }
    }
  }

  /**
   * Get the plain text content without field references
   * @returns The plain text content with field names instead of references
   */
  public getPlainText(): string {
    let text = this.value

    // Replace field references with their display names
    text = text.replace(/@\{([^}]+)\}/g, (match, fieldId) => {
      const field = this.suggestions.find((f) => f.id === fieldId)
      return field ? field.label : fieldId
    })

    return text
  }

  /**
   * Sets the caret position at the beginning of a specific text node
   */
  private setCaretPositionAtTextNode(textNode: Text, offset = 0): void {
    const selection = window.getSelection()
    if (!selection) return

    const range = document.createRange()
    range.setStart(textNode, offset)
    range.setEnd(textNode, offset)

    selection.removeAllRanges()
    selection.addRange(range)
  }

  /**
   * Synchronizes the value property from the editor content
   */
  private syncValueFromEditor(): void {
    const newValue = this.getValueFromEditor()
    if (newValue !== this._value) {
      // Update internal value directly to avoid triggering setter
      this._value = newValue
      this.requestUpdate('value')
      this.dispatchEvent(new Event('input', { bubbles: true }))
    }

    this.updateContentState()
  }

  /**
   * Gets the value from editor content, converting chips to @{mention} format
   */
  private getValueFromEditor(): string {
    const childNodes = Array.from(this.editorElement.childNodes)
    let result = ''

    for (const node of childNodes) {
      if (node.nodeType === Node.TEXT_NODE) {
        result += node.textContent || ''
      } else if (node.nodeType === Node.ELEMENT_NODE) {
        const element = node as Element
        if (element.classList.contains('mention-chip')) {
          const mentionId = element.getAttribute('data-mention-id')
          if (mentionId) {
            result += `@{${mentionId}}`
          }
        } else {
          result += element.textContent || ''
        }
      }
    }

    return result
  }

  /**
   * Synchronizes the editor content from the value property
   */
  private syncEditorFromValue(): void {
    if (!this.editorElement) return

    const fragments = this.parseValueToFragments(this._value)
    this.editorElement.innerHTML = ''

    for (const fragment of fragments) {
      if (fragment.type === 'text') {
        this.editorElement.appendChild(document.createTextNode(fragment.content))
      } else if (fragment.type === 'mention') {
        if (fragment.mentionId) {
          let mention = this.mentionMap.get(fragment.mentionId)

          // If mention not in map, try to find it in suggestions
          if (!mention) {
            mention = this.suggestions.find((s) => s.id === fragment.mentionId)
            if (mention) {
              // Add to map for future use
              this.mentionMap.set(mention.id, mention)
            }
          }

          if (mention) {
            const chipElement = this.createMentionChip(mention)
            this.editorElement.appendChild(chipElement)
          } else {
            // Fallback to text if mention not found anywhere
            this.editorElement.appendChild(document.createTextNode(`@{${fragment.mentionId}}`))
          }
        }
      }
    }

    this.updateContentState()
  }

  /**
   * Parses value string into text and mention fragments
   */
  private parseValueToFragments(value: string): Fragment[] {
    const fragments: Fragment[] = []
    const mentionRegex = /@\{([^}]+)\}/g
    let lastIndex = 0
    let match

    while ((match = mentionRegex.exec(value)) !== null) {
      // Add text before mention
      if (match.index > lastIndex) {
        fragments.push({
          type: 'text',
          content: value.substring(lastIndex, match.index),
        })
      }

      // Add mention
      fragments.push({
        type: 'mention',
        content: match[0],
        mentionId: match[1],
      })

      lastIndex = mentionRegex.lastIndex
    }

    // Add remaining text
    if (lastIndex < value.length) {
      fragments.push({
        type: 'text',
        content: value.substring(lastIndex),
      })
    }

    return fragments
  }

  /**
   * Creates a mention chip element
   */
  private createMentionChip(mention: MentionSuggestion): HTMLElement {
    const wrapper = document.createElement('span')
    wrapper.className = 'mention-chip'
    wrapper.setAttribute('data-mention-id', mention.id)
    wrapper.setAttribute('contenteditable', 'false')

    const chip = document.createElement('ui-chip')
    chip.setAttribute('type', 'input')
    chip.setAttribute('removable', 'true')
    chip.textContent = mention.label
    chip.addEventListener('remove', () => {
      this.removeMention(wrapper, mention)
    })

    wrapper.appendChild(chip)
    return wrapper
  }

  /**
   * Updates content state flags
   */
  private updateContentState(): void {
    const hasAnyContent =
      this.editorElement.childNodes.length > 0 && (this.editorElement.textContent?.trim().length || 0) > 0

    this.hasContent = hasAnyContent
    this.isLabelFloating = hasAnyContent || this.isShowingSuggestions || this.isEditorFocus
  }

  /**
   * Handles input events in the editor
   */
  private handleEditorInput(event: Event): void {
    event.stopPropagation()

    // Check for mention trigger
    this.checkForMentionTrigger()

    // Sync value
    this.syncValueFromEditor()
  }

  /**
   * Handles keydown events in the editor
   */
  private handleEditorKeyDown(event: KeyboardEvent): void {
    if (this.isShowingSuggestions) {
      this.handleSuggestionKeyDown(event)
    }
  }

  /**
   * Handles keydown events when suggestions are visible
   */
  private handleSuggestionKeyDown(event: KeyboardEvent): void {
    switch (event.key) {
      case 'ArrowUp':
        event.preventDefault()
        this.selectedSuggestionIndex = Math.max(0, this.selectedSuggestionIndex - 1)
        break
      case 'ArrowDown':
        event.preventDefault()
        this.selectedSuggestionIndex = Math.min(this.filteredSuggestions.length - 1, this.selectedSuggestionIndex + 1)
        break
      case 'Enter':
      case 'Tab':
        event.preventDefault()
        this.selectSuggestion(this.filteredSuggestions[this.selectedSuggestionIndex])
        break
      case 'Escape':
        event.preventDefault()
        this.hideSuggestions()
        break
    }
  }

  /**
   * Handles focus events
   */
  private handleEditorFocus(): void {
    this.isEditorFocus = true
    this.updateContentState()
  }

  /**
   * Handles blur events
   */
  private handleEditorBlur(event: FocusEvent): void {
    // Don't hide suggestions if focus moved to suggestions popover
    if (event.relatedTarget && this.suggestionsPopover?.contains(event.relatedTarget as Node)) {
      return
    }

    this.isEditorFocus = false
    this.hideSuggestions()
    this.updateContentState()
    this.dispatchEvent(new Event('change', { bubbles: true }))
  }

  private handleEditorPaste(event: ClipboardEvent): void {
    // Prevent default paste behavior to handle mentions properly
    event.preventDefault()

    const pastedContent = event.clipboardData?.getData('text/plain')
    if (!pastedContent) return

    const selection = window.getSelection()
    if (!selection || selection.rangeCount === 0) return

    // Get the current range
    let range: Range | StaticRange
    if ('getComposedRanges' in selection && typeof selection.getComposedRanges === 'function' && this.shadowRoot) {
      const composedRanges = selection.getComposedRanges({ shadowRoots: [this.shadowRoot] })
      if (composedRanges.length === 0) return
      range = composedRanges[0]
    } else {
      range = selection.getRangeAt(0)
    }

    // Convert StaticRange to Range if needed for deleteContents
    let workingRange: Range
    if (range instanceof StaticRange) {
      workingRange = document.createRange()
      workingRange.setStart(range.startContainer, range.startOffset)
      workingRange.setEnd(range.endContainer, range.endOffset)
    } else {
      workingRange = range
    }

    // Delete the current selection (if any)
    workingRange.deleteContents()

    // Insert the pasted text at the current position
    const textNode = document.createTextNode(pastedContent)
    workingRange.insertNode(textNode)

    // Position cursor at the end of the inserted text
    workingRange.setStartAfter(textNode)
    workingRange.setEndAfter(textNode)
    selection.removeAllRanges()
    selection.addRange(workingRange)

    // Hide suggestions if showing
    this.hideSuggestions()

    // Sync value and trigger events
    this.syncValueFromEditor()
  }

  /**
   * Checks if the current caret position indicates a mention trigger
   */
  private checkForMentionTrigger(): void {
    const selection = window.getSelection()
    if (!selection || selection.rangeCount === 0) {
      this.hideSuggestions()
      return
    }

    // Use getComposedRanges() for shadow DOM support, fallback to getRangeAt()
    let range: Range | StaticRange
    if ('getComposedRanges' in selection && typeof selection.getComposedRanges === 'function' && this.shadowRoot) {
      const composedRanges = selection.getComposedRanges({ shadowRoots: [this.shadowRoot] })
      if (composedRanges.length === 0) {
        this.hideSuggestions()
        return
      }
      range = composedRanges[0]
    } else {
      range = selection.getRangeAt(0)
    }

    const caretContainer = range.startContainer
    const caretOffset = range.startOffset

    // Ensure the caret is within our editor element
    if (!this.editorElement.contains(caretContainer)) {
      this.hideSuggestions()
      return
    }

    // Only look for triggers in text nodes
    if (caretContainer.nodeType !== Node.TEXT_NODE) {
      this.hideSuggestions()
      return
    }

    const textContent = caretContainer.textContent || ''

    // Look backwards from caret to find mention trigger
    let mentionStart = -1
    for (let i = caretOffset - 1; i >= 0; i--) {
      const char = textContent[i]
      if (char === this.mentionTrigger) {
        mentionStart = i
        break
      }
      if (char === ' ' || char === '\n') {
        break
      }
    }
    if (mentionStart >= 0) {
      const query = textContent.substring(mentionStart + 1, caretOffset)

      if (query.length >= this.minQueryLength) {
        this.currentMentionQuery = query
        this.currentMentionStart = this.getGlobalTextPosition(caretContainer, mentionStart)
        this.showSuggestions()
        this.updateFilteredSuggestions()
      } else {
        this.hideSuggestions()
      }
    } else {
      this.hideSuggestions()
    }
  }

  private walkerNodeFilter(node: Node): number {
    if (node.nodeType === Node.TEXT_NODE) {
      return NodeFilter.FILTER_ACCEPT
    } else if (node.nodeType === Node.ELEMENT_NODE) {
      const element = node as Element
      if (element.classList.contains('mention-chip')) {
        // Accept mention chips as single characters
        return NodeFilter.FILTER_ACCEPT
      } else if (element.parentElement?.classList.contains('mention-chip')) {
        // It's a chip, ignore the entire tree
        return NodeFilter.FILTER_REJECT
      }
    }
    return NodeFilter.FILTER_SKIP
  }

  /**
   * Gets the global text position within the editor for a position within a text node
   */
  private getGlobalTextPosition(textNode: Node, offsetInNode: number): number {
    let position = 0
    const walker = document.createTreeWalker(this.editorElement, ShowMask, this.walkerNodeFilter)

    let node = walker.nextNode()
    while (node && node !== textNode) {
      if (node.nodeType === Node.TEXT_NODE) {
        position += node.textContent?.length || 0
      } else if (node.nodeType === Node.ELEMENT_NODE) {
        const element = node as Element
        if (element.classList?.contains('mention-chip')) {
          position += 1 // Count chip as 1 character
        }
      }
      node = walker.nextNode()
    }

    return position + offsetInNode
  }

  /**
   * Finds the DOM node and offset that corresponds to a global text position
   */
  private findNodeAtGlobalPosition(globalPosition: number): { node: Node; offset: number } | null {
    let currentPosition = 0
    const walker = document.createTreeWalker(this.editorElement, ShowMask, this.walkerNodeFilter)

    let node = walker.nextNode()
    while (node) {
      if (node.nodeType === Node.TEXT_NODE) {
        const textLength = node.textContent?.length || 0
        if (currentPosition + textLength >= globalPosition) {
          return {
            node,
            offset: globalPosition - currentPosition,
          }
        }
        currentPosition += textLength
      } else if (node.nodeType === Node.ELEMENT_NODE) {
        const element = node as Element
        if (element.classList?.contains('mention-chip')) {
          if (currentPosition === globalPosition) {
            // Position is right before the chip
            const parentNode = element.parentNode
            if (parentNode) {
              return {
                node: parentNode,
                offset: Array.from(parentNode.childNodes).indexOf(element),
              }
            }
          }
          currentPosition += 1 // Count chip as 1 character
        }
      }
      node = walker.nextNode()
    }

    // Position is beyond the end, return the last position
    const lastChild = this.editorElement.lastChild
    if (lastChild) {
      if (lastChild.nodeType === Node.TEXT_NODE) {
        return {
          node: lastChild,
          offset: lastChild.textContent?.length || 0,
        }
      } else {
        return {
          node: this.editorElement,
          offset: this.editorElement.childNodes.length,
        }
      }
    }

    return null
  }

  /**
   * Shows the suggestions popover
   */
  private showSuggestions(): void {
    if (!this.isShowingSuggestions) {
      this.isShowingSuggestions = true
      this.selectedSuggestionIndex = 0
      this.updateContentState()

      // Use Popover API to show the popover
      this.updateComplete.then(() => {
        if (this.suggestionsPopover) {
          this.suggestionsPopover.showPopover()
          this.positionSuggestions()
        }
      })
    }
  }

  /**
   * Hides the suggestions popover
   */
  private hideSuggestions(): void {
    if (this.isShowingSuggestions) {
      this.isShowingSuggestions = false
      this.currentMentionQuery = ''
      this.currentMentionStart = -1
      this.updateContentState()

      // Use Popover API to hide the popover
      if (this.suggestionsPopover) {
        this.suggestionsPopover.hidePopover()
      }
    }
  }

  /**
   * Updates filtered suggestions based on current query
   */
  private updateFilteredSuggestions(): void {
    if (!this.currentMentionQuery) {
      this.filteredSuggestions = this.suggestions.slice(0, 10) // Limit to 10
      return
    }

    const query = this.currentMentionQuery.toLowerCase()
    this.filteredSuggestions = this.suggestions
      .filter(
        (suggestion) =>
          suggestion.label.toLowerCase().includes(query) ||
          suggestion.description?.toLowerCase().includes(query) ||
          suggestion.suffix?.toLowerCase().includes(query)
      )
      .slice(0, 10) // Limit to 10

    // Reset selection if needed
    if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
      this.selectedSuggestionIndex = 0
    }
  }

  /**
   * Positions the suggestions popover using the Popover API anchor positioning
   */
  private positionSuggestions(): void {
    if (!this.suggestionsPopover) return

    const selection = window.getSelection()
    if (!selection || selection.rangeCount === 0) return

    // Use getComposedRanges() for shadow DOM support, fallback to getRangeAt()
    let range: Range | StaticRange
    if ('getComposedRanges' in selection && typeof selection.getComposedRanges === 'function' && this.shadowRoot) {
      const composedRanges = selection.getComposedRanges({ shadowRoots: [this.shadowRoot] })
      if (composedRanges.length === 0) return
      range = composedRanges[0]
    } else {
      range = selection.getRangeAt(0)
    }

    // Convert StaticRange to Range if needed for getBoundingClientRect
    let workingRange: Range
    if (range instanceof StaticRange) {
      workingRange = document.createRange()
      workingRange.setStart(range.startContainer, range.startOffset)
      workingRange.setEnd(range.endContainer, range.endOffset)
    } else {
      workingRange = range
    }

    const rect = workingRange.getBoundingClientRect()

    // Position suggestions below the caret using manual positioning as fallback
    // The Popover API will handle proper layering and overflow handling
    this.suggestionsPopover.style.left = `${rect.left}px`
    this.suggestionsPopover.style.top = `${rect.bottom + 4}px`
  }

  /**
   * Selects a suggestion and inserts it as a mention
   */
  private selectSuggestion(suggestion: MentionSuggestion): void {
    if (!suggestion) return

    // Add to mention map
    this.mentionMap.set(suggestion.id, suggestion)

    // Find the position where the mention trigger starts
    const mentionStart = this.currentMentionStart

    // Find the DOM node and offset for the mention start position
    const mentionStartInfo = this.findNodeAtGlobalPosition(mentionStart)

    if (!mentionStartInfo) {
      // Fallback: insert at the end
      const chip = this.createMentionChip(suggestion)
      const afterTextNode = document.createTextNode('')
      this.editorElement.appendChild(chip)
      this.editorElement.appendChild(afterTextNode)
      this.setCaretPositionAtTextNode(afterTextNode, 0)
      this.hideSuggestions()
      this.syncValueFromEditor()
      this.dispatchEvent(
        new CustomEvent<MentionInsertEvent>('mention-insert', {
          detail: {
            suggestion,
            trigger: this.mentionTrigger + this.currentMentionQuery,
            position: mentionStart,
          },
          bubbles: true,
        })
      )
      return
    }

    // If the target is not a text node, insert at the container level
    if (mentionStartInfo.node.nodeType !== Node.TEXT_NODE) {
      const chip = this.createMentionChip(suggestion)
      const container = mentionStartInfo.node
      const beforeNode = container.childNodes[mentionStartInfo.offset]

      // Create an empty text node for caret positioning
      const afterTextNode = document.createTextNode('')

      if (beforeNode) {
        container.insertBefore(chip, beforeNode)
        container.insertBefore(afterTextNode, beforeNode)
      } else {
        container.appendChild(chip)
        container.appendChild(afterTextNode)
      }

      // Position caret at the beginning of the after text node
      this.setCaretPositionAtTextNode(afterTextNode, 0)
      this.hideSuggestions()
      this.syncValueFromEditor()
      this.dispatchEvent(
        new CustomEvent<MentionInsertEvent>('mention-insert', {
          detail: {
            suggestion,
            trigger: this.mentionTrigger + this.currentMentionQuery,
            position: mentionStart,
          },
          bubbles: true,
        })
      )
      return
    }

    // We have a text node - split it and insert the chip
    const textNode = mentionStartInfo.node as Text
    const splitOffset = mentionStartInfo.offset

    // Create the mention chip
    const chip = this.createMentionChip(suggestion)

    // Split the text node at the mention start position
    const beforeText = textNode.textContent?.substring(0, splitOffset) || ''
    const afterText = textNode.textContent?.substring(splitOffset + 1) || '' // +1 to skip the trigger

    // Replace the original text node with the split content
    const parentNode = textNode.parentNode
    if (parentNode) {
      // Create new text nodes - always create afterTextNode even if empty
      const beforeTextNode = beforeText ? document.createTextNode(beforeText) : null
      const afterTextNode = document.createTextNode(afterText) // Always create, even if empty

      // Insert the nodes in order
      if (beforeTextNode) {
        parentNode.insertBefore(beforeTextNode, textNode)
      }
      parentNode.insertBefore(chip, textNode)
      parentNode.insertBefore(afterTextNode, textNode)

      // Remove the original text node
      parentNode.removeChild(textNode)

      // Position caret at the beginning of the after text node
      this.setCaretPositionAtTextNode(afterTextNode, 0)
    } else {
      // Fallback: append to editor and create a text node for caret positioning
      const afterTextNode = document.createTextNode('')
      this.editorElement.appendChild(chip)
      this.editorElement.appendChild(afterTextNode)
      this.setCaretPositionAtTextNode(afterTextNode, 0)
    }

    // Hide suggestions
    this.hideSuggestions()

    // Sync value and dispatch events
    this.syncValueFromEditor()
    this.dispatchEvent(
      new CustomEvent<MentionInsertEvent>('mention-insert', {
        detail: {
          suggestion,
          trigger: this.mentionTrigger + this.currentMentionQuery,
          position: mentionStart,
        },
        bubbles: true,
      })
    )
  }

  /**
   * Removes a mention chip
   */
  private removeMention(chipElement: HTMLElement, mention: MentionSuggestion): void {
    const position = this.getElementPosition(chipElement)
    chipElement.remove()

    this.syncValueFromEditor()
    this.dispatchEvent(
      new CustomEvent<MentionRemoveEvent>('mention-remove', {
        detail: {
          suggestion: mention,
          position,
        },
        bubbles: true,
      })
    )
  }

  /**
   * Gets the text position of an element
   */
  private getElementPosition(element: HTMLElement): number {
    let position = 0
    let currentNode = this.editorElement.firstChild

    while (currentNode && currentNode !== element) {
      if (currentNode.nodeType === Node.TEXT_NODE) {
        position += currentNode.textContent?.length || 0
      } else if (currentNode.nodeType === Node.ELEMENT_NODE) {
        const element = currentNode as Element
        if (element.classList?.contains('mention-chip')) {
          position += 1 // Count chip as 1 character
        }
      }
      currentNode = currentNode.nextSibling
    }

    return position
  }

  /**
   * Handles clicking on a suggestion
   */
  private handleSuggestionClick(suggestion: MentionSuggestion, event: Event): void {
    event.preventDefault()
    this.selectSuggestion(suggestion)
    this.focus()
  }

  /**
   * Renders a suggestion item
   */
  private renderSuggestion(suggestion: MentionSuggestion, index: number): TemplateResult {
    const isSelected = index === this.selectedSuggestionIndex
    const classes = classMap({
      'suggestion-item': true,
      'selected': isSelected,
    })

    return html`
      <button
        class="${classes}"
        @click="${(e: Event) => this.handleSuggestionClick(suggestion, e)}"
        @mouseenter="${() => (this.selectedSuggestionIndex = index)}"
      >
        <div class="suggestion-content">
          <div class="suggestion-headline">${suggestion.label}</div>
          ${suggestion.description
            ? html` <div class="suggestion-supporting-text">${suggestion.description}</div> `
            : nothing}
        </div>
        ${suggestion.suffix ? html` <div class="suggestion-suffix">${suggestion.suffix}</div> ` : nothing}
      </button>
    `
  }

  /**
   * Renders the suggestions popover
   */
  private renderSuggestions(): TemplateResult {
    return html`
      <div class="suggestions-popover" popover="manual">
        ${this.isShowingSuggestions && this.filteredSuggestions.length > 0
          ? this.filteredSuggestions.map((suggestion, index) => this.renderSuggestion(suggestion, index))
          : nothing}
      </div>
    `
  }

  override render(): TemplateResult {
    const surfaceClasses = classMap({
      'surface': true,
      'has-content': this.hasContent,
    })

    const labelClasses = classMap({
      label: true,
      floating: this.isLabelFloating,
    })

    return html`
      <div class="${surfaceClasses}">
        <div class="container"></div>
        <div class="content">
          <div class="body">
            ${this.label ? html`<div class="${labelClasses}">${this.label}</div>` : nothing}
            <div
              class="editor"
              contenteditable="${!this.disabled}"
              data-placeholder="${ifDefined(this.placeholder)}"
              @input="${this.handleEditorInput}"
              @keydown="${this.handleEditorKeyDown}"
              @focus="${this.handleEditorFocus}"
              @blur="${this.handleEditorBlur}"
              @paste="${this.handleEditorPaste}"
              role="textbox"
              aria-label="${ifDefined(this.label)}"
              aria-multiline="true"
              aria-required="${this.required}"
              aria-invalid="${this.invalid}"
              tabindex="${this.disabled ? -1 : 0}"
            ></div>
          </div>
        </div>
        ${this.renderSuggestions()}
      </div>
      ${this.supportingText ? html` <div class="supporting-text">${this.supportingText}</div> ` : nothing}
    `
  }
}
