import type { DimSdkStore } from '#cli/core/store'
import type { ShortcutBindings } from '#cli/core/ui-settings'
import type { AnsiColorName, Style } from '@simon_he/vue-tui'
import type { ComputedRef, Ref } from 'vue'
import type { UiThemeContext } from '../../../ui/theme'
import { formatShortcutLabel } from '#cli/core/ui-settings'
import {
  isDoneStatusText,
  isErrorStatusText,
  t,
} from '#cli/i18n'
import {
  getBuiltinProvider,
  getModelDisplayName,
  getProviderDisplayName,
  isCustomProviderId,
} from '#cli/compat/providers'
import { computed, ref, watch } from 'vue'
import {
  normalizeLastRequestBudget,
} from '../../../core/store/request-budget'

const FALLBACK_THINKING_WORDS = [
  'thinking',
  'reasoning',
  'planning',
  'pondering',
  'analyzing',
  'drafting',
  'refining',
  'composing',
]

function parseThinkingWords(raw: string): string[] {
  if (!raw || raw === 'chatPage.thinkingWords') return []
  return raw
    .split(/[,\n|]/)
    .map((word) => word.trim())
    .filter(Boolean)
}

function splitWordLetters(word: string): string[] {
  const trimmed = word.trim()
  if (!trimmed) return []
  return Array.from(trimmed.replace(/\s+/g, ''))
}

function computeTokenUsagePct(tokens: number, budget: number): number | null {
  if (!Number.isFinite(tokens) || tokens < 0)
    return null
  if (!Number.isFinite(budget) || budget <= 0)
    return null
  return Math.min(
    100,
    Math.max(
      0,
      Math.round(tokens / Math.max(1, budget) * 100),
    ),
  )
}

export function useChatStatus(
  opts: Readonly<{
    store: DimSdkStore
    ui: UiThemeContext
    uiMode: Ref<'plan' | 'agent'>
    isCompacting?: Ref<boolean>
    invalidate?: (options?: { priority?: 'high' | 'normal' | 'low' }) => void
    shortcuts?: ComputedRef<ShortcutBindings>
    contentW?: ComputedRef<number>
  }>,
): Readonly<{
  headerStatsDisplay: ComputedRef<string>
  ctxPct: ComputedRef<number | null>
  ctxPctStyle: ComputedRef<Style>
  ctxPctChip: ComputedRef<string>
  modelInfo: ComputedRef<{ build: string; model: string; providerName: string }>
  modeLabel: ComputedRef<string>
  assistantStatusDisplay: ComputedRef<string>
  assistantStatusStyle: ComputedRef<Style>
  isLoading: ComputedRef<boolean>
  loadingFrame: Ref<number>
  loadingAnimIntervalMs: number
  loadingSpinnerCols: ComputedRef<readonly [Style, Style]>
  loadingSpinnerGlyph: ComputedRef<string>
  loadingSpinnerGlyphStyle: ComputedRef<Style>
  thinkingStatusDisplay: ComputedRef<string>
  thinkingStatusStyle: ComputedRef<Style>
  footerRight: ComputedRef<string>
  footerRightFull: ComputedRef<string>
  footerRightCompact: ComputedRef<string>
  footerRightMicro: ComputedRef<string>
  footerRightMini: ComputedRef<string>
}> {
  const totalUsageTokens = computed(() =>
    Math.max(0, Number(opts.store.state.usage.total_tokens) || 0),
  )
  const configuredContextWindow = computed(() =>
    Math.max(0, Number(opts.store.state.config.context_window) || 0),
  )
  const lastRequestBudget = computed(() =>
    normalizeLastRequestBudget((opts.store.state as any)?.lastRequestBudget),
  )
  const compactTriggerPct = computed(() => {
    const liveBudget = lastRequestBudget.value
    const thresholdTokens = Number(
      liveBudget?.thresholdTokens ?? liveBudget?.inputBudgetTokens ?? 0,
    )
    const maxInputTokens = Number(
      liveBudget?.maxInputTokens ?? liveBudget?.inputBudgetTokens ?? 0,
    )
    if (!liveBudget || thresholdTokens <= 0 || maxInputTokens <= 0)
      return null
    return Math.min(
      100,
      Math.max(0, Math.round(thresholdTokens / Math.max(1, maxInputTokens) * 100)),
    )
  })

  const headerStatsDisplay = computed(() => {
    const msgCount = opts.store.state.messages.length
    const tokens = totalUsageTokens.value
    return t('chatPage.headerStats', {
      messages: msgCount.toLocaleString(),
      tokens: tokens.toLocaleString(),
    })
  })

  function ringForPct(pct: number): string {
    if (pct >= 88) return '●'
    if (pct >= 63) return '◕'
    if (pct >= 38) return '◑'
    if (pct >= 13) return '◔'
    return '○'
  }

  function formatDuration(ms: number): string {
    if (!Number.isFinite(ms) || ms <= 0) return ''
    if (ms < 1000) return `${Math.round(ms)}ms`
    const sec = ms / 1000
    if (sec < 60) {
      const digits = sec < 10 ? 1 : 0
      return `${sec.toFixed(digits)}s`
    }
    const mins = Math.floor(sec / 60)
    const rem = Math.round(sec - mins * 60)
    return `${mins}m${String(rem).padStart(2, '0')}s`
  }
  const ctxPct = computed(() => {
    const liveBudget = lastRequestBudget.value
    const contextWindow = configuredContextWindow.value
    if (!liveBudget || contextWindow <= 0) {
      return null
    }
    return computeTokenUsagePct(
      liveBudget.estimatedInputTokens,
      contextWindow,
    )
  })

  const modelInfo = computed(() => {
    const providerId = String(
      (opts.store.state as any)?.provider?.activeId ?? '',
    ).trim()
    const modelId =
      String(opts.store.state.config.model ?? '').trim() || 'model'
    let providerName = ''
    if (providerId) {
      if (providerId === 'custom') {
        providerName = t('homePage.providerCustom')
      } else if (isCustomProviderId(providerId)) {
        const customProviders = Array.isArray(
          (opts.store.state as any)?.provider?.customProviders,
        )
          ? (opts.store.state as any).provider.customProviders
          : []
        const match = customProviders.find(
          (p: any) => String(p?.id ?? '').trim() === providerId,
        )
        providerName =
          String(match?.name ?? '').trim() || t('homePage.providerCustom')
      } else {
        providerName = getProviderDisplayName(
          getBuiltinProvider(providerId) ?? { id: providerId },
        )
      }
    }

    let model = modelId
    if (providerId) {
      if (providerId === 'custom' || isCustomProviderId(providerId)) {
        const customModels =
          providerId === 'custom'
            ? (((opts.store.state as any)?.provider?.custom?.models ??
                []) as Array<{
                id: string
                name?: string
                display_name?: string
              }>)
            : (() => {
                const list = Array.isArray(
                  (opts.store.state as any)?.provider?.customProviders,
                )
                  ? (opts.store.state as any).provider.customProviders
                  : []
                const match = list.find(
                  (p: any) => String(p?.id ?? '').trim() === providerId,
                )
                return (match?.models ?? []) as Array<{
                  id: string
                  name?: string
                  display_name?: string
                }>
              })()
        const match = customModels.find(
          (m) => String(m?.id ?? '').trim() === modelId,
        )
        if (match) model = getModelDisplayName(match)
      } else {
        const builtin = getBuiltinProvider(providerId)
        const customModels =
          builtin?.customModels &&
          (opts.store.state as any)?.provider?.connections?.[providerId]?.models
        const models = Array.isArray(customModels)
          ? customModels
          : builtin?.models
        const match = models?.find(
          (m) => String(m?.id ?? '').trim() === modelId,
        )
        if (match) model = getModelDisplayName(match)
      }
    }

    return { build: '■', model, providerName }
  })

  const modeLabel = computed(() => {
    const mode = opts.uiMode.value
    return mode
  })

  // Optimization: Cache the last assistant message index to avoid O(n) scans,
  // while still being reactive to streaming updates within the message parts.
  let lastAssistantIndex = -1
  let lastMessageCount = -1
  const assistantStatusText = computed(() => {
    const messages = opts.store.state.messages
    const currentCount = messages.length

    if (currentCount !== lastMessageCount) {
      // Message list changed (typically new user/assistant messages appended).
      // Always re-locate the last assistant message so status doesn't get stuck
      // on an older assistant (e.g. after a previous error).
      lastMessageCount = currentCount
      lastAssistantIndex = -1
    }

    let m = lastAssistantIndex >= 0 ? messages[lastAssistantIndex] : undefined
    if (!m || m.role !== 'assistant') {
      lastAssistantIndex = -1
      for (let i = messages.length - 1; i >= 0; i--) {
        const candidate = messages[i]
        if (candidate && candidate.role === 'assistant') {
          lastAssistantIndex = i
          m = candidate
          break
        }
      }
    }

    if (!m || m.role !== 'assistant') return ''

    const s = m.parts.find((p) => p.type === 'status') as any
    const text = String(s?.text ?? '').trim()

    if (!text || isDoneStatusText(text)) return ''

    if (isErrorStatusText(text)) {
      return ''
    }

    return text
  })

  const isLoading = computed(
    () =>
      Boolean(opts.store.state.runningToolCallId) ||
      Boolean(opts.store.state.runningAgentRound),
  )
  const isCompacting = computed(() => Boolean(opts.isCompacting?.value))
  const loadingFrame = ref(0)
  const loadingAnimIntervalMs = (() => {
    const raw = String(
      (globalThis as any)?.process?.env?.DIMCODE_LOADING_ANIM_MS ?? '',
    ).trim()
    const n = Number(raw)
    if (Number.isFinite(n) && n > 0)
      return Math.min(1000, Math.max(16, Math.floor(n)))
    // 160ms matches cli-spinners "mindblown" animation.
    return 160
  })()

  const statusAnimationActive = computed(() => {
    if (isCompacting.value) return true
    if (isLoading.value) return true
    return /^thinking\b/i.test(assistantStatusText.value)
  })

  watch(
    statusAnimationActive,
    (active, _prev, onCleanup) => {
      if (!active) {
        loadingFrame.value = 0
        return
      }

      // Trigger an immediate render when loading starts so the user can tell we're
      // waiting on the model (vs. stuck rendering).
      opts.invalidate?.({ priority: 'high' })

      const timer = setInterval(() => {
        // Keep bounded while still allowing independent modulo logic per indicator.
        loadingFrame.value = (loadingFrame.value + 1) % 1_000_000
        opts.invalidate?.({ priority: 'high' })
      }, loadingAnimIntervalMs)

      onCleanup(() => {
        clearInterval(timer)
        loadingFrame.value = 0
      })
    },
    { immediate: true },
  )

  const thinkingWords = computed(() => {
    const list = parseThinkingWords(t('chatPage.thinkingWords'))
    return list.length ? list : FALLBACK_THINKING_WORDS
  })

  const thinkingStatusDisplay = computed(() => {
    if (isCompacting.value) return t('chatPage.toast.compactingSession')
    if (!statusAnimationActive.value) return ''
    const words = thinkingWords.value
    if (!words.length) return ''
    const framesPerLetter = 1
    const holdFrames = 6
    const framesByWord = words.map((word) => {
      const letters = splitWordLetters(word)
      return Math.max(4, letters.length * framesPerLetter + holdFrames)
    })
    const totalFrames = framesByWord.reduce((sum, count) => sum + count, 0)
    if (totalFrames <= 0) return ''
    let frame = loadingFrame.value % totalFrames
    let wordIndex = 0
    for (let i = 0; i < framesByWord.length; i++) {
      const block = framesByWord[i] ?? 0
      if (frame < block) {
        wordIndex = i
        break
      }
      frame -= block
    }
    const word = words[wordIndex] ?? words[0] ?? 'thinking'
    const letters = splitWordLetters(word)
    if (!letters.length) return ''
    const revealCount = Math.min(
      letters.length,
      Math.floor(frame / framesPerLetter) + 1,
    )
    const compact = letters.slice(0, revealCount).join('')
    const dotCount = 1 + (frame % 3)
    return `${compact}${'.'.repeat(dotCount)}`
  })

  const thinkingStatusStyle = computed<Style>(() => {
    return opts.ui.style(['status.warning', 'bg.window'], { bold: true })
  })

  const loadingAccentColor = computed<AnsiColorName>(() => {
    // Prefer a warning-ish yellow when available.
    const warn = opts.ui.token('status.warning').fg
    if (warn) return warn
    const accent = opts.ui.token('accent.warning').fg
    if (accent) return accent
    return (
      opts.ui.capability.level === 8 ? 'yellow' : 'yellowBright'
    ) as AnsiColorName
  })

  const loadingSpinnerCols = computed<readonly [Style, Style]>(() => {
    // 2x2 spinner encoded into 1 terminal row using two "▀" characters:
    // - foreground color paints the top half
    // - background color paints the bottom half
    const inactive = (opts.ui.token('bg.window').bg ??
      'blackBright') as AnsiColorName

    const step = loadingFrame.value % 4
    const activeColor = loadingAccentColor.value

    // 0: top-left, 1: top-right, 2: bottom-right, 3: bottom-left (clockwise)
    const squareColor = (square: 0 | 1 | 2 | 3) =>
      square === step ? activeColor : inactive

    const leftTop = squareColor(0)
    const rightTop = squareColor(1)
    const rightBottom = squareColor(2)
    const leftBottom = squareColor(3)

    const left = opts.ui.style([], { fg: leftTop, bg: leftBottom })
    const right = opts.ui.style([], { fg: rightTop, bg: rightBottom })
    return [left, right] as const
  })

  const loadingSpinnerGlyph = computed(() => {
    const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
    const step = loadingFrame.value % frames.length
    return frames[step] ?? '○'
  })

  const loadingSpinnerGlyphStyle = computed<Style>(() =>
    opts.ui.style(['bg.window'], { fg: loadingAccentColor.value, bold: true }),
  )

  const toolStatusText = computed(() => {
    const uiState: any = (opts.store.state as any)?.ui ?? {}
    const tool = uiState.tool ?? {}
    const callId = String(tool.callId ?? '').trim()
    if (!callId || !opts.store.state.runningToolCallId) return ''
    if (String(opts.store.state.runningToolCallId) !== callId) return ''
    const name = String(tool.name ?? '').trim() || t('chatStatus.toolFallback')
    const startedAt = Number(tool.startedAtMs ?? 0)
    const duration = startedAt ? formatDuration(Date.now() - startedAt) : ''
    return duration ? `${name} · ${duration}` : name
  })

  const assistantStatusDisplay = computed(() => {
    const raw = assistantStatusText.value
    const tool = toolStatusText.value
    if (tool) return tool
    if (!raw) return ''
    return raw
  })

  const assistantStatusStyle = computed<Style>(() => {
    if (toolStatusText.value)
      return opts.ui.style(['status.warning', 'bg.window'])
    if (isLoading.value) return opts.ui.style(['status.warning', 'bg.window'])
    const raw = assistantStatusText.value
    if (isErrorStatusText(raw))
      return opts.ui.style(['status.danger', 'bg.window'])
    return opts.ui.style(['text.muted', 'bg.window'])
  })

  const ctxPctChip = computed(() => {
    const usedPct = ctxPct.value
    if (usedPct == null) return '--'
    return `${ringForPct(usedPct)} ${usedPct}%`
  })

  const ctxPctStyle = computed<Style>(() => {
    const pct = ctxPct.value
    const triggerPct = compactTriggerPct.value
    if (pct == null || triggerPct == null)
      return opts.ui.style(['text.muted', 'bg.window'])
    const warningPct = Math.max(1, triggerPct - 10)
    if (pct >= triggerPct) return opts.ui.style(['status.danger', 'bg.window'])
    if (pct >= warningPct) return opts.ui.style(['status.warning', 'bg.window'])
    return opts.ui.style(['text.muted', 'bg.window'])
  })

  const commandsKey = computed(() => {
    return opts.shortcuts?.value?.commands
      ? formatShortcutLabel(opts.shortcuts.value.commands)
      : 'Ctrl+P'
  })

  const footerRightFull = computed(() =>
    t('chatPage.footer.shortcuts', { commandsKey: commandsKey.value }),
  )
  const footerRightCompact = computed(() =>
    t('chatPage.footer.shortcutsCompact', { commandsKey: commandsKey.value }),
  )
  const footerRightMicro = computed(() =>
    t('chatPage.footer.shortcutsMicro', { commandsKey: commandsKey.value }),
  )
  const footerRightMini = computed(() =>
    t('chatPage.footer.shortcutsMini', { commandsKey: commandsKey.value }),
  )
  const footerRight = computed(() => footerRightFull.value)

  return {
    headerStatsDisplay,
    ctxPct,
    ctxPctStyle,
    ctxPctChip,
    modelInfo,
    modeLabel,
    assistantStatusDisplay,
    assistantStatusStyle,
    isLoading,
    loadingFrame,
    loadingAnimIntervalMs,
    loadingSpinnerCols,
    loadingSpinnerGlyph,
    loadingSpinnerGlyphStyle,
    thinkingStatusDisplay,
    thinkingStatusStyle,
    footerRight,
    footerRightFull,
    footerRightCompact,
    footerRightMicro,
    footerRightMini,
  }
}
