import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger'
import { useDefaultModel } from '@renderer/hooks/useModel'
import { ipcApi } from '@renderer/ipc'
import { UNKNOWN_LANG_CODE } from '@renderer/utils/translate'
import { LANG_DETECT_PROMPT } from '@shared/ai/prompts'
import {
  type AutoDetectionMethod,
  isTranslateLangCode,
  type TranslateLangCode
} from '@shared/data/preference/preferenceTypes'
import { BUILTIN_LANGUAGE } from '@shared/data/presets/translateLanguages'
import type { Model } from '@shared/data/types/model'
import { isQwenMTModel } from '@shared/utils/model'
import { franc } from 'franc-min'
import i18n from 'i18next'
import { useCallback, useRef } from 'react'
import { estimateTokenCount, sliceByTokens } from 'tokenx'

import { useLanguages } from './useTranslateLanguages'

const logger = loggerService.withContext('translate/useDetectLang')

/** Max tokens to slice from input text for LLM detection. */
const LLM_INPUT_MAX_TOKENS = 100

/**
 * Token threshold for 'auto' mode: texts shorter than this prefer LLM,
 * longer texts try franc first and fall back to LLM on failure.
 */
const AUTO_MODE_LLM_THRESHOLD = 100

// ---------------------------------------------------------------------------
// Pure helpers (no React dependency)
// ---------------------------------------------------------------------------

/**
 * Detect language using an LLM with the provided language list as candidates.
 *
 * @internal Exported for unit tests alongside the other pure helpers.
 */
export const detectLanguageByLLM = async (
  inputText: string,
  langCodes: TranslateLangCode[],
  model: Model | undefined
): Promise<TranslateLangCode> => {
  logger.info('Detect language by LLM')
  const text = sliceByTokens(inputText, 0, LLM_INPUT_MAX_TOKENS)
  const listLangText = JSON.stringify(langCodes)

  if (!model) {
    throw new Error(i18n.t('error.model.not_exists'))
  }

  if (isQwenMTModel(model)) {
    throw new Error(i18n.t('translate.error.detect.qwen_mt'))
  }

  const systemPrompt = LANG_DETECT_PROMPT.replace('{{list_lang}}', listLangText).replace('{{input}}', text)

  const { text: result } = await ipcApi.request('ai.generate_text', {
    uniqueModelId: model.id,
    system: systemPrompt,
    prompt: 'follow system prompt'
  })

  const trimmed = result.trim()
  if (!trimmed) {
    throw new Error(i18n.t('translate.error.detect.empty'))
  }

  if (!isTranslateLangCode(trimmed)) {
    logger.error(`Invalid language code: ${trimmed}`)
    throw new Error(i18n.t('translate.error.detect.invalid'))
  }

  return trimmed
}

/**
 * Detect language using the franc library (offline, fast).
 *
 * @internal Exported for unit tests alongside the other pure helpers.
 */
export const detectLanguageByFranc = (inputText: string): TranslateLangCode => {
  logger.info('Detect language by franc')
  const iso3 = franc(inputText)

  const isoMap: Record<string, TranslateLangCode> = {
    cmn: BUILTIN_LANGUAGE.zhCN.langCode,
    jpn: BUILTIN_LANGUAGE.jaJP.langCode,
    kor: BUILTIN_LANGUAGE.koKR.langCode,
    rus: BUILTIN_LANGUAGE.ruRU.langCode,
    ara: BUILTIN_LANGUAGE.arSA.langCode,
    spa: BUILTIN_LANGUAGE.esES.langCode,
    fra: BUILTIN_LANGUAGE.frFR.langCode,
    deu: BUILTIN_LANGUAGE.deDE.langCode,
    ita: BUILTIN_LANGUAGE.itIT.langCode,
    por: BUILTIN_LANGUAGE.ptPT.langCode,
    eng: BUILTIN_LANGUAGE.enUS.langCode,
    pol: BUILTIN_LANGUAGE.plPL.langCode,
    tur: BUILTIN_LANGUAGE.trTR.langCode,
    tha: BUILTIN_LANGUAGE.thTH.langCode,
    vie: BUILTIN_LANGUAGE.viVN.langCode,
    ind: BUILTIN_LANGUAGE.idID.langCode,
    urd: BUILTIN_LANGUAGE.urPK.langCode,
    zsm: BUILTIN_LANGUAGE.msMY.langCode
  }

  const mapped = isoMap[iso3]
  if (mapped === undefined) {
    // franc recognized a language but we have no mapping for it yet. Log so
    // we can discover cold languages that real users speak.
    logger.debug('franc iso3 not in isoMap, falling back to UNKNOWN', { iso3 })
    return UNKNOWN_LANG_CODE
  }
  return mapped
}

/**
 * Run detection with the given method and language candidate list.
 *
 * @internal Exported for unit tests alongside the other pure helpers.
 */
export const detectWithMethod = async (
  text: string,
  method: AutoDetectionMethod,
  langCodes: TranslateLangCode[],
  model: Model | undefined
): Promise<TranslateLangCode> => {
  switch (method) {
    case 'auto':
      if (estimateTokenCount(text) < AUTO_MODE_LLM_THRESHOLD) {
        return detectLanguageByLLM(text, langCodes, model)
      } else {
        const francResult = detectLanguageByFranc(text)
        if (francResult === UNKNOWN_LANG_CODE) {
          // Auto mode's contract is "pick what works"; we fall back silently from
          // the user's perspective but log so `auto` → LLM quota bursts are traceable.
          logger.info('franc returned UNKNOWN, falling back to LLM detection')
          return detectLanguageByLLM(text, langCodes, model)
        }
        return francResult
      }
    case 'franc':
      return detectLanguageByFranc(text)
    case 'llm':
      return detectLanguageByLLM(text, langCodes, model)
    default:
      throw new Error('Invalid detection method.')
  }
}

// ---------------------------------------------------------------------------
// Hook
// ---------------------------------------------------------------------------

/**
 * Hook that returns a stable `detectLanguage` callback.
 *
 * The detection method (`auto` / `franc` / `llm`) is read from the
 * `feature.translate.auto_detection_method` preference via {@link usePreference},
 * and the candidate language list comes from {@link useLanguages},
 * so both stay in sync with user settings without prop-drilling.
 *
 * @returns `detectLanguage(text: string) => Promise<TranslateLangCode>`
 */
export const useDetectLang = () => {
  const [method] = usePreference('feature.translate.auto_detection_method')
  const { languages } = useLanguages()
  const { quickModel } = useDefaultModel()

  const toastedNotReadyRef = useRef(false)
  const toastedEmptyRef = useRef(false)

  const detectLanguage = useCallback(
    async (inputText: string): Promise<TranslateLangCode> => {
      const text = inputText.trim()
      if (!text) return UNKNOWN_LANG_CODE

      if (languages === undefined) {
        logger.warn('useDetectLang invoked before languages were ready, returning UNKNOWN')
        if (!toastedNotReadyRef.current) {
          toastedNotReadyRef.current = true
          window.toast?.error(i18n.t('translate.error.languages_load_failed'))
        }
        return UNKNOWN_LANG_CODE
      }

      // No data: endpoint resolved with an empty list. Seeder failure or DB
      // corruption — log loudly for Sentry and surface a one-shot toast so
      // the user knows why every translation is coming back as UNKNOWN.
      if (languages.length === 0) {
        logger.error('useDetectLang invoked with an empty language list')
        if (!toastedEmptyRef.current) {
          toastedEmptyRef.current = true
          window.toast?.error(i18n.t('translate.error.languages_load_failed'))
        }
        return UNKNOWN_LANG_CODE
      }

      const langCodes = languages.map((l) => l.langCode)
      logger.info(`Auto detection method: ${method}`)
      const result = await detectWithMethod(text, method, langCodes, quickModel)
      logger.info(`Detected language: ${result}`)
      return result
    },
    [method, languages, quickModel]
  )

  return detectLanguage
}
