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 { linter } from '@codemirror/lint'
import { EditorState, Extension } from '@codemirror/state'
import {
  autocompletion,
  CompletionContext,
  type CompletionResult,
  type CompletionSource,
  type Completion,
} from '@codemirror/autocomplete'
import { javascript } from '@codemirror/lang-javascript'
import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language'
import { oneDark } from '@codemirror/theme-one-dark'
import { keymap } from '@codemirror/view'
import { defaultKeymap } from '@codemirror/commands'
import {
  EditorView,
  Decoration,
  DecorationSet,
  ViewPlugin,
  ViewUpdate,
  hoverTooltip,
  type Tooltip,
} from '@codemirror/view'
import type { FunctionSchema } from '@pawel-up/jexl/schemas/types.js'
import { getTypeStringFromSchema } from '@pawel-up/jexl/schemas/utils.js'
import { functionLinter } from './Linter.js'
import type { FunctionInsertEvent, Suggestion, SuggestionInsertEvent } from './types.js'
import { SuggestionMatchDecorator } from './SuggestionMatchDecorator.js'

/**
 * A CodeMirror 6 based editor component that supports function autocomplete and suggestion placeholders.
 *
 * Features:
 * - Dynamic function schema loading
 * - Autocomplete for functions and suggestions
 * - Suggestion placeholders with double curly braces ({{suggestion}})
 * - Keyboard navigation
 * - Accessibility support
 *
 * @fires function-insert - When a function is inserted
 * @fires suggestion-insert - When a suggestion is inserted
 * @fires input - When the editor content changes
 * @fires change - When the editor loses focus and content has changed
 */
export default class CodeEditor 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 editor
   */
  @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 editor is empty
   */
  @property({ type: String })
  accessor placeholder = ''

  /**
   * The editor content value
   */
  @property({ type: String })
  accessor value = ''

  /**
   * Available function schemas for autocomplete
   */
  @property({ type: Array, attribute: false })
  accessor functionSchemas: FunctionSchema[] = []

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

  /**
   * Whether to use dark theme
   */
  @property({ type: Boolean, attribute: 'dark-theme' })
  accessor darkTheme = false

  /**
   * Programming language for syntax highlighting
   */
  @property({ type: String })
  accessor language = 'javascript'

  @query('.editor-container')
  private accessor editorContainer!: HTMLDivElement

  @state()
  private accessor hasContent = false

  @state()
  private accessor isEditorFocus = false

  private editorView?: EditorView
  private _previousValue = ''
  /**
   * Matcher for suggestion placeholders in the editor.
   */
  placeholderMatcher?: SuggestionMatchDecorator

  /**
   * Get all suggestions (placeholders) currently in the editor
   */
  get activeSuggestions(): Suggestion[] {
    const suggestions: Suggestion[] = []
    const placeholderPattern = /\{\{(\w+)\}\}/g
    let match: RegExpExecArray | null
    while ((match = placeholderPattern.exec(this.value)) !== null) {
      const placeholderText = match[1]
      // Find suggestion by label
      const suggestion = this.suggestions.find((s) => s.label.toLowerCase() === placeholderText.toLowerCase())
      if (suggestion) {
        suggestions.push(suggestion)
      }
    }
    return suggestions
  }

  override disconnectedCallback(): void {
    super.disconnectedCallback()
    this.editorView?.destroy()
  }

  override firstUpdated(): void {
    this.initializeCodeMirror()
    this.updateEditorContent()
  }

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

    if (changedProperties.has('suggestions')) {
      if (this.placeholderMatcher) {
        this.placeholderMatcher.suggestions = this.suggestions || []
      }
    }

    if (changedProperties.has('value') && this.editorView) {
      this.updateEditorContent()
    }

    if (changedProperties.has('disabled')) {
      this.updateEditorState()
    }
  }

  /**
   * Initialize CodeMirror editor with extensions
   */
  private initializeCodeMirror(): void {
    const extensions: Extension[] = [
      keymap.of(defaultKeymap),
      syntaxHighlighting(defaultHighlightStyle),
      autocompletion({
        override: [this.createCompletionSource()],
        activateOnTyping: true,
        maxRenderedOptions: 10,
      }),
      EditorView.updateListener.of((update: ViewUpdate) => {
        if (update.docChanged) {
          this.handleEditorChange()
        }
        if (update.focusChanged) {
          this.handleFocusChange(update.view.hasFocus)
        }
      }),
      this.createPlaceholderPlugin(),
      hoverTooltip(this.createHoverTooltipSource),
      linter((view) => functionLinter(view, this.functionSchemas, (e) => this.dispatchEvent(e))),
    ]

    // Add language support
    if (this.language === 'javascript') {
      extensions.push(javascript())
    }

    // Add theme
    if (this.darkTheme) {
      extensions.push(oneDark)
    }

    // Add placeholder
    if (this.placeholder) {
      extensions.push(
        EditorView.contentAttributes.of({
          'aria-placeholder': this.placeholder,
        })
      )
    }

    const state = EditorState.create({
      doc: this.value,
      extensions,
    })

    this.editorView = new EditorView({
      state,
      parent: this.editorContainer,
    })
  }

  /**
   * Creates the ViewPlugin for rendering suggestion placeholders.
   * This is created as a method to get access to the component's `suggestions`.
   */
  private createPlaceholderPlugin(): Extension {
    const placeholderMatcher = new SuggestionMatchDecorator(/\{\{(\w+)\}\}/g, this.suggestions)
    this.placeholderMatcher = placeholderMatcher
    // This class needs to be defined here to have access to the `placeholderMatcher`
    class AtomicDecorationRange {
      placeholders: DecorationSet
      constructor(view: EditorView) {
        this.placeholders = placeholderMatcher.createDeco(view)
      }
      update(update: ViewUpdate) {
        this.placeholders = placeholderMatcher.updateDeco(update, this.placeholders)
      }
    }

    return ViewPlugin.fromClass(AtomicDecorationRange, {
      decorations: (instance) => instance.placeholders,
      provide: (plugin) => EditorView.atomicRanges.of((view) => view.plugin(plugin)?.placeholders || Decoration.none),
    })
  }

  /**
   * Create completion source for functions and suggestions
   */
  private createCompletionSource(): CompletionSource {
    return (context: CompletionContext): CompletionResult | null => {
      const { pos } = context
      const line = context.state.doc.lineAt(pos)
      const textBefore = line.text.slice(0, pos - line.from)

      // Check if we're typing a function name
      const functionMatch = textBefore.match(/(\w+)$/)
      if (functionMatch) {
        const prefix = functionMatch[1]
        const functions = this.functionSchemas
          .filter((fn) => fn.name.toLowerCase().startsWith(prefix.toLowerCase()))
          .map((fn) => this.createFunctionCompletion(fn))

        if (functions.length > 0) {
          return {
            from: pos - prefix.length,
            options: functions,
          }
        }
      }

      // Check if we're typing a suggestion trigger (e.g., {{)
      const suggestionMatch = textBefore.match(/\{\{(\w*)$/)
      if (suggestionMatch) {
        const prefix = suggestionMatch[1]
        const suggestions = this.suggestions
          .filter((suggestion) => suggestion.label.toLowerCase().startsWith(prefix.toLowerCase()))
          .map((suggestion) => this.createSuggestionCompletion(suggestion))

        if (suggestions.length > 0) {
          return {
            from: pos - prefix.length,
            options: suggestions,
          }
        }
      }

      return null
    }
  }

  private createFunctionCompletion(schema: FunctionSchema): Completion {
    const result: Completion = {
      label: schema.name,
      detail: schema.description || '',
      info: () => this.createFunctionInfoElement(schema),
      apply: (view: EditorView, completion: unknown, from: number, to: number) => {
        const functionCall = this.formatFunctionCall(schema)
        view.dispatch({
          changes: { from, to, insert: functionCall },
          selection: { anchor: from + functionCall.length },
        })
        this.dispatchFunctionInsert(schema, from)
      },
    }

    return result
  }

  private createSuggestionCompletion(suggestion: Suggestion): Completion {
    const result: Completion = {
      label: suggestion.label,
      detail: suggestion.description || '',
      info: suggestion.suffix || '',
      apply: (view: EditorView, completion: unknown, from: number, to: number) => {
        const placeholderText = `{{${suggestion.label}}}`
        view.dispatch({
          changes: { from: from - 2, to, insert: placeholderText }, // -2 to include the {{
          selection: { anchor: from - 2 + placeholderText.length },
        })
        this.dispatchSuggestionInsert(suggestion, from - 2)
      },
    }

    return result
  }

  /**
   * Creates the source for the hover tooltips.
   * This is an arrow function to automatically bind `this`.
   */
  private createHoverTooltipSource = (view: EditorView, pos: number): Tooltip | null => {
    const word = view.state.wordAt(pos)
    if (!word) {
      return null
    }

    const functionName = view.state.doc.sliceString(word.from, word.to)
    const fnSchema = this.functionSchemas.find((schema) => schema.name === functionName)

    if (!fnSchema) {
      return null
    }

    return {
      pos: word.from,
      end: word.to,
      above: true,
      create: () => ({ dom: this.createFunctionInfoElement(fnSchema) }),
    }
  }

  /**
   * Creates a styled HTML element to display function documentation.
   * This is used for both hover tooltips and autocomplete info panels.
   */
  private createFunctionInfoElement(fn: FunctionSchema): HTMLElement {
    const container = document.createElement('div')
    container.className = 'function-info' // for styling

    if (fn.description) {
      const description = document.createElement('p')
      description.className = 'description'
      description.textContent = fn.description
      container.appendChild(description)
    }

    if (fn.parameters && fn.parameters.length > 0) {
      const paramsContainer = document.createElement('div')
      paramsContainer.className = 'parameters'

      const paramsHeader = document.createElement('h4')
      paramsHeader.textContent = 'Parameters'
      paramsContainer.appendChild(paramsHeader)

      const paramsList = document.createElement('ul')
      fn.parameters.forEach((param) => {
        const listItem = document.createElement('li')

        const name = document.createElement('span')
        name.className = 'param-name'
        name.textContent = param.name

        const type = document.createElement('span')
        type.className = 'param-type'
        type.textContent = `: ${getTypeStringFromSchema(param.schema)}`

        listItem.appendChild(name)
        listItem.appendChild(type)

        if (param.schema.description) {
          const paramDesc = document.createElement('p')
          paramDesc.className = 'param-description'
          paramDesc.textContent = param.schema.description
          listItem.appendChild(paramDesc)
        }
        paramsList.appendChild(listItem)
      })
      paramsContainer.appendChild(paramsList)
      container.appendChild(paramsContainer)
    }

    if (fn.returns) {
      const returnsContainer = document.createElement('div')
      returnsContainer.className = 'returns'

      const returnsHeader = document.createElement('h4')
      returnsHeader.textContent = 'Returns'
      returnsContainer.appendChild(returnsHeader)

      const returnsDesc = document.createElement('p')
      const returnType = getTypeStringFromSchema(fn.returns)
      returnsDesc.textContent = fn.returns.description ? `${returnType}: ${fn.returns.description}` : returnType
      returnsContainer.appendChild(returnsDesc)
      container.appendChild(returnsContainer)
    }

    return container
  }

  /**
   * Format function call with parameters
   */
  private formatFunctionCall(fn: FunctionSchema): string {
    if (!fn.parameters || fn.parameters.length === 0) {
      return `${fn.name}()`
    }

    const params = fn.parameters
      .map((param) => {
        if (param.required) {
          return param.name
        }
        return `${param.name}?`
      })
      .join(', ')

    return `${fn.name}(${params})`
  }

  /**
   * Update editor content when value changes
   */
  private updateEditorContent(): void {
    if (!this.editorView) return

    const currentValue = this.editorView.state.doc.toString()
    if (currentValue !== this.value) {
      this.editorView.dispatch({
        changes: {
          from: 0,
          to: this.editorView.state.doc.length,
          insert: this.value,
        },
      })
    }
  }

  /**
   * Update editor state (e.g., disabled state)
   */
  private updateEditorState(): void {
    if (!this.editorView) return

    // For now, we'll handle disabled state differently
    // CodeMirror 6 doesn't use reconfigure for editable
    if (this.disabled) {
      this.editorView.contentDOM.setAttribute('contenteditable', 'false')
    } else {
      this.editorView.contentDOM.setAttribute('contenteditable', 'true')
    }
  }

  /**
   * Handle editor content change
   */
  private handleEditorChange(): void {
    if (!this.editorView) return

    const newValue = this.editorView.state.doc.toString()
    if (newValue !== this._previousValue) {
      this._previousValue = newValue
      this.value = newValue
      this.hasContent = newValue.length > 0

      this.dispatchEvent(new Event('input', { bubbles: true }))
    }
  }

  /**
   * Handle focus change
   */
  private handleFocusChange(hasFocus: boolean): void {
    this.isEditorFocus = hasFocus

    if (!hasFocus && this.value !== this._previousValue) {
      this.dispatchEvent(new Event('change', { bubbles: true }))
    }
  }

  /**
   * Dispatch function insert event
   */
  private dispatchFunctionInsert(functionSchema: FunctionSchema, position: number): void {
    const event = new CustomEvent<FunctionInsertEvent>('function-insert', {
      detail: { functionSchema, position },
      bubbles: true,
    })
    this.dispatchEvent(event)
  }

  /**
   * Dispatch suggestion insert event
   */
  private dispatchSuggestionInsert(suggestion: Suggestion, position: number): void {
    const event = new CustomEvent<SuggestionInsertEvent>('suggestion-insert', {
      detail: { suggestion, position },
      bubbles: true,
    })
    this.dispatchEvent(event)
  }

  /**
   * Focus the editor
   */
  override focus(): void {
    this.editorView?.focus()
  }

  /**
   * Get the editor's current selection
   */
  getSelection(): { from: number; to: number } | null {
    if (!this.editorView) return null
    const { from, to } = this.editorView.state.selection.main
    return { from, to }
  }

  /**
   * Insert text at the current cursor position
   */
  insertText(text: string): void {
    if (!this.editorView) return

    const { from, to } = this.editorView.state.selection.main
    this.editorView.dispatch({
      changes: { from, to, insert: text },
      selection: { anchor: from + text.length },
    })
  }

  override render(): TemplateResult {
    const hasLabel = !!this.label
    const hasSupportingText = !!this.supportingText

    return html`
      <div class="surface ${classMap({ 'has-focus': this.isEditorFocus, 'invalid': this.invalid })}">
        <div class="content">
          ${hasLabel ? html`<div class="label">${this.label}</div>` : nothing}

          <div class="editor-container" part="editor"></div>
        </div>

        ${hasSupportingText ? html`<div class="supporting-text">${this.supportingText}</div>` : nothing}
      </div>
    `
  }
}
