import type {
  DiffPreview,
  DiffPreviewLine,
  ShikiToken,
} from '#cli/core/model'
import type { AnsiColorName, Style } from '@simon_he/vue-tui'
import type { UiThemeContext } from '../theme'
import type { LineSegment } from './types'
import { codeToTokensBaseOffThread } from '#cli/core/shiki-worker'
import { guessLanguageFromFilePath } from '#cli/core/syntax-highlight'
import { textCellWidth } from '@simon_he/vue-tui'
import { getTrimmedDiffLines } from './diff-renderer'
import { sanitizeTerminalText } from './sanitize-terminal-text'

type ShikiTokenInput = Readonly<{
  content?: unknown
  color?: unknown
  fontStyle?: unknown
}>

type ShikiTokenLineInput = readonly ShikiTokenInput[]

type ShikiCodeToTokensBase = (
  code: string,
  opts: Readonly<{ lang: string, theme: string }>,
) => Promise<unknown>

type LoadShiki = () => Promise<unknown>

const DEFAULT_SHIKI_THEME = 'monokai'

function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
  const s = String(hex ?? '').trim()
  const m = s.match(/^#?([0-9a-f]{6})([0-9a-f]{2})?$/i)
  if (!m) return null
  const v = m[1]!
  const r = Number.parseInt(v.slice(0, 2), 16)
  const g = Number.parseInt(v.slice(2, 4), 16)
  const b = Number.parseInt(v.slice(4, 6), 16)
  return Number.isFinite(r) && Number.isFinite(g) && Number.isFinite(b)
    ? { r, g, b }
    : null
}

const ANSI16_HEX: Readonly<Record<AnsiColorName, string>> = Object.freeze({
  black: '#000000',
  red: '#c91b00',
  green: '#00c200',
  yellow: '#c7c400',
  blue: '#0225c7',
  magenta: '#c930c7',
  cyan: '#00c5c7',
  white: '#c7c7c7',
  blackBright: '#686868',
  redBright: '#ff6e67',
  greenBright: '#5ffa68',
  yellowBright: '#fffc67',
  blueBright: '#6871ff',
  magentaBright: '#ff76ff',
  cyanBright: '#5ffdff',
  whiteBright: '#ffffff',
})

type PaletteKey = Readonly<Partial<Record<AnsiColorName, string>>>
const nearestCacheByPalette = new WeakMap<PaletteKey, Map<string, AnsiColorName>>()
const nearestCacheAnsi16 = new Map<string, AnsiColorName>()

function isRecord(value: unknown): value is Record<string, unknown> {
  return value != null && typeof value === 'object' && !Array.isArray(value)
}

function normalizeTokenLines(value: unknown): ShikiTokenLineInput[] | null {
  return Array.isArray(value) ? (value as ShikiTokenLineInput[]) : null
}

function resolveCodeToTokensBase(
  shiki: unknown,
): ShikiCodeToTokensBase | null {
  if (!isRecord(shiki))
    return null
  const codeToTokensBase = shiki.codeToTokensBase
  return typeof codeToTokensBase === 'function'
    ? (codeToTokensBase as ShikiCodeToTokensBase)
    : null
}

async function loadDefaultShiki(): Promise<unknown> {
  return await import('shiki')
}

function nearestAnsiColorName(
  hex: string,
  ui: UiThemeContext,
): AnsiColorName | null {
  const rgb = hexToRgb(hex)
  if (!rgb) return null

  const palette: PaletteKey =
    ui.capability.level === 256 && ui.palette
      ? ui.palette as PaletteKey
      : ANSI16_HEX

  const cache =
    ui.capability.level === 256 && ui.palette
      ? (nearestCacheByPalette.get(ui.palette as PaletteKey) ??
        (() => {
          const m = new Map<string, AnsiColorName>()
          nearestCacheByPalette.set(ui.palette as PaletteKey, m)
          return m
        })())
      : nearestCacheAnsi16

  const key = `${ui.capability.level}:${hex.toLowerCase()}`
  const cached = cache.get(key)
  if (cached) return cached

  let best: AnsiColorName = 'white'
  let bestDist = Number.POSITIVE_INFINITY
  for (const [name, pHex] of Object.entries(palette) as Array<
    [AnsiColorName, string | undefined]
  >) {
    if (typeof pHex !== 'string')
      continue
    const pRgb = hexToRgb(pHex)
    if (!pRgb) continue
    const dr = rgb.r - pRgb.r
    const dg = rgb.g - pRgb.g
    const db = rgb.b - pRgb.b
    const dist = dr * dr + dg * dg + db * db
    if (dist < bestDist) {
      bestDist = dist
      best = name
    }
  }

  cache.set(key, best)
  return best
}

function styleFromFontStyleBits(
  fontStyle: number | undefined,
): Pick<Style, 'bold' | 'italic' | 'underline'> {
  const bits = Number(fontStyle ?? 0)
  return {
    ...(bits & 2 ? { bold: true } : {}),
    ...(bits & 1 ? { italic: true } : {}),
    ...(bits & 4 ? { underline: true } : {}),
  }
}

export function shikiTokensToSegments(
  tokens: readonly ShikiToken[] | null,
  opts: Readonly<{
    ui: UiThemeContext
    baseX: number
    baseStyle?: Style
    text: string
  }>,
): LineSegment[] {
  if (!tokens || tokens.length === 0) return []

  const baseDim = opts.baseStyle?.dim === true
  let x = opts.baseX
  const segments: LineSegment[] = []

  for (const t of tokens) {
    const tokenText = sanitizeTerminalText(String(t?.content ?? ''))
    const w = textCellWidth(tokenText)
    if (w <= 0) {
      continue
    }

    const colorName = t?.color ? nearestAnsiColorName(t.color, opts.ui) : null
    const font = styleFromFontStyleBits(t?.fontStyle)
    const style: Style = {
      ...(baseDim ? { dim: true } : {}),
      ...(colorName ? { fg: colorName } : {}),
      ...font,
    }

    // Avoid emitting segments that don't actually change style.
    if (Object.keys(style).length > 0) {
      segments.push({
        x,
        w,
        text: tokenText,
        style,
      })
    }
    x += w
  }

  return segments
}

export async function computeDiffPreviewWithShiki(
  oldString: string,
  newString: string,
  opts?: Readonly<{
    filePath?: string
    language?: string
    theme?: string
    maxLines?: number
    loadShiki?: LoadShiki
  }>,
): Promise<DiffPreview | null> {
  const langFromPath = opts?.filePath
    ? guessLanguageFromFilePath(opts.filePath)
    : null
  const language =
    String(opts?.language ?? langFromPath ?? 'text').trim() || 'text'
  const theme =
    String(opts?.theme ?? DEFAULT_SHIKI_THEME).trim() || DEFAULT_SHIKI_THEME
  const maxLines = Math.max(1, Math.floor(opts?.maxLines ?? 24))
  const loadShiki: LoadShiki = opts?.loadShiki ?? loadDefaultShiki

  const lines = getTrimmedDiffLines(oldString, newString, { maxLines })
  if (lines.length === 0) return null

  const previewLines: DiffPreviewLine[] = []
  const safeLines = lines.map((l) =>
    sanitizeTerminalText(String(l.content ?? '')),
  )
  const snippet = safeLines.join('\n')

  let tokenLines: ShikiTokenLineInput[] | null = null
  if (opts?.loadShiki) {
    let shiki: unknown
    try {
      shiki = await loadShiki()
    } catch {
      return null
    }
    const codeToTokensBase = resolveCodeToTokensBase(shiki)
    if (typeof codeToTokensBase !== 'function') return null
    try {
      tokenLines = normalizeTokenLines(
        await codeToTokensBase(snippet, { lang: language, theme }),
      )
    } catch {
      tokenLines = null
    }
  } else {
    try {
      tokenLines = normalizeTokenLines(
        await codeToTokensBaseOffThread(snippet, {
          lang: language,
          theme,
        }),
      )
    }
    catch {
      tokenLines = null
    }
    if (tokenLines == null) {
      let shiki: unknown
      try {
        shiki = await loadShiki()
      } catch {
        return null
      }
      const codeToTokensBase = resolveCodeToTokensBase(shiki)
      if (typeof codeToTokensBase !== 'function') return null
      try {
        tokenLines = normalizeTokenLines(
          await codeToTokensBase(snippet, { lang: language, theme }),
        )
      } catch {
        tokenLines = null
      }
    }
  }

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i]!
    const safeContent = safeLines[i] ?? ''
    const tl = tokenLines ? tokenLines[i] : null
    const tokens = Array.isArray(tl)
      ? tl.map((token) => ({
          content: String(token.content ?? ''),
          ...(typeof token.color === 'string' && token.color
            ? { color: String(token.color) }
            : {}),
          ...(typeof token.fontStyle === 'number'
            ? { fontStyle: token.fontStyle }
            : {}),
        }))
      : null

    previewLines.push({ type: line.type, content: safeContent, tokens })
  }

  return {
    kind: 'shiki',
    language,
    theme,
    oldString,
    newString,
    lines: previewLines,
  }
}
