/**
 * LogViewer - Read-only streaming log display
 * @countermeasure/web-components/editors
 */

import { EditorView } from '@codemirror/view'
import { StreamLanguage, LanguageSupport } from '@codemirror/language'
import { type Extension } from '@codemirror/state'
import { search, searchKeymap } from '@codemirror/search'
import { keymap } from '@codemirror/view'
import { BaseEditor } from './base'
import { type LogViewerConfig, type LogSeverity } from './types'

/** Language tokenizer for log output with severity highlighting */
const logLanguage = StreamLanguage.define({
  token(stream): string | null {
    if (stream.sol()) {
      if (stream.match('[ERROR]') !== null) {
        return 'invalid'
      }
      if (stream.match('[WARN]') !== null) {
        return 'keyword'
      }
      if (stream.match('[OK]') !== null) {
        return 'string'
      }
      if (stream.match('[INFO]') !== null) {
        return 'comment'
      }
    }

    // Timestamps like 2024-01-01T12:00:00
    if (stream.match(/\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}/) !== null) {
      return 'number'
    }

    // IP addresses
    if (stream.match(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/) !== null) {
      return 'atom'
    }

    // Quoted strings
    if (stream.match(/"(?:[^"\\]|\\.)*"/) !== null) {
      return 'string'
    }

    // Numbers
    if (stream.match(/\d+(\.\d+)?/) !== null) {
      return 'number'
    }

    // Key=value pairs
    if (stream.match(/[A-Za-z_][\w]*(?==)/) !== null) {
      return 'propertyName'
    }

    stream.next()
    return null
  },
})

const logSupport = new LanguageSupport(logLanguage)

/** Map a severity level to a log prefix tag */
function severityPrefix(severity: LogSeverity): string {
  switch (severity) {
    case 'error':
      return '[ERROR] '
    case 'warning':
      return '[WARN]  '
    case 'success':
      return '[OK]    '
    case 'info':
      return '[INFO]  '
  }
}

export class LogViewer extends BaseEditor {
  declare private maxLines: number
  declare private followMode: boolean

  constructor(config: LogViewerConfig) {
    const maxLinesVal = config.maxLines ?? 10000
    const followModeVal = config.followMode !== false
    const useSeverityColors = config.severityColors !== false

    const extraExtensions: Extension[] = [
      // Always read-only
      // (BaseEditor handles this, but we force it)

      // Log syntax highlighting
      ...(useSeverityColors ? [logSupport] : []),

      // Search support (Ctrl+F to find in logs)
      search(),
      keymap.of(searchKeymap),
    ]

    super(
      {
        ...config,
        readOnly: true,
        lineNumbers: config.lineNumbers ?? false,
        wordWrap: config.wordWrap ?? true,
      },
      extraExtensions
    )

    this.maxLines = maxLinesVal
    this.followMode = followModeVal
  }

  /** Append a single line to the log */
  appendLine(text: string, severity?: LogSeverity): void {
    const prefix = severity ? severityPrefix(severity) : ''
    const line = `${prefix}${text}`
    const doc = this.view.state.doc
    const insertion = doc.length > 0 ? `\n${line}` : line

    interface DocChange {
      from: number
      to?: number
      insert?: string
    }

    const changes: DocChange[] = []

    // Trim excess lines from top (ring buffer)
    const newLineCount = doc.lines + 1
    if (newLineCount > this.maxLines) {
      const excess = newLineCount - this.maxLines
      const cutLine = doc.line(excess + 1)
      changes.push({ from: 0, to: cutLine.from })
    }

    // Append at bottom
    changes.push({ from: doc.length, insert: insertion })

    this.view.dispatch({ changes })

    // Auto-scroll to bottom
    if (this.followMode) {
      const newDoc = this.view.state.doc
      this.view.dispatch({
        effects: EditorView.scrollIntoView(newDoc.length),
      })
    }
  }

  /** Append multiple lines at once */
  appendLines(lines: { text: string; severity?: LogSeverity }[]): void {
    if (lines.length === 0) {
      return
    }

    const formatted = lines
      .map(line => {
        const prefix = line.severity ? severityPrefix(line.severity) : ''
        return `${prefix}${line.text}`
      })
      .join('\n')

    const doc = this.view.state.doc
    const insertion = doc.length > 0 ? `\n${formatted}` : formatted

    interface DocChange {
      from: number
      to?: number
      insert?: string
    }

    const changes: DocChange[] = []

    // Trim excess lines from top
    const newLineCount = doc.lines + lines.length
    if (newLineCount > this.maxLines) {
      const excess = newLineCount - this.maxLines
      const safeLine = Math.min(excess + 1, doc.lines)
      const cutLine = doc.line(safeLine)
      changes.push({ from: 0, to: cutLine.from })
    }

    // Append at bottom
    changes.push({ from: doc.length, insert: insertion })

    this.view.dispatch({ changes })

    if (this.followMode) {
      const newDoc = this.view.state.doc
      this.view.dispatch({
        effects: EditorView.scrollIntoView(newDoc.length),
      })
    }
  }

  /** Clear all log content */
  clear(): void {
    this.view.dispatch({
      changes: { from: 0, to: this.view.state.doc.length },
    })
  }

  /** Toggle follow mode (auto-scroll to bottom) */
  setFollowMode(follow: boolean): void {
    this.followMode = follow
    if (follow) {
      this.view.dispatch({
        effects: EditorView.scrollIntoView(this.view.state.doc.length),
      })
    }
  }

  /** Get follow mode state */
  getFollowMode(): boolean {
    return this.followMode
  }

  /** Update the maximum line buffer size */
  setMaxLines(maxLines: number): void {
    this.maxLines = maxLines
  }
}
