import type { Kysely } from 'kysely'
import type { KyselyDB } from '../database-types.js'
import type { TranslationTokens } from '../engines/language.types.gen.js'
import {
  backendTranslationSnapshot,
  type BackendTranslationNamespace,
} from './backend-translation-snapshot.js'

type LocaleData = Record<string, any>
type Locales = Record<string, LocaleData>

type BackendTranslationRow = {
  locale: string
  namespace: BackendTranslationNamespace
  contentJson: unknown
}

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

function parseLocaleData(value: unknown): LocaleData {
  if (typeof value === 'string') {
    return parseLocaleData(JSON.parse(value))
  }

  if (isPlainObject(value)) {
    return value as LocaleData
  }

  return {}
}

function syncTranslationValue(existing: unknown, baseline: unknown): unknown {
  if (Array.isArray(baseline)) {
    if (!Array.isArray(existing)) {
      return baseline
    }

    return baseline.map((item, index) => syncTranslationValue(existing[index], item))
  }

  if (isPlainObject(baseline)) {
    const existingObject = isPlainObject(existing) ? existing : {}
    return Object.fromEntries(
      Object.entries(baseline).map(([key, value]) => [
        key,
        syncTranslationValue(existingObject[key], value),
      ])
    )
  }

  if (existing === undefined || existing === null) {
    return baseline
  }

  return typeof existing === typeof baseline ? existing : baseline
}

function contentJsonString(content: unknown): string {
  return JSON.stringify(content)
}

const mergeLocale = (...parts: LocaleData[]): LocaleData =>
  parts.reduce((acc, part) => ({ ...acc, ...part }), {})

export class TranslationService {
  private locales: Locales = {}
  private loaded = false

  constructor(private readonly kysely: Kysely<KyselyDB>) {}

  private async ensureSnapshotSynced(): Promise<void> {
    const existingRows = await this.kysely
      .selectFrom('backendTranslation')
      .select(['locale', 'namespace', 'contentJson'])
      .execute() as BackendTranslationRow[]

    const existingMap = new Map<string, BackendTranslationRow>(
      existingRows.map((row) => [`${row.locale}:${row.namespace}`, row] as const)
    )
    const snapshotMap = new Map<string, (typeof backendTranslationSnapshot)[number]>(
      backendTranslationSnapshot.map((row) => [
        `${row.locale}:${row.namespace}`,
        row,
      ] as const)
    )

    for (const snapshotRow of backendTranslationSnapshot) {
      const key = `${snapshotRow.locale}:${snapshotRow.namespace}`
      const existingRow = existingMap.get(key)

      if (!existingRow) {
        await this.kysely
          .insertInto('backendTranslation')
          .values({
            locale: snapshotRow.locale,
            namespace: snapshotRow.namespace,
            contentJson: contentJsonString(snapshotRow.content),
          })
          .execute()
        continue
      }

      const currentContent = parseLocaleData(existingRow.contentJson)
      const syncedContent = syncTranslationValue(currentContent, snapshotRow.content)

      if (contentJsonString(currentContent) === contentJsonString(syncedContent)) {
        continue
      }

      await this.kysely
        .updateTable('backendTranslation')
        .set({
          contentJson: contentJsonString(syncedContent),
          updatedAt: new Date(),
        })
        .where('locale', '=', snapshotRow.locale)
        .where('namespace', '=', snapshotRow.namespace)
        .execute()
    }

    for (const existingRow of existingRows) {
      const key = `${existingRow.locale}:${existingRow.namespace}`
      if (snapshotMap.has(key)) continue

      await this.kysely
        .deleteFrom('backendTranslation')
        .where('locale', '=', existingRow.locale)
        .where('namespace', '=', existingRow.namespace)
        .execute()
    }
  }

  async init(): Promise<void> {
    if (this.loaded) return

    await this.ensureSnapshotSynced()

    const rows = await this.kysely
      .selectFrom('backendTranslation')
      .select(['locale', 'contentJson'])
      .orderBy('locale')
      .orderBy('namespace')
      .execute() as Array<Pick<BackendTranslationRow, 'locale' | 'contentJson'>>

    const locales: Locales = {}
    for (const row of rows) {
      locales[row.locale] = mergeLocale(
        locales[row.locale] ?? {},
        parseLocaleData(row.contentJson)
      )
    }

    this.locales = locales
    this.loaded = true
  }

  get(locale: string): LocaleData {
    if (!this.loaded) {
      throw new Error('TranslationService not loaded. Call init() first.')
    }

    if (!this.locales[locale]) {
      console.warn(`Missing locale: ${locale}`)
    }

    return this.locales[locale] || this.locales.en || {}
  }

  getAll(): Locales {
    if (!this.loaded) {
      throw new Error('TranslationService not loaded. Call init() first.')
    }

    return this.locales
  }

  t(
    path: TranslationTokens,
    locale: string,
    variables: Record<string, any> = {}
  ): string {
    const localeData = this.get(locale)
    const keys = path.split('.')
    let value: any = localeData

    for (const key of keys) {
      if (value && typeof value === 'object' && key in value) {
        value = value[key]
      } else {
        return `[Missing: ${path}]`
      }
    }

    if (typeof value !== 'string') {
      return `[Invalid: ${path}]`
    }

    return value.replace(/\{\{(\w+)\}\}|\{(\w+)\}/g, (_, keyA, keyB) => {
      const key = keyA ?? keyB
      return variables[key] || `{${key}}`
    })
  }

  tArray(path: string, locale: string = 'en'): string[] {
    const localeData = this.get(locale)
    const keys = path.split('.')
    let value: any = localeData

    for (const key of keys) {
      if (value && typeof value === 'object' && key in value) {
        value = value[key]
      } else {
        return [`[Missing: ${path}]`]
      }
    }

    if (!Array.isArray(value)) {
      return [`[Invalid array: ${path}]`]
    }

    return value
  }
}
