const DEFAULT_THEME = 'github-dark'

function normalizePathBasename(filePath: string): string {
  const s = String(filePath ?? '').replace(/\\/g, '/')
  const parts = s.split('/').filter(Boolean)
  return (parts[parts.length - 1] ?? '').trim()
}

export function guessLanguageFromFilePath(filePath: string): string | null {
  const base = normalizePathBasename(filePath).toLowerCase()
  if (!base)
    return null

  if (base === 'dockerfile')
    return 'dockerfile'
  if (base === 'makefile')
    return 'make'

  if (base.endsWith('.d.ts'))
    return 'ts'

  const ext = base.includes('.') ? (base.split('.').pop() ?? '') : ''
  switch (ext) {
    case 'ts':
      return 'ts'
    case 'tsx':
      return 'tsx'
    case 'js':
      return 'js'
    case 'jsx':
      return 'jsx'
    case 'mjs':
      return 'js'
    case 'cjs':
      return 'js'
    case 'json':
      return 'json'
    case 'jsonc':
      return 'jsonc'
    case 'md':
      return 'md'
    case 'vue':
      return 'vue'
    case 'html':
      return 'html'
    case 'css':
      return 'css'
    case 'scss':
      return 'scss'
    case 'less':
      return 'less'
    case 'yml':
      return 'yaml'
    case 'yaml':
      return 'yaml'
    case 'toml':
      return 'toml'
    case 'xml':
      return 'xml'
    case 'sql':
      return 'sql'
    case 'sh':
      return 'bash'
    case 'bash':
      return 'bash'
    case 'zsh':
      return 'bash'
    case 'fish':
      return 'fish'
    case 'py':
      return 'python'
    case 'go':
      return 'go'
    case 'rs':
      return 'rust'
    case 'c':
      return 'c'
    case 'cc':
      return 'cpp'
    case 'cpp':
      return 'cpp'
    case 'h':
      return 'cpp'
    case 'hpp':
      return 'cpp'
    case 'java':
      return 'java'
    case 'kt':
      return 'kotlin'
    case 'swift':
      return 'swift'
    case 'rb':
      return 'ruby'
    case 'php':
      return 'php'
    case 'diff':
      return 'diff'
    case 'patch':
      return 'diff'
    case 'graphql':
      return 'graphql'
    case 'gql':
      return 'graphql'
    default:
      return 'text'
  }
}

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
}

function sgr(codes: number[]): string {
  return `\u001B[${codes.join(';')}m`
}

function fgRgb(rgb: { r: number, g: number, b: number }): string {
  return sgr([38, 2, rgb.r, rgb.g, rgb.b])
}

function styleFromFontStyleBits(fontStyle: number | undefined): number[] {
  // Shiki uses VSCode TextMate bits:
  // 1 italic, 2 bold, 4 underline, 8 strikethrough.
  const bits = Number(fontStyle ?? 0)
  const out: number[] = []
  if (bits & 2)
    out.push(1)
  if (bits & 1)
    out.push(3)
  if (bits & 4)
    out.push(4)
  return out
}

function truncateByLines(
  text: string,
  maxLines: number,
): { text: string, truncated: boolean } {
  const lines = String(text ?? '')
    .replace(/\r/g, '')
    .split('\n')
  if (lines.length <= maxLines)
    return { text: lines.join('\n'), truncated: false }
  return { text: lines.slice(0, maxLines).join('\n'), truncated: true }
}

export async function highlightCodePreviewToAnsi(
  opts: Readonly<{
    code: string
    language: string
    theme?: string
    maxLines?: number
  }>,
): Promise<string | null> {
  const lang = String(opts.language ?? '').trim() || 'text'
  const theme = String(opts.theme ?? DEFAULT_THEME).trim() || DEFAULT_THEME
  const maxLines = Math.max(1, Math.floor(opts.maxLines ?? 24))

  const cut = truncateByLines(opts.code, maxLines)
  const code = cut.text
  if (!code.trim())
    return null

  try {
    const shiki: any = await import('shiki')
    const codeToTokensBase = shiki.codeToTokensBase as (
      code: string,
      opts: any,
    ) => Promise<any[]>
    const getSingletonHighlighter
      = shiki.getSingletonHighlighter as () => Promise<any>
    if (
      typeof codeToTokensBase !== 'function'
      || typeof getSingletonHighlighter !== 'function'
    ) {
      return null
    }

    const tokenLines = await codeToTokensBase(code, { lang, theme })
    const highlighter = await getSingletonHighlighter()
    const themeReg = highlighter?.getTheme ? highlighter.getTheme(theme) : null
    const defaultFg = String(themeReg?.fg ?? '#ffffff')
    const defaultRgb = hexToRgb(defaultFg) ?? { r: 0xFF, g: 0xFF, b: 0xFF }

    let output = ''
    for (let i = 0; i < tokenLines.length; i++) {
      const line = tokenLines[i]
      for (const token of line as any[]) {
        const rgb = hexToRgb(token?.color) ?? defaultRgb
        const codes = styleFromFontStyleBits(token?.fontStyle)
        output += fgRgb(rgb)
        if (codes.length)
          output += sgr(codes)
        output += String(token?.content ?? '')
        output += sgr([0])
      }
      if (i < tokenLines.length - 1)
        output += '\n'
    }

    return output
  }
  catch {
    return null
  }
}
