/**
 * DiffViewer - Side-by-side diff viewer
 * @countermeasure/web-components/editors
 */

import {
  EditorView,
  lineNumbers,
  highlightSpecialChars,
  drawSelection,
  Decoration,
  type DecorationSet,
} from '@codemirror/view'
import { EditorState, StateField, type Extension } from '@codemirror/state'
import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language'
import { type DiffViewerConfig } from './types'
import { resolveContainer, editorTheme, tfHighlightStyle } from './base'

interface DiffLine {
  type: 'equal' | 'added' | 'removed'
  originalLine: string
  modifiedLine: string
}

/** Simple line-level diff algorithm */
function computeDiff(original: string, modified: string): DiffLine[] {
  const origLines = original.split('\n')
  const modLines = modified.split('\n')
  const result: DiffLine[] = []

  // LCS-based diff
  const m = origLines.length
  const n = modLines.length
  const dp: number[][] = Array.from({ length: m + 1 }, () => new Array<number>(n + 1).fill(0))

  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      const prevI = dp[i - 1]
      const currI = dp[i]
      if (!prevI || !currI) {
        continue
      }
      if (origLines[i - 1] === modLines[j - 1]) {
        currI[j] = (prevI[j - 1] ?? 0) + 1
      } else {
        currI[j] = Math.max(prevI[j] ?? 0, currI[j - 1] ?? 0)
      }
    }
  }

  // Backtrack to find the diff
  let i = m
  let j = n
  const stack: DiffLine[] = []

  while (i > 0 || j > 0) {
    const currRow = dp[i]
    const prevRow = dp[i - 1]

    if (i > 0 && j > 0 && origLines[i - 1] === modLines[j - 1]) {
      stack.push({
        type: 'equal',
        originalLine: origLines[i - 1] ?? '',
        modifiedLine: modLines[j - 1] ?? '',
      })
      i--
      j--
    } else if (j > 0 && (i === 0 || (currRow?.[j - 1] ?? 0) >= (prevRow?.[j] ?? 0))) {
      stack.push({
        type: 'added',
        originalLine: '',
        modifiedLine: modLines[j - 1] ?? '',
      })
      j--
    } else {
      stack.push({
        type: 'removed',
        originalLine: origLines[i - 1] ?? '',
        modifiedLine: '',
      })
      i--
    }
  }

  // Reverse the stack to get correct order
  for (let k = stack.length - 1; k >= 0; k--) {
    const item = stack[k]
    if (item) {
      result.push(item)
    }
  }

  return result
}

/**
 * CSS classes for diff line highlighting. Backed by the design-system
 * severity tokens so the highlight tracks the active theme. Falls back
 * to standalone rgba() values for bare/no-theme installs.
 *
 *   --ok       → addition tint
 *   --critical → deletion tint
 *
 * Gap rows (empty padding lines on the opposing pane) get a striped
 * background to indicate "no content here" without competing with the
 * highlighted side.
 */
const diffTheme = EditorView.theme({
  '.cmm-diff-added': {
    backgroundColor: 'var(--cmm-diff-added-bg, rgba(34, 197, 94, 0.18))',
  },
  '.cmm-diff-added .cm-gutterElement': {
    backgroundColor: 'var(--cmm-diff-added-gutter, rgba(34, 197, 94, 0.28))',
    color: 'var(--cmm-diff-added-gutter-text, #34d399)',
  },
  '.cmm-diff-removed': {
    backgroundColor: 'var(--cmm-diff-removed-bg, rgba(239, 68, 68, 0.18))',
  },
  '.cmm-diff-removed .cm-gutterElement': {
    backgroundColor: 'var(--cmm-diff-removed-gutter, rgba(239, 68, 68, 0.28))',
    color: 'var(--cmm-diff-removed-gutter-text, #fb7185)',
  },
  '.cmm-diff-gap': {
    backgroundImage:
      'var(--cmm-diff-gap-stripe, repeating-linear-gradient(135deg, rgba(148,163,184,0.05) 0 6px, transparent 6px 12px))',
  },
})

const addedLine = Decoration.line({ attributes: { class: 'cmm-diff-added' } })
const removedLine = Decoration.line({ attributes: { class: 'cmm-diff-removed' } })
const gapLine = Decoration.line({ attributes: { class: 'cmm-diff-gap' } })

/**
 * Apply per-line decorations to the editor based on a precomputed diff.
 * `marks[i]` (0-based) names the class for line `i+1`.
 */
function lineDecorationsExt(marks: Array<'added' | 'removed' | 'gap' | null>): Extension {
  return StateField.define<DecorationSet>({
    create(state) {
      const builder: Array<ReturnType<typeof addedLine.range>> = []
      const lineCount = state.doc.lines
      for (let i = 0; i < lineCount && i < marks.length; i++) {
        const mark = marks[i]
        if (mark === null || mark === undefined) continue
        const line = state.doc.line(i + 1)
        if (mark === 'added') builder.push(addedLine.range(line.from))
        else if (mark === 'removed') builder.push(removedLine.range(line.from))
        else builder.push(gapLine.range(line.from))
      }
      return Decoration.set(builder, true)
    },
    update(value) {
      return value
    },
    provide: f => EditorView.decorations.from(f),
  })
}

export class DiffViewer {
  private container: HTMLElement
  private originalView: EditorView | null = null
  private modifiedView: EditorView | null = null
  private wrapperEl: HTMLElement | null = null

  constructor(config: DiffViewerConfig) {
    this.container = resolveContainer(config.container)
    this.render(config)
  }

  private render(config: DiffViewerConfig): void {
    const diff = computeDiff(config.original, config.modified)
    const showLineNumbers = config.lineNumbers !== false

    // Build original and modified text with markers, plus per-line
    // decoration intent for each pane. The original pane tints removed
    // lines red and shows a striped "gap" where the modified pane added
    // a line; the modified pane mirrors that with green/gap.
    const originalLines: string[] = []
    const modifiedLines: string[] = []
    const originalMarks: Array<'removed' | 'gap' | null> = []
    const modifiedMarks: Array<'added' | 'gap' | null> = []

    for (const line of diff) {
      if (line.type === 'equal') {
        originalLines.push(line.originalLine)
        modifiedLines.push(line.modifiedLine)
        originalMarks.push(null)
        modifiedMarks.push(null)
      } else if (line.type === 'removed') {
        originalLines.push(line.originalLine)
        modifiedLines.push('')
        originalMarks.push('removed')
        modifiedMarks.push('gap')
      } else {
        originalLines.push('')
        modifiedLines.push(line.modifiedLine)
        originalMarks.push('gap')
        modifiedMarks.push('added')
      }
    }

    // Create wrapper
    this.wrapperEl = document.createElement('div')
    this.wrapperEl.className = 'cmm-diff-viewer'
    this.wrapperEl.style.display = 'grid'
    this.wrapperEl.style.gridTemplateColumns = '1fr 1fr'
    this.wrapperEl.style.gap = '1px'
    this.wrapperEl.style.backgroundColor = 'var(--cmm-border, #e2e8f0)'

    const leftPane = document.createElement('div')
    leftPane.className = 'cmm-diff-pane cmm-diff-original'
    const rightPane = document.createElement('div')
    rightPane.className = 'cmm-diff-pane cmm-diff-modified'

    this.wrapperEl.appendChild(leftPane)
    this.wrapperEl.appendChild(rightPane)
    this.container.appendChild(this.wrapperEl)

    const baseExtensions = [
      highlightSpecialChars(),
      drawSelection(),
      EditorState.readOnly.of(true),
      syntaxHighlighting(tfHighlightStyle),
      syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
      editorTheme,
      diffTheme,
      ...(showLineNumbers ? [lineNumbers()] : []),
    ]

    this.originalView = new EditorView({
      state: EditorState.create({
        doc: originalLines.join('\n'),
        extensions: [...baseExtensions, lineDecorationsExt(originalMarks)],
      }),
      parent: leftPane,
    })

    this.modifiedView = new EditorView({
      state: EditorState.create({
        doc: modifiedLines.join('\n'),
        extensions: [...baseExtensions, lineDecorationsExt(modifiedMarks)],
      }),
      parent: rightPane,
    })

    // Sync scrolling between panes
    const syncScroll = (source: EditorView, target: EditorView) => {
      const sourceScroll = source.scrollDOM
      sourceScroll.addEventListener('scroll', () => {
        target.scrollDOM.scrollTop = sourceScroll.scrollTop
        target.scrollDOM.scrollLeft = sourceScroll.scrollLeft
      })
    }

    syncScroll(this.originalView, this.modifiedView)
    syncScroll(this.modifiedView, this.originalView)
  }

  /** Update the diff content */
  setContent(original: string, modified: string): void {
    this.destroy()
    this.render({
      container: this.container,
      original,
      modified,
    })
  }

  /** Get the original (left) content */
  getOriginal(): string {
    return this.originalView?.state.doc.toString() ?? ''
  }

  /** Get the modified (right) content */
  getModified(): string {
    return this.modifiedView?.state.doc.toString() ?? ''
  }

  /** Destroy both editor views */
  destroy(): void {
    this.originalView?.destroy()
    this.modifiedView?.destroy()
    this.originalView = null
    this.modifiedView = null
    if (this.wrapperEl) {
      this.wrapperEl.remove()
      this.wrapperEl = null
    }
  }
}
