import { getCommitIdFromObjectPath } from '#cli/core/commit-mentions'
import { charCellWidth } from '@simon_he/vue-tui'

interface GraphemeSegment {
  segment: string
  index: number
}

let graphemeSegmenter: Intl.Segmenter | null = null
try {
  graphemeSegmenter =
    typeof Intl !== 'undefined' && 'Segmenter' in Intl
      ? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
      : null
} catch {
  graphemeSegmenter = null
}

function forEachGraphemeWithIndex(
  text: string,
  cb: (seg: GraphemeSegment) => void,
): void {
  if (!text) return
  if (graphemeSegmenter) {
    for (const part of graphemeSegmenter.segment(
      text,
    ) as any as Iterable<GraphemeSegment>)
      cb(part)
    return
  }

  // Fallback: code point iteration with best-effort indices.
  // This will still split variation selector sequences, but avoids crashing on runtimes
  // without Intl.Segmenter.
  let i = 0
  while (i < text.length) {
    const cp = text.codePointAt(i) ?? 0
    const ch = String.fromCodePoint(cp)
    cb({ segment: ch, index: i })
    i += ch.length
  }
}

export interface FileReferenceSegment {
  start: number
  end: number
  absPath: string
  kind: 'file' | 'commit' | 'image'
  commitId?: string
}

function isSafeLocalPathReference(rawPath: string): boolean {
  const p = String(rawPath ?? '')
  if (!p) return false
  if (p.includes('\0')) return false
  if (/[\r\n]/.test(p)) return false
  if (/[;&|`]/.test(p)) return false
  return true
}

export function parseFileReferences(text: string): {
  displayText: string
  segments: FileReferenceSegment[]
} {
  // Supports:
  // - @file references: "@/abs/path" (and "./rel", "../rel")
  // - Inline markdown links: "[label](/abs/path)" or "[label](</abs/path>)"
  const raw = String(text ?? '')
  if (!raw || (!raw.includes('@') && !raw.includes('[')))
    return { displayText: raw, segments: [] }
  const regex =
    /\[([^\]]+)\]\(((?:\\\)|[^)])+)\)|\[?@((?:\/|\.\.?\/)[^\s[\](){}'"`,;:!?]+)\]*/g
  const segments: FileReferenceSegment[] = []
  let displayText = ''
  let lastIndex = 0
  let match: RegExpExecArray | null

  match = regex.exec(raw)
  while (match !== null) {
    const fullMatch = match[0]
    const markdownLabel = match[1]
    const markdownPathRaw = match[2]
    const atPathRaw = match[3]
    const matchStart = match.index

    displayText += raw.slice(lastIndex, matchStart)

    const renderSegment = (
      cleanPath: string,
      displayName: string,
      kind: FileReferenceSegment['kind'],
      commitId?: string,
    ): void => {
      const segmentStart = displayText.length
      displayText += displayName
      const segmentEnd = displayText.length
      segments.push({
        start: segmentStart,
        end: segmentEnd,
        absPath: cleanPath,
        kind,
        commitId,
      })
    }

    if (markdownLabel != null && markdownPathRaw != null) {
      let path = String(markdownPathRaw ?? '').trim()
      if (path.startsWith('<') && path.endsWith('>')) path = path.slice(1, -1)
      path = path.replace(/\\\)/g, ')').trim()
      const commitId = getCommitIdFromObjectPath(path)
      if (commitId) {
        const label = String(markdownLabel ?? '').trim()
        const name = label || commitId
        renderSegment(path, `[${name}]`, 'commit', commitId)
      } else {
        const isAbs = path.startsWith('/') || /^[A-Z]:[\\/]/i.test(path)
        if (isAbs && isSafeLocalPathReference(path)) {
          const label = String(markdownLabel ?? '').trim()
          const name =
            label || (path.split(/[/\\]/).filter(Boolean).pop() ?? path)
          const isImage = /^Image\s*#\d+$/i.test(label)
          renderSegment(path, `[${name}]`, isImage ? 'image' : 'file')
        } else {
          displayText += fullMatch
        }
      }
    } else if (atPathRaw != null) {
      const absPath = String(atPathRaw ?? '')
      const cleanPath = absPath
        .replace(/[\])'"`,;:!?]+$/, '')
        .replace(/[.-]+$/, '')
      const filename = cleanPath.split('/').pop() || cleanPath
      const displayName = `[${filename}]`
      if (isSafeLocalPathReference(cleanPath))
        renderSegment(cleanPath, displayName, 'file')
      else displayText += fullMatch
    } else {
      displayText += fullMatch
    }

    lastIndex = matchStart + fullMatch.length
    match = regex.exec(raw)
  }

  displayText += raw.slice(lastIndex)

  return { displayText, segments }
}

export function wrapTextByWidthWithOffsets(
  text: string,
  width: number,
): Array<{ text: string; start: number; end: number }> {
  width = Math.max(1, Math.floor(width))
  const out: Array<{ text: string; start: number; end: number }> = []

  const s = String(text ?? '')
  if (!s) {
    out.push({ text: '', start: 0, end: 0 })
    return out
  }

  // Fast path: ASCII-only (plus '\n'), fixed 1-cell width per char.
  // Uses word-aware wrapping: prefer breaking at spaces over mid-word.
  let asciiFixedWidth = true
  for (let i = 0; i < s.length; i++) {
    const code = s.charCodeAt(i)
    if (code === 10) continue
    if (code < 0x20 || code === 0x7f || code >= 0x80) {
      asciiFixedWidth = false
      break
    }
  }
  if (asciiFixedWidth) {
    let lineStart = 0
    let cells = 0
    let lastSpaceIdx = -1
    let cellsAtLastSpace = 0

    for (let i = 0; i < s.length; i++) {
      const code = s.charCodeAt(i)
      if (code === 10) {
        out.push({ text: s.slice(lineStart, i), start: lineStart, end: i })
        lineStart = i + 1
        cells = 0
        lastSpaceIdx = -1
        continue
      }
      if (code === 32) {
        lastSpaceIdx = i
        cellsAtLastSpace = cells
      }
      cells++
      if (cells > width) {
        if (lastSpaceIdx >= lineStart) {
          // Word-aware break: wrap at last space boundary
          out.push({ text: s.slice(lineStart, lastSpaceIdx), start: lineStart, end: lastSpaceIdx })
          lineStart = lastSpaceIdx + 1
          cells = cells - cellsAtLastSpace - 1
          lastSpaceIdx = -1
        } else {
          // No space on this line — fall back to character break (overflow-wrap: break-word)
          out.push({ text: s.slice(lineStart, i), start: lineStart, end: i })
          lineStart = i
          cells = 1
        }
      }
    }
    out.push({ text: s.slice(lineStart), start: lineStart, end: s.length })
    return out
  }

  // Non-ASCII path: grapheme-aware, word-aware wrapping.
  // Track last space as a pending break opportunity (à la pretext's greedy line fitter).
  let lineStart = 0
  let cells = 0
  let lastSpaceBytePos = -1
  let cellsIncludingSpace = 0

  forEachGraphemeWithIndex(s, ({ segment, index }) => {
    if (segment === '\n') {
      const safeEnd = Math.max(lineStart, index)
      out.push({ text: s.slice(lineStart, safeEnd), start: lineStart, end: safeEnd })
      lineStart = index + 1
      cells = 0
      lastSpaceBytePos = -1
      return
    }

    if (segment === ' ') {
      lastSpaceBytePos = index
      cellsIncludingSpace = cells + 1
    }

    const w = charCellWidth(segment)
    cells += w

    if (cells > width) {
      if (lastSpaceBytePos >= lineStart) {
        // Word-aware break at last space
        out.push({ text: s.slice(lineStart, lastSpaceBytePos), start: lineStart, end: lastSpaceBytePos })
        lineStart = lastSpaceBytePos + 1
        cells = cells - cellsIncludingSpace
        lastSpaceBytePos = -1
      } else if (cells > w) {
        // No space — break before current grapheme (overflow-wrap: break-word)
        out.push({ text: s.slice(lineStart, index), start: lineStart, end: index })
        lineStart = index
        cells = w
      } else {
        // Single grapheme exceeds width — emit it alone
        const end = index + segment.length
        out.push({ text: s.slice(lineStart, end), start: lineStart, end })
        lineStart = end
        cells = 0
      }
    }
  })

  out.push({ text: s.slice(lineStart), start: lineStart, end: s.length })
  return out
}

export function truncateByCells(
  text: string,
  maxCells: number,
): { text: string; truncated: boolean } {
  const limit = Math.max(0, Math.floor(maxCells))
  if (!limit) return { text: '', truncated: text.length > 0 }
  const s = String(text ?? '')
  if (!s) return { text: '', truncated: false }

  // Fast path: ASCII-only, fixed 1-cell width per char.
  let asciiFixedWidth = true
  for (let i = 0; i < s.length; i++) {
    const code = s.charCodeAt(i)
    if (code < 0x20 || code === 0x7f || code >= 0x80) {
      asciiFixedWidth = false
      break
    }
  }
  if (asciiFixedWidth) {
    if (s.length <= limit) return { text: s, truncated: false }
    return { text: s.slice(0, limit), truncated: true }
  }

  let cells = 0
  let end = 0

  forEachGraphemeWithIndex(s, ({ segment, index }) => {
    const w = charCellWidth(segment)
    if (cells + w > limit) return
    cells += w
    end = index + segment.length
  })
  return { text: s.slice(0, end), truncated: end < s.length }
}

export type DiffLineType = 'context' | 'added' | 'removed'

export interface DiffLine {
  type: DiffLineType
  oldLine?: number
  newLine?: number
  content: string
}

// Security: Size limits to prevent OOM on large diffs
// Keep this low: the current LCS implementation allocates an O(m*n) matrix.
// Even "only" 10k lines would be catastrophic in JS (hundreds of MB+).
const MAX_DIFF_LINES = 2000 // Maximum number of lines per file
const MAX_DIFF_SIZE = 5_000_000 // Maximum file size in bytes (5MB)

function utf8ByteLength(text: string): number {
  const buf = (globalThis as any)?.Buffer
  if (buf && typeof buf.byteLength === 'function')
    return buf.byteLength(text, 'utf8')

  // Fallback: compute UTF-8 length without allocating an encoded buffer.
  // Matches TextEncoder behavior for surrogate pairs; unpaired surrogates are treated
  // as replacement chars (3 bytes).
  let bytes = 0
  for (let i = 0; i < text.length; i++) {
    const code = text.charCodeAt(i)
    if (code < 0x80) {
      bytes += 1
      continue
    }
    if (code < 0x800) {
      bytes += 2
      continue
    }
    // Surrogates
    if (code >= 0xd800 && code <= 0xdbff) {
      const next = text.charCodeAt(i + 1)
      if (next >= 0xdc00 && next <= 0xdfff) {
        bytes += 4
        i++
      } else {
        bytes += 3
      }
      continue
    }
    if (code >= 0xdc00 && code <= 0xdfff) {
      bytes += 3
      continue
    }
    bytes += 3
  }
  return bytes
}

/**
 * Computes a simple line-based diff between two strings.
 * Uses a basic algorithm to find common lines and identify added/removed lines.
 *
 * Security: Implements size limits to prevent OOM attacks. For files exceeding
 * the limits, returns a summary instead of a full diff.
 */
export function computeLineDiff(oldText: string, newText: string): DiffLine[] {
  // Security: Check size limits to prevent OOM
  const oldSize = utf8ByteLength(oldText)
  const newSize = utf8ByteLength(newText)

  if (oldSize > MAX_DIFF_SIZE || newSize > MAX_DIFF_SIZE) {
    return [
      {
        type: 'context',
        content: `[Diff too large: ${Math.max(oldSize, newSize).toLocaleString()} bytes. Size limit: ${MAX_DIFF_SIZE.toLocaleString()} bytes]`,
      },
    ]
  }

  const oldLines = oldText.replace(/\r/g, '').split('\n')
  const newLines = newText.replace(/\r/g, '').split('\n')

  // Use a simple longest common subsequence (LCS) approach for line diffing
  const m = oldLines.length
  const n = newLines.length

  // Security: Check line count limits to prevent OOM
  if (m > MAX_DIFF_LINES || n > MAX_DIFF_LINES) {
    return [
      {
        type: 'context',
        content: `[Diff too large: ${Math.max(m, n).toLocaleString()} lines. Line limit: ${MAX_DIFF_LINES.toLocaleString()} lines]`,
      },
    ]
  }

  // Build LCS matrix (compact): Uint16Array avoids array-of-arrays overhead and
  // keeps peak memory bounded to ~8MB at 2000x2000.
  const rowSize = n + 1
  const dp = new Uint16Array((m + 1) * rowSize)

  for (let i = 1; i <= m; i++) {
    const rowOffset = i * rowSize
    const prevRowOffset = (i - 1) * rowSize
    const oldLine = oldLines[i - 1]
    for (let j = 1; j <= n; j++) {
      const idx = rowOffset + j
      if (oldLine === newLines[j - 1]) {
        dp[idx] = dp[prevRowOffset + j - 1] + 1
      } else {
        const up = dp[prevRowOffset + j]
        const left = dp[rowOffset + j - 1]
        dp[idx] = up > left ? up : left
      }
    }
  }

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

  while (i > 0 || j > 0) {
    if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
      // Common line
      result.push({
        type: 'context',
        oldLine: i,
        newLine: j,
        content: newLines[j - 1],
      })
      i--
      j--
    } else if (
      j > 0 &&
      (i === 0 || dp[i * rowSize + (j - 1)] >= dp[(i - 1) * rowSize + j])
    ) {
      // Added line
      result.push({ type: 'added', newLine: j, content: newLines[j - 1] })
      j--
    } else if (
      i > 0 &&
      (j === 0 || dp[i * rowSize + (j - 1)] < dp[(i - 1) * rowSize + j])
    ) {
      // Removed line
      result.push({ type: 'removed', oldLine: i, content: oldLines[i - 1] })
      i--
    }
  }

  result.reverse()
  return result
}

/**
 * Computes diff statistics (added/removed line counts).
 */
export function computeDiffStats(
  oldText: string,
  newText: string,
): { added: number; removed: number } {
  const diff = computeLineDiff(oldText, newText)
  let added = 0
  let removed = 0

  for (const line of diff) {
    if (line.type === 'added') added++
    else if (line.type === 'removed') removed++
  }

  return { added, removed }
}
