import fs from 'node:fs'
import dotenv from 'dotenv'
import bingTranslateApi from 'bing-translate-api'
import { v2 as GoogleTranslate } from '@google-cloud/translate'
import googleTranslateApi from '@vitalets/google-translate-api'
import path from 'node:path'
import crypto from 'node:crypto'
import { fileURLToPath } from 'node:url'
import { HttpProxyAgent } from 'http-proxy-agent'
import languages from './config/languages.json' with { type: 'json' }

const __dirname = path.dirname(fileURLToPath(import.meta.url))
dotenv.config({
  path: path.resolve(__dirname, '../../../env/.env'),
})

// ==================================================
// Config
// ==================================================
// Translator
// - google: default
// - google-free
// - bing-free
const TRANSLATOR = 'google'
const TRANSLATOR_INTERVAL = 100            // translate interval
const HttpProxy = 'http://127.0.0.1:7890'   // leave blank if proxy is not required

// ==================================================
// Constants
// ==================================================
const ARGS = parseArgs(process.argv)
const BASE_LANG = ARGS.base || 'en-US'
const MANUAL_LANGS = ['en-US', 'zh-Hans']
const TARGET_LANGS = languages.filter(item => item.supported && item.base === BASE_LANG)

const ROOT_FILES = ['common.ts']
const LOCALE_DIR = path.join(__dirname, '../messages')
const BASE_LANG_DIR = path.join(LOCALE_DIR, BASE_LANG)
const META_DIR = path.join(__dirname, 'meta')
const BASE_META_FILE = path.join(META_DIR, `${BASE_LANG}.meta.json`)

let googleTranslateClient = null
let TRANSLATE_ERROR = false
let TRANSLATE_ERROR_MSG = ''

// ==================================================
// Utility
// ==================================================
function parseArgs(argv) {
  const args = {}
  for (const arg of argv.slice(2)) {
    if (arg.startsWith('--')) {
      const [key, val] = arg.slice(2).split('=')
      args[key] = val === undefined ? true : val
    }
  }
  return args
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms))
}

function hashText(text) {
  return crypto.createHash('md5').update(text).digest('hex').slice(0, 8)
}

function getLanguageKey(lang) {
  if (lang === 'zh-Hans' || lang === 'zh-Hant') {
    return lang
  } else {
    return lang.split('-')[0]
  }
}

function sortDeep(obj) {
  if (typeof obj !== 'object' || obj === null) return obj
  if (Array.isArray(obj)) return obj.map(sortDeep)

  return Object.keys(obj)
    .sort((a, b) => a.localeCompare(b))
    .reduce((acc, key) => {
      acc[key] = sortDeep(obj[key])
      return acc
    }, {})
}

function sortJsonKeys(jsonFile) {
  if (!fs.existsSync(jsonFile)) {
    return
  }

  // sort keys
  const json = JSON.parse(fs.readFileSync(jsonFile, 'utf8'))
  const sorted = sortDeep(json)
  fs.writeFileSync(jsonFile, JSON.stringify(sorted, null, 2), 'utf8')

  console.log('⛵ Sorted keys:', jsonFile)
}

// ==================================================
// Load/Write ts files
// ==================================================
function loadTsObject(filePath) {
  const code = fs.readFileSync(filePath, 'utf8')
  const match = code.match(/export\s+default\s+({[\s\S]*})\s*$/)

  if (!match) {
    throw new Error(`Invalid i18n ts file format: ${filePath}`)
  }

  return eval(`(${match[1]})`)
}

function writeTsFile(filePath, data) {
  const content =
    `export default ${serializeToTs(sortDeep(data), 2)}
`
  fs.mkdirSync(path.dirname(filePath), { recursive: true })
  fs.writeFileSync(filePath, content, 'utf8')
}

function serializeToTs(obj, indent = 2, level = 0) {
  const pad = ' '.repeat(indent * level)
  const padInner = ' '.repeat(indent * (level + 1))

  if (typeof obj === 'string') {
    return `'${obj.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
  }

  if (Array.isArray(obj)) {
    if (obj.length === 0) return '[]'
    return `[\n${obj
      .map(v => `${padInner}${serializeToTs(v, indent, level + 1)}`)
      .join(',\n')}\n${pad}]`
  }

  if (typeof obj === 'object' && obj !== null) {
    const entries = Object.entries(obj)
    if (entries.length === 0) return '{}'

    return `{\n${entries
      .map(([key, val]) => {
        return `${padInner}${key}: ${serializeToTs(val, indent, level + 1)}`
      })
      .join(',\n')}\n${pad}}`
  }

  // number / boolean / null
  return String(obj)
}

// Post-translation recovery based on your method:
// Replaces whatever is inside the translated {} with the original placeholders by order
function forceRestorePlaceholders(translatedText, originalText) {
  let restored = typeof translatedText === 'string' ? translatedText : String(translatedText || '')

  // Extract all original placeholders like {length}, {key}
  const originalRegex = /\{[^}]+\}/g
  const originals = originalText.match(originalRegex)

  // If the original text didn't have any placeholders, do nothing
  if (!originals || originals.length === 0) {
    return restored
  }

  // Match any corrupted or translated placeholders in the translated output,
  // including cases where spaces were injected, e.g., "{ tecla }", "{longitud}"
  const translatedRegex = /\{[^}]+\}/g
  let matchIndex = 0

  restored = restored.replace(translatedRegex, (match) => {
    // Replace with the original placeholder if available, otherwise keep the match
    const replacement = originals[matchIndex] !== undefined ? originals[matchIndex] : match
    matchIndex++
    return replacement
  })

  return restored
}

// ==================================================
// Translate
// ==================================================
async function translateText(text, lang) {
  console.log(`➡️ Translating (${lang.value}): ${text}`)
  // Return blank text if translate failed, and try next time
  if (TRANSLATE_ERROR) {
    return ''
  }

  await sleep(TRANSLATOR_INTERVAL + Math.random() * TRANSLATOR_INTERVAL * 2)

  let translatedResult = ''

  // 1. Perform translation on the safe text
  switch (TRANSLATOR) {
    case 'bing-free':
      translatedResult = await bingFreeTranslate(text, lang)
      break
    case 'google-free':
      translatedResult = await googleFreeTranslate(text, lang)
      break
    default:
      translatedResult = await googleTranslate(text, lang)
      break
  }

  // 2. Restore the original placeholders
  return forceRestorePlaceholders(translatedResult, text)
}

async function bingFreeTranslate(text, lang) {
  try {
    const res = await bingTranslateApi.translate(
      text,
      getLanguageKey(BASE_LANG),
      getLanguageKey(lang.value)
    )
    return res.translation
  } catch (err) {
    console.error(`❌ Translation failed：`, err.message)
    TRANSLATE_ERROR = true
    TRANSLATE_ERROR_MSG = err.message
    return '' // Return blank text if failed
  }
}

function initGoogleTranslate() {
  if (!googleTranslateClient) {
    let fetchOptions = {}
    if (HttpProxy) {
      const agent = new HttpProxyAgent(HttpProxy)
      fetchOptions = { agent }
    }

    googleTranslateClient = new GoogleTranslate.Translate({
      key: process.env.GOOGLE_API_KEY,
      requestOptions: fetchOptions,
    })
  }
}

async function googleTranslate(text, lang) {
  try {
    initGoogleTranslate()

    const [result] = await googleTranslateClient.translate(text, {
      to: lang.value,
      format: 'text',
    })
    return result
  } catch (err) {
    console.error(`❌ Translation failed：`, err.message)
    TRANSLATE_ERROR = true
    TRANSLATE_ERROR_MSG = err.message
    return '' // Return blank text if failed
  }
}

async function googleFreeTranslate(text, lang) {
  try {
    let fetchOptions = {}
    if (HttpProxy) {
      const agent = new HttpProxyAgent(HttpProxy)
      fetchOptions = { agent }
    }
    const res = await googleTranslateApi.translate(text, {
      to: lang.value,
      fetchOptions: fetchOptions
    })
    return res.text
  } catch (err) {
    console.error(`❌ Translation failed：`, err.message)
    TRANSLATE_ERROR = true
    TRANSLATE_ERROR_MSG = err.message
    return '' // Return blank text if failed
  }
}


// ==================================================
// Sync removed key
// ==================================================
function pruneRemovedKeys(baseObj, targetObj, metaObj, parentKey = '') {
  if (typeof targetObj !== 'object' || targetObj === null) {
    return targetObj
  }

  const result = Array.isArray(targetObj) ? [] : {}

  for (const key in targetObj) {
    const fullKey = parentKey ? `${parentKey}.${key}` : key
    const existsInBase = baseObj && key in baseObj
    const manual = metaObj?.[fullKey]?.manual

    // ❌ base deleted
    if (!existsInBase) {
      if (manual) {
        result[key] = targetObj[key]
        console.log(`🛑 Keep manual key: ${fullKey}`)
      } else {
        console.log(`🗑 Remove key: ${fullKey}`)
        delete metaObj[fullKey]
      }
      continue
    }

    const baseVal = baseObj[key]
    const targetVal = targetObj[key]

    if (
      typeof baseVal === 'object' &&
      baseVal !== null &&
      typeof targetVal === 'object'
    ) {
      const pruned = pruneRemovedKeys(baseVal, targetVal, metaObj, fullKey)

      // Blank object
      if (
        typeof pruned === 'object' &&
        pruned !== null &&
        Object.keys(pruned).length === 0
      ) {
        delete metaObj[fullKey]
      } else {
        result[key] = pruned
      }
    } else {
      result[key] = targetVal
    }
  }

  return result
}

function pruneMetaByBase(baseObj, metaObj, parentKey = '') {
  for (const key in metaObj) {
    const path = key.split('.')
    let cur = baseObj

    for (const p of path) {
      if (!cur || typeof cur !== 'object' || !(p in cur)) {
        console.log(`🧹 Remove stale meta: ${key}`)
        delete metaObj[key]
        break
      }
      cur = cur[p]
    }
  }
}

// ==================================================
// Fill Translation
// ==================================================
let baseMeta = {}

function loadBaseMeta() {
  if (fs.existsSync(BASE_META_FILE)) {
    baseMeta = JSON.parse(fs.readFileSync(BASE_META_FILE, 'utf8'))
  }
}

async function fillTranslations(
  baseObj,
  targetObj,
  metaObj,
  lang,
  updatedList,
  parentKey = ''
) {
  const result = { ...targetObj }

  for (const key in baseObj) {
    const fullKey = parentKey ? `${parentKey}.${key}` : key
    const baseVal = baseObj[key]
    const targetVal = targetObj?.[key]

    if (typeof baseVal === 'string') {
      const currentHash = hashText(baseVal)
      const oldHash = baseMeta[fullKey]
      const useBase = metaObj[fullKey]?.base
      const manual = metaObj[fullKey]?.manual
      const manualUpdate = targetVal && manual
      const baseChanged = oldHash !== currentHash
      const needUpdate = !targetVal || baseChanged

      // missing OR changed
      if (useBase) {
        if (baseChanged) {
          console.log(`👉🏼 Use base: ${fullKey} → ${targetVal}`)
          result[key] = baseVal

          updatedList.push({
            key: fullKey,
            from: baseVal,
            to: baseVal,
          })
        }
      } else if (manualUpdate) {
        // ignore
        console.log(`👉🏼 Manual: ${fullKey} → ${targetVal}`)
      } else if (needUpdate) {
        const translated = await translateText(baseVal, lang)
        result[key] = translated

        updatedList.push({
          key: fullKey,
          from: baseVal,
          to: translated,
        })
      }

      // Update meta hash
      // baseMeta[fullKey] = currentHash
    } else if (typeof baseVal === 'object' && baseVal !== null) {
      result[key] = await fillTranslations(
        baseVal,
        targetVal || {},
        metaObj,
        lang,
        updatedList,
        fullKey
      )
    }
  }

  return result
}

function updateBaseMeta(obj, parentKey = '') {
  for (const key in obj) {
    const fullKey = parentKey ? `${parentKey}.${key}` : key
    const val = obj[key]

    if (typeof val === 'string') {
      baseMeta[fullKey] = hashText(val)
    } else if (typeof val === 'object' && val !== null) {
      updateBaseMeta(val, fullKey)
    }
  }
}

// ==================================================
// Main process
// ==================================================
function sortBaseFiles() {
  const baseFiles = getLocaleFiles(BASE_LANG_DIR)
  for (const file of baseFiles) {
    const filePath = path.join(BASE_LANG_DIR, file)
    if (!fs.existsSync(filePath)) continue

    const obj = loadTsObject(filePath)
    const sortedObj = sortDeep(obj)
    writeTsFile(filePath, sortedObj)
  }
  console.log(`⛵ Sorted base file: ${BASE_LANG_DIR}`)
}

function prepare() {
  sortBaseFiles()
  sortJsonKeys(BASE_META_FILE)
  loadBaseMeta()
}

function getLocaleFiles(langDir) {
  return fs.readdirSync(langDir)
    .filter(f => f.endsWith('.ts') && f !== 'index.ts')
}

function getRootKey(fileName) {
  return ROOT_FILES.includes(fileName)
    ? ''
    : fileName.replace('.ts', '')
}

function copyIndexFile(langDir, lang) {
  const baseIndexFile = path.join(BASE_LANG_DIR, 'index.ts')
  const targetIndexFile = path.join(langDir, 'index.ts')
  if (fs.existsSync(baseIndexFile)) {
    fs.copyFileSync(baseIndexFile, targetIndexFile)
    console.log(`📄 Copied index.ts to ${lang}`)
  }
}

async function main() {
  // prepare
  prepare()

  // translate
  console.log(`📌 Translator: ${TRANSLATOR}`)
  console.log(`📌 Base language: ${BASE_LANG}`)
  console.log(`📌 Target languages: ${TARGET_LANGS.map(item => item.value).join(', ')}`)
  console.log('')

  const fullBaseObj = {}
  const baseFiles = getLocaleFiles(BASE_LANG_DIR)
  for (const lang of TARGET_LANGS) {
    console.log('============================================================')

    if (MANUAL_LANGS.includes(lang.value)) {
      console.log(`👉🏻 Translate manually → ${lang.value} | ${lang.prompt_name}`)
      continue
    }

    console.log(`🌍 Translating → ${lang.value} | ${lang.prompt_name}`)

    // Target lang dir
    const langDir = path.join(LOCALE_DIR, lang.value)
    fs.mkdirSync(langDir, { recursive: true })
    copyIndexFile(langDir, lang.value)

    // Target lang meta
    const langMetaFile = path.join(META_DIR, `${lang.value}.meta.json`)
    const metaObj = fs.existsSync(langMetaFile)
      ? JSON.parse(fs.readFileSync(langMetaFile, 'utf8'))
      : {}


    // Translate
    for (const file of baseFiles) {
      console.log('--------------------')
      const baseFile = path.join(BASE_LANG_DIR, file)
      const targetFile = path.join(langDir, file)

      const baseObj = loadTsObject(baseFile)
      const targetObj = fs.existsSync(targetFile)
        ? loadTsObject(targetFile)
        : {}

      const updatedList = []
      const rootKey = getRootKey(file)

      // Translate
      const filled = await fillTranslations(
        baseObj,
        targetObj,
        metaObj,
        lang,
        updatedList,
        rootKey
      )

      // Clean removed
      const cleaned = pruneRemovedKeys(baseObj, filled, metaObj, '')

      writeTsFile(targetFile, cleaned)

      if (updatedList.length) {
        console.log(`🔄 ${file}: ${updatedList.length} updated`)
      } else {
        console.log(`✨ ${file}: up to date`)
      }

      // Prune meta
      if (rootKey) {
        fullBaseObj[rootKey] = baseObj
      } else {
        Object.assign(fullBaseObj, baseObj)
      }
    }

    console.log('============================================================')

    // Break if translate failed
    if (TRANSLATE_ERROR) {
      console.log(`🛑 Translator encounter problems → ${lang.value} | ${lang.prompt_name}`)
      console.log(`❌ Error: ${TRANSLATE_ERROR_MSG}`)
      console.log('============================================================')
      console.log('')
      break
    }
    console.log('')
  }

  // Update meta hash
  if (!TRANSLATE_ERROR) {
    console.log('📝 Synchronizing base meta hashes...')
    updateBaseMeta(fullBaseObj)
  }

  // Prune meta
  pruneMetaByBase(fullBaseObj, baseMeta)

  // Save meta file
  fs.mkdirSync(META_DIR, { recursive: true })
  fs.writeFileSync(BASE_META_FILE, JSON.stringify(baseMeta, null, 2), 'utf8')
  console.log(`📝 Updated meta: ${BASE_META_FILE}`)
  sortJsonKeys(BASE_META_FILE)

  console.log('')
  console.log('🎉 All languages have been updated completely!')
}

main().catch(console.error)
