/**
 * CodeEditor - Full-featured code editor with autocomplete and validation
 * @countermeasure/web-components/editors
 */

import { type EditorView, keymap } from '@codemirror/view'
import { Compartment, type Extension } from '@codemirror/state'
import { autocompletion, type Completion, type CompletionContext } from '@codemirror/autocomplete'
import { linter, lintGutter, type Diagnostic } from '@codemirror/lint'
import { foldGutter, foldKeymap } from '@codemirror/language'
import { BaseEditor } from './base'
import { fluxl, fluxlCompletions } from './languages/fluxl'
import { yaral, yaralCompletions } from './languages/yaral'
import { yara, yaraCompletions } from './languages/yara'
import { sql, sqlCompletions } from './languages/sql'
import { json } from './languages/json'
import { yaml } from './languages/yaml'
import { python } from './languages/python'
import { javascript, typescript } from './languages/javascript'
import { rust } from './languages/rust'
import { markdown } from './languages/markdown'
import { shellLanguage } from './languages/shell'
import {
  type CodeEditorConfig,
  type AutocompleteSchema,
  type ValidationConfig,
  type ValidationError,
  type ValidationResult,
} from './types'

/** Resolve a language string to a CM6 extension */
function resolveLanguage(language: string): Extension {
  switch (language) {
    case 'fluxl':
      return fluxl()
    case 'sql':
      return sql()
    case 'yaral':
      return yaral()
    case 'yara':
      return yara()
    case 'json':
      return json()
    case 'yaml':
    case 'yml':
      return yaml()
    case 'python':
    case 'py':
      return python()
    case 'javascript':
    case 'js':
    case 'jsx':
      return javascript()
    case 'typescript':
    case 'ts':
    case 'tsx':
      return typescript()
    case 'rust':
    case 'rs':
      return rust()
    case 'markdown':
    case 'md':
      return markdown()
    case 'bash':
    case 'sh':
    case 'shell':
    case 'zsh':
      return shellLanguage()
    default:
      return []
  }
}

/** Get static completions for a language */
function getLanguageCompletions(language: string): Completion[] {
  switch (language) {
    case 'fluxl':
      return fluxlCompletions
    case 'sql':
      return sqlCompletions
    case 'yaral':
      return yaralCompletions
    case 'yara':
      return yaraCompletions
    default:
      return []
  }
}

/** Build a completion source combining language keywords + schema */
function buildCompletionSource(language: string, schema: AutocompleteSchema) {
  return (
    context: CompletionContext
  ): ReturnType<typeof context.matchBefore> extends infer _R
    ? { from: number; options: Completion[] } | null
    : never => {
    const word = context.matchBefore(/[\w.]+/)
    if (!word && !context.explicit) {
      return null
    }

    const query = word?.text.toLowerCase() ?? ''

    const langCompletions = getLanguageCompletions(language)

    const tableCompletions: Completion[] = (schema.tables ?? []).map(table => {
      const base: Completion = {
        label: table.name,
        type: 'class' as const,
      }
      if (table.description !== undefined) {
        base.info = table.description
      }
      return base
    })

    const columnCompletions: Completion[] = (schema.tables ?? []).flatMap(table =>
      table.columns.map(col => ({
        label: col.name,
        type: 'property' as const,
        info: `${table.name}.${col.name} (${col.dataType})`,
      }))
    )

    const funcCompletions: Completion[] = (schema.functions ?? []).map(name => ({
      label: `${name}()`,
      type: 'function' as const,
    }))

    const opCompletions: Completion[] = (schema.operators ?? []).map(name => ({
      label: name,
      type: 'keyword' as const,
    }))

    const customCompletions: Completion[] = (schema.custom ?? []).map(item => {
      const base: Completion = {
        label: item.label,
        type: item.type,
      }
      if (item.info !== undefined) {
        base.info = item.info
      }
      if (item.detail !== undefined) {
        base.detail = item.detail
      }
      return base
    })

    const allOptions = [
      ...langCompletions,
      ...tableCompletions,
      ...columnCompletions,
      ...funcCompletions,
      ...opCompletions,
      ...customCompletions,
    ].filter(opt => (query.length > 0 ? opt.label.toLowerCase().startsWith(query) : true))

    return {
      from: word?.from ?? context.pos,
      options: allOptions.slice(0, 100),
    }
  }
}

/** Build a linter callback from a validation config */
function buildValidationLinter(config: ValidationConfig, getLanguage: () => string) {
  return async (view: EditorView): Promise<Diagnostic[]> => {
    const query = view.state.doc.toString()
    if (!query.trim()) {
      return []
    }

    try {
      const language = getLanguage()
      const body = config.transformRequest
        ? config.transformRequest(query, language)
        : { query, type: language }

      const response = await fetch(config.endpoint, {
        method: config.method ?? 'POST',
        headers: {
          'Content-Type': 'application/json',
          ...(config.headers ?? {}),
        },
        body: JSON.stringify(body),
      })

      if (!response.ok) {
        return []
      }

      const data = (await response.json()) as unknown
      const result: ValidationResult = config.transformResponse
        ? config.transformResponse(data)
        : (data as ValidationResult)

      if (result.valid) {
        return []
      }

      return mapErrorsToDiagnostics(view, result.errors ?? [])
    } catch {
      return []
    }
  }
}

/** Convert validation errors to CM6 diagnostics */
function mapErrorsToDiagnostics(view: EditorView, errors: ValidationError[]): Diagnostic[] {
  return errors.map(error => {
    const line = error.line ?? 1
    const column = error.column ?? 1
    const safeLine = Math.min(Math.max(1, line), view.state.doc.lines)
    const lineInfo = view.state.doc.line(safeLine)
    const from = Math.min(lineInfo.to, lineInfo.from + Math.max(0, column - 1))
    const to = Math.min(lineInfo.to, from + 1)
    return {
      from,
      to,
      severity: error.severity ?? 'error',
      message: error.message,
    }
  })
}

export class CodeEditor extends BaseEditor {
  declare private languageCompartment: Compartment
  declare private lintCompartment: Compartment
  declare private autocompleteCompartment: Compartment
  declare private currentLanguage: string
  declare private currentSchema: AutocompleteSchema | null

  constructor(config: CodeEditorConfig) {
    const langComp = new Compartment()
    const lintComp = new Compartment()
    const autocompleteComp = new Compartment()
    const language = config.language ?? 'fluxl'

    const customKeybindings: Extension = config.keybindings
      ? keymap.of(
          Object.entries(config.keybindings).map(([key, handler]) => ({
            key,
            run: () => {
              handler()
              return true
            },
          }))
        )
      : []

    const extraExtensions: Extension[] = [
      // Language
      langComp.of(resolveLanguage(language)),

      // Autocompletion
      autocompleteComp.of(
        config.autocomplete
          ? autocompletion({
              override: [buildCompletionSource(language, config.autocomplete)],
            })
          : autocompletion()
      ),

      // Linting
      lintComp.of(
        config.validation
          ? linter(
              buildValidationLinter(config.validation, () => this.currentLanguage),
              { delay: config.validation.debounceMs ?? 800 }
            )
          : []
      ),
      lintGutter(),

      // Code folding
      foldGutter(),
      keymap.of(foldKeymap),

      // Custom keybindings (before defaults so they take priority)
      customKeybindings,
    ]

    super(config, extraExtensions)

    this.languageCompartment = langComp
    this.lintCompartment = lintComp
    this.autocompleteCompartment = autocompleteComp
    this.currentLanguage = language
    this.currentSchema = config.autocomplete ?? null
  }

  /** Switch the editor language */
  setLanguage(language: string): void {
    this.currentLanguage = language
    this.view.dispatch({
      effects: this.languageCompartment.reconfigure(resolveLanguage(language)),
    })
    // Update autocomplete with new language completions
    if (this.currentSchema) {
      this.setAutocompleteSchema(this.currentSchema)
    }
  }

  /** Get the current language identifier */
  getLanguage(): string {
    return this.currentLanguage
  }

  /** Update the autocomplete schema */
  setAutocompleteSchema(schema: AutocompleteSchema): void {
    this.currentSchema = schema
    this.view.dispatch({
      effects: this.autocompleteCompartment.reconfigure(
        autocompletion({
          override: [buildCompletionSource(this.currentLanguage, schema)],
        })
      ),
    })
  }

  /** Set inline error markers from external validation */
  setErrors(errors: ValidationError[]): void {
    const diagnostics = mapErrorsToDiagnostics(this.view, errors)
    this.view.dispatch({
      effects: this.lintCompartment.reconfigure(linter(() => diagnostics, { delay: 0 })),
    })
  }

  /** Remove all inline error markers */
  clearErrors(): void {
    this.view.dispatch({
      effects: this.lintCompartment.reconfigure([]),
    })
  }

  /** Insert text at the current cursor position */
  insertText(text: string): void {
    const selection = this.view.state.selection.main
    this.view.dispatch({
      changes: { from: selection.from, to: selection.to, insert: text },
    })
    this.view.focus()
  }

  /** Get the currently selected text */
  getSelection(): string {
    const selection = this.view.state.selection.main
    return this.view.state.sliceDoc(selection.from, selection.to)
  }
}
