/**
 * Terminal - Terminal emulator UI with command input, history, and output
 * @countermeasure/web-components/visualization/terminal
 */

import { resolveContainer, uniqueId } from '../../primitives/utils'
import type { TerminalConfig } from './types'

export type { TerminalConfig } from './types'

/** Default prompt string */
const DEFAULT_PROMPT = '$ '

/** Default maximum command history entries */
const DEFAULT_MAX_HISTORY = 100

/** Default maximum output lines in scroll buffer */
const DEFAULT_MAX_LINES = 1000

/** Default font size in pixels */
const DEFAULT_FONT_SIZE = 13

/** Default monospace font family */
const DEFAULT_FONT_FAMILY = 'Menlo, Monaco, Consolas, monospace'

/** Line type for styling */
type LineType = 'command' | 'output' | 'error' | 'greeting'

/** Stored output line */
interface OutputLine {
  text: string
  type: LineType
}

export class Terminal {
  private readonly id: string
  private readonly container: HTMLElement
  private readonly config: TerminalConfig
  private readonly prompt: string
  private readonly maxHistory: number
  private readonly maxLines: number

  private outputEl: HTMLElement | null = null
  private inputEl: HTMLInputElement | null = null

  /** Command history ring buffer */
  private readonly history: string[] = []
  private historyIndex = -1
  private currentInput = ''

  /** Output line buffer */
  private readonly lines: OutputLine[] = []

  /** Whether we are currently processing a command */
  private processing = false

  constructor(config: TerminalConfig) {
    this.id = uniqueId('cmm-terminal')
    this.container = resolveContainer(config.container)
    this.config = config
    this.prompt = config.prompt ?? DEFAULT_PROMPT
    this.maxHistory = config.maxHistory ?? DEFAULT_MAX_HISTORY
    this.maxLines = config.maxLines ?? DEFAULT_MAX_LINES

    this.render()

    // Show greeting if configured
    if (config.greeting !== undefined) {
      this.addLine(config.greeting, 'greeting')
    }

    this.focusInput()
  }

  /** Write text to the output area (no trailing newline) */
  write(text: string): void {
    this.addLine(text, 'output')
  }

  /** Write text to the output area with a trailing newline */
  writeLine(text: string): void {
    this.addLine(text, 'output')
  }

  /** Write error text to the output area */
  writeError(text: string): void {
    this.addLine(text, 'error')
  }

  /** Clear all output lines */
  clear(): void {
    this.lines.length = 0
    this.renderOutput()
  }

  /** Focus the input field */
  focus(): void {
    this.focusInput()
  }

  /** Clean up the component and remove from DOM */
  destroy(): void {
    this.container.innerHTML = ''
    this.outputEl = null
    this.inputEl = null
    this.history.length = 0
    this.lines.length = 0
  }

  // ---------------------------------------------------------------------------
  // Rendering
  // ---------------------------------------------------------------------------

  /** Create the main DOM structure */
  private render(): void {
    this.container.innerHTML = ''

    const fontSize = this.config.fontSize ?? DEFAULT_FONT_SIZE
    const fontFamily = this.config.fontFamily ?? DEFAULT_FONT_FAMILY

    const wrapper = document.createElement('div')
    wrapper.className = 'cmm-terminal'
    wrapper.id = this.id
    wrapper.style.fontFamily = fontFamily
    wrapper.style.fontSize = `${String(fontSize)}px`

    if (this.config.className !== undefined) {
      wrapper.classList.add(this.config.className)
    }

    // Click anywhere to focus input
    wrapper.addEventListener('click', () => {
      this.focusInput()
    })

    // Output area
    const output = document.createElement('div')
    output.className = 'cmm-terminal__output'
    output.setAttribute('role', 'log')
    output.setAttribute('aria-live', 'polite')
    wrapper.appendChild(output)
    this.outputEl = output

    // Input line
    const inputLine = document.createElement('div')
    inputLine.className = 'cmm-terminal__input-line'

    const promptSpan = document.createElement('span')
    promptSpan.className = 'cmm-terminal__prompt'
    promptSpan.textContent = this.prompt
    inputLine.appendChild(promptSpan)

    const input = document.createElement('input')
    input.className = 'cmm-terminal__input'
    input.type = 'text'
    input.setAttribute('autocomplete', 'off')
    input.setAttribute('autocorrect', 'off')
    input.setAttribute('autocapitalize', 'off')
    input.setAttribute('spellcheck', 'false')
    input.setAttribute('aria-label', 'Terminal input')

    input.addEventListener('keydown', this.handleInputKeyDown.bind(this))

    inputLine.appendChild(input)
    wrapper.appendChild(inputLine)
    this.inputEl = input

    this.container.appendChild(wrapper)
  }

  /** Re-render the output lines */
  private renderOutput(): void {
    if (this.outputEl === null) {
      return
    }

    this.outputEl.innerHTML = ''

    for (const line of this.lines) {
      const lineEl = document.createElement('div')
      lineEl.className = `cmm-terminal__line cmm-terminal__line--${line.type}`
      lineEl.textContent = line.text
      this.outputEl.appendChild(lineEl)
    }

    this.scrollToBottom()
  }

  // ---------------------------------------------------------------------------
  // Output management
  // ---------------------------------------------------------------------------

  /** Add a line to the output buffer and render it */
  private addLine(text: string, type: LineType): void {
    // Split multiline text into individual lines
    const parts = text.split('\n')
    for (const part of parts) {
      this.lines.push({ text: part, type })
    }

    // Trim buffer if over max
    while (this.lines.length > this.maxLines) {
      this.lines.shift()
    }

    // Append new lines to output without full re-render
    if (this.outputEl !== null) {
      for (const part of parts) {
        const lineEl = document.createElement('div')
        lineEl.className = `cmm-terminal__line cmm-terminal__line--${type}`
        lineEl.textContent = part
        this.outputEl.appendChild(lineEl)
      }

      // Trim DOM nodes if over max
      while (this.outputEl.childNodes.length > this.maxLines) {
        const first = this.outputEl.firstChild
        if (first !== null) {
          this.outputEl.removeChild(first)
        }
      }

      this.scrollToBottom()
    }
  }

  /** Auto-scroll output to the bottom */
  private scrollToBottom(): void {
    if (this.outputEl !== null) {
      this.outputEl.scrollTop = this.outputEl.scrollHeight
    }
  }

  /** Focus the input element */
  private focusInput(): void {
    if (this.inputEl !== null) {
      this.inputEl.focus()
    }
  }

  // ---------------------------------------------------------------------------
  // Command handling
  // ---------------------------------------------------------------------------

  /** Handle keydown events on the input */
  private handleInputKeyDown(e: KeyboardEvent): void {
    switch (e.key) {
      case 'Enter': {
        e.preventDefault()
        if (!this.processing) {
          void this.submitCommand()
        }
        break
      }
      case 'ArrowUp': {
        e.preventDefault()
        this.navigateHistory(-1)
        break
      }
      case 'ArrowDown': {
        e.preventDefault()
        this.navigateHistory(1)
        break
      }
      case 'c': {
        // Ctrl+C clears the current input
        if (e.ctrlKey) {
          e.preventDefault()
          if (this.inputEl !== null) {
            this.addLine(`${this.prompt}${this.inputEl.value}^C`, 'command')
            this.inputEl.value = ''
            this.historyIndex = -1
            this.currentInput = ''
          }
        }
        break
      }
      case 'l': {
        // Ctrl+L clears the screen
        if (e.ctrlKey) {
          e.preventDefault()
          this.clear()
        }
        break
      }
    }
  }

  /** Submit the current command */
  private async submitCommand(): Promise<void> {
    if (this.inputEl === null) {
      return
    }

    const command = this.inputEl.value.trim()
    this.inputEl.value = ''
    this.historyIndex = -1
    this.currentInput = ''

    // Show the command line in output
    this.addLine(`${this.prompt}${command}`, 'command')

    if (command.length === 0) {
      return
    }

    // Add to history
    this.pushHistory(command)

    // Handle built-in clear command
    if (command === 'clear') {
      this.clear()
      return
    }

    // Execute command handler if provided
    if (this.config.onCommand !== undefined) {
      this.processing = true
      this.setInputDisabled(true)

      try {
        const result = this.config.onCommand(command)
        let output: string

        if (result instanceof Promise) {
          output = await result
        } else {
          output = result
        }

        if (output.length > 0) {
          this.addLine(output, 'output')
        }
      } catch (err: unknown) {
        const message = err instanceof Error ? err.message : String(err)
        this.addLine(message, 'error')
      } finally {
        this.processing = false
        this.setInputDisabled(false)
        this.focusInput()
      }
    }
  }

  /** Push a command to the history buffer */
  private pushHistory(command: string): void {
    // Avoid consecutive duplicates
    const lastEntry = this.history[this.history.length - 1]
    if (lastEntry === command) {
      return
    }

    this.history.push(command)

    // Trim if over max
    while (this.history.length > this.maxHistory) {
      this.history.shift()
    }
  }

  /** Navigate through command history */
  private navigateHistory(direction: number): void {
    if (this.inputEl === null || this.history.length === 0) {
      return
    }

    // Save current input when first entering history
    if (this.historyIndex === -1 && direction === -1) {
      this.currentInput = this.inputEl.value
      this.historyIndex = this.history.length
    }

    const newIndex = this.historyIndex + direction

    if (newIndex < 0) {
      return
    }

    if (newIndex >= this.history.length) {
      // Returned to the current input
      this.historyIndex = -1
      this.inputEl.value = this.currentInput
      return
    }

    this.historyIndex = newIndex
    const entry = this.history[this.historyIndex]
    if (entry !== undefined) {
      this.inputEl.value = entry
    }

    // Move cursor to end
    const len = this.inputEl.value.length
    this.inputEl.setSelectionRange(len, len)
  }

  /** Enable or disable the input element */
  private setInputDisabled(disabled: boolean): void {
    if (this.inputEl !== null) {
      this.inputEl.disabled = disabled
    }
  }
}
