import type {
  ReadPreview,
  ReadPreviewLine,
  ShikiToken,
} from '#cli/core/model'
import { debugLog } from '#cli/core/debug-log'
import { codeToTokensBaseOffThread } from '#cli/core/shiki-worker'
import { guessLanguageFromFilePath } from '#cli/core/syntax-highlight'
import { splitReadGutter, stripReadLineNumberPadding } from './read-utils'
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>

type RuntimeGlobals = typeof globalThis & {
  process?: {
    env?: Record<string, string | undefined>
  }
  performance?: {
    now?: () => number
  }
}

const DEFAULT_SHIKI_THEME = 'monokai'
const runtimeGlobals = globalThis as RuntimeGlobals
const runtimeProcessEnv = runtimeGlobals.process?.env ?? {}
const PROFILE_SHIKI = String(runtimeProcessEnv.DIMCODE_PROFILE_SHIKI ?? '') === '1'
const SHIKI_SLOW_MS = (() => {
  const raw = String(runtimeProcessEnv.DIMCODE_PROFILE_SHIKI_SLOW_MS ?? '').trim()
  const n = Number(raw)
  if (Number.isFinite(n) && n > 0)
    return Math.min(60_000, Math.max(10, Math.floor(n)))
  return 120
})()

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

function nowMs(): number {
  const p = runtimeGlobals.performance
  if (p && typeof p.now === 'function') return p.now()
  return Date.now()
}

function getTextHash(text: string): string {
  // Cheap stable hash (same algo style as core/markdown.ts).
  let hash = 0
  const str = String(text ?? '')
  for (let i = 0; i < str.length; i++) {
    const ch = str.charCodeAt(i)
    hash = (hash << 5) - hash + ch
    hash = hash & hash
  }
  return hash.toString(36)
}

function hashAppend(prev: number, text: string): number {
  let hash = prev | 0
  const str = String(text ?? '')
  for (let i = 0; i < str.length; i++) {
    const ch = str.charCodeAt(i)
    hash = (hash << 5) - hash + ch
    hash = hash & hash
  }
  return hash
}

function extractTextLinesFromJsonish(seg: string): string[] | null {
  const raw = String(seg ?? '').trim()
  if (!raw) return null
  // Fast path: avoid throwing JSON.parse on plain text tool output.
  if (!(raw.startsWith('{') || raw.startsWith('['))) return null
  try {
    const parsed = JSON.parse(raw)
    const items = Array.isArray(parsed) ? parsed : [parsed]
    const out: string[] = []
    for (const item of items) {
      if (!isRecord(item) || item.type !== 'text') continue
      const text = String(item.text ?? '').replace(/\r/g, '')
      if (text) out.push(...text.split('\n'))
    }
    return out.length > 0 ? out : null
  } catch {
    return null
  }
}

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 toShikiTokens(tokenLine: unknown): ShikiToken[] | null {
  if (!Array.isArray(tokenLine)) return null
  return tokenLine.map((token) => {
    const content = isRecord(token) ? String(token.content ?? '') : ''
    return {
      content,
      ...(isRecord(token) && typeof token.color === 'string' && token.color
        ? { color: String(token.color) }
        : {}),
      ...(isRecord(token) && typeof token.fontStyle === 'number'
        ? { fontStyle: token.fontStyle }
        : {}),
    }
  })
}

export async function computeMarkdownCodeBlockPreviewWithShiki(
  code: string,
  opts: Readonly<{
    language?: string
    theme?: string
    loadShiki?: LoadShiki
  }>,
): Promise<
  import('#cli/core/model').MarkdownCodeBlockPreview
> {
  const t0 = PROFILE_SHIKI ? nowMs() : 0
  const safe = String(code ?? '').replace(/\r/g, '')
  const language = String(opts.language ?? 'text').trim() || 'text'
  const theme =
    String(opts.theme ?? DEFAULT_SHIKI_THEME).trim() || DEFAULT_SHIKI_THEME
  const loadShiki: LoadShiki = opts.loadShiki ?? loadDefaultShiki

  const lines = safe.split('\n')
  const hash = getTextHash(`${language}\0${safe}`)
  const totalLines = lines.length

  const sanitizedLines = Array.from({ length: totalLines }, () => '')
  for (let i = 0; i < totalLines; i++)
    sanitizedLines[i] = sanitizeTerminalText(lines[i] ?? '')
  const snippet = sanitizedLines.join('\n')

  let tokenLines: ShikiTokenLineInput[] | null = null
  if (opts.loadShiki) {
    let shiki: unknown
    try {
      shiki = await loadShiki()
    } catch {
      return { kind: 'skipped', hash, totalLines, reason: 'shiki_unavailable' }
    }
    const codeToTokensBase = resolveCodeToTokensBase(shiki)
    if (typeof codeToTokensBase !== 'function')
      return { kind: 'skipped', hash, totalLines, reason: 'shiki_unavailable' }
    try {
      tokenLines = normalizeTokenLines(
        await codeToTokensBase(snippet, { lang: language, theme }),
      )
    } catch {
      tokenLines = null
    }
  } else {
    const tOffStart = PROFILE_SHIKI ? nowMs() : 0
    try {
      tokenLines = normalizeTokenLines(
        await codeToTokensBaseOffThread(snippet, {
          lang: language,
          theme,
        }),
      )
    }
    catch {
      tokenLines = null
    }
    const tOffEnd = PROFILE_SHIKI ? nowMs() : 0
    if (tokenLines == null) {
      if (PROFILE_SHIKI) {
        const elapsed = tOffEnd - tOffStart
        debugLog('shiki_offthread_failed_fallback', {
          language,
          theme,
          chars: snippet.length,
          elapsedMs: Math.round(elapsed),
        })
      }
      let shiki: unknown
      try {
        shiki = await loadShiki()
      } catch {
        return {
          kind: 'skipped',
          hash,
          totalLines,
          reason: 'shiki_unavailable',
        }
      }
      const codeToTokensBase = resolveCodeToTokensBase(shiki)
      if (typeof codeToTokensBase !== 'function')
        return {
          kind: 'skipped',
          hash,
          totalLines,
          reason: 'shiki_unavailable',
        }
      try {
        tokenLines = normalizeTokenLines(
          await codeToTokensBase(snippet, { lang: language, theme }),
        )
      } catch {
        tokenLines = null
      }
    }
  }

  const previewLines: Array<{
    content: string
    tokens: readonly ShikiToken[] | null
  }> = []
  for (let i = 0; i < totalLines; i++) {
    const content = sanitizedLines[i] ?? ''
    const tokens =
      tokenLines && Array.isArray(tokenLines)
        ? toShikiTokens(tokenLines[i])
        : null
    previewLines.push({ content, tokens })
  }

  if (PROFILE_SHIKI) {
    const elapsed = nowMs() - t0
    if (elapsed >= SHIKI_SLOW_MS) {
      debugLog('shiki_preview_slow', {
        language,
        theme,
        totalLines,
        chars: snippet.length,
        elapsedMs: Math.round(elapsed),
      })
    }
  }

  return {
    kind: 'shiki',
    language,
    theme,
    hash,
    totalLines,
    lines: previewLines,
  }
}

export async function computeReadPreviewWithShiki(
  output: string,
  opts: Readonly<{
    filePath?: string
    language?: string
    theme?: string
    mode: 'collapsed' | 'expanded'
    maxLines?: number
    loadShiki?: LoadShiki
  }>,
): Promise<ReadPreview | null> {
  const t0 = PROFILE_SHIKI ? nowMs() : 0
  const jsonLines = extractTextLinesFromJsonish(output)
  const rawLines =
    jsonLines ??
    String(output ?? '')
      .replace(/\r/g, '')
      .split('\n')

  const totalLines = rawLines.length

  const maxLines = Math.max(1, Math.floor(opts.maxLines ?? 10))
  const takeLines =
    opts.mode === 'collapsed' ? Math.min(totalLines, maxLines) : totalLines

  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 loadShiki: LoadShiki = opts.loadShiki ?? loadDefaultShiki

  const previewGutters = Array.from({ length: takeLines }, () => '')
  const previewContents = Array.from({ length: takeLines }, () => '')
  const snippetParts = Array.from({ length: takeLines }, () => '')

  let hashNum = 0
  for (let i = 0; i < totalLines; i++) {
    const normalized = stripReadLineNumberPadding(rawLines[i] ?? '').text
    if (i > 0) hashNum = hashAppend(hashNum, '\n')
    hashNum = hashAppend(hashNum, normalized)

    if (i >= takeLines) continue
    const split = splitReadGutter(normalized)
    previewGutters[i] = split?.gutter ?? ''
    previewContents[i] = split?.content ?? normalized
    snippetParts[i] = sanitizeTerminalText(previewContents[i] ?? '')
  }
  const hash = hashNum.toString(36)

  const snippet = snippetParts.join('\n')

  let tokenLines: ShikiTokenLineInput[] | null = null
  if (opts.loadShiki) {
    let shiki: unknown
    try {
      shiki = await loadShiki()
    } catch {
      return {
        kind: 'skipped',
        mode: opts.mode,
        hash,
        totalLines,
        reason: 'shiki_unavailable',
      }
    }
    const codeToTokensBase = resolveCodeToTokensBase(shiki)
    if (typeof codeToTokensBase !== 'function')
      return {
        kind: 'skipped',
        mode: opts.mode,
        hash,
        totalLines,
        reason: 'shiki_unavailable',
      }
    try {
      tokenLines = normalizeTokenLines(
        await codeToTokensBase(snippet, { lang: language, theme }),
      )
    } catch {
      tokenLines = null
    }
  } else {
    const tOffStart = PROFILE_SHIKI ? nowMs() : 0
    try {
      tokenLines = normalizeTokenLines(
        await codeToTokensBaseOffThread(snippet, {
          lang: language,
          theme,
        }),
      )
    }
    catch {
      tokenLines = null
    }
    const tOffEnd = PROFILE_SHIKI ? nowMs() : 0
    if (tokenLines == null) {
      if (PROFILE_SHIKI) {
        const elapsed = tOffEnd - tOffStart
        debugLog('shiki_offthread_failed_fallback', {
          mode: opts.mode,
          language,
          theme,
          chars: snippet.length,
          elapsedMs: Math.round(elapsed),
        })
      }
      let shiki: unknown
      try {
        shiki = await loadShiki()
      } catch {
        return {
          kind: 'skipped',
          mode: opts.mode,
          hash,
          totalLines,
          reason: 'shiki_unavailable',
        }
      }
      const codeToTokensBase = resolveCodeToTokensBase(shiki)
      if (typeof codeToTokensBase !== 'function')
        return {
          kind: 'skipped',
          mode: opts.mode,
          hash,
          totalLines,
          reason: 'shiki_unavailable',
        }
      try {
        tokenLines = normalizeTokenLines(
          await codeToTokensBase(snippet, { lang: language, theme }),
        )
      } catch {
        tokenLines = null
      }
    }
  }

  const previewLines: ReadPreviewLine[] = []
  for (let i = 0; i < takeLines; i++) {
    const tokens =
      tokenLines && Array.isArray(tokenLines)
        ? toShikiTokens(tokenLines[i])
        : null
    previewLines.push({
      gutter: previewGutters[i] ?? '',
      content: previewContents[i] ?? '',
      tokens,
    })
  }

  if (PROFILE_SHIKI) {
    const elapsed = nowMs() - t0
    if (elapsed >= SHIKI_SLOW_MS) {
      debugLog('shiki_read_preview_slow', {
        mode: opts.mode,
        language,
        theme,
        totalLines,
        takeLines,
        chars: snippet.length,
        elapsedMs: Math.round(elapsed),
      })
    }
  }

  return {
    kind: 'shiki',
    mode: opts.mode,
    language,
    theme,
    hash,
    totalLines,
    lines: previewLines,
  }
}
