export type NestedMessages = { readonly [key: string]: string | NestedMessages }

export interface I18n<L extends string = string> {
  locales: readonly L[]
  defaultLocale: L
  messages: Record<L, NestedMessages>
}

export class I18nConfigError extends Error {
  constructor(message: string, public readonly offending: Record<string, unknown>) {
    super(message)
    this.name = 'I18nConfigError'
  }
}

export function createI18n<L extends string>(config: I18n<L>): I18n<L> {
  if (!(config.locales as readonly string[]).includes(config.defaultLocale)) {
    throw new I18nConfigError(
      `createI18n: defaultLocale "${config.defaultLocale}" not in locales [${config.locales.join(', ')}]`,
      { defaultLocale: config.defaultLocale, locales: config.locales },
    )
  }
  for (const locale of config.locales) {
    if (!Object.hasOwn(config.messages, locale)) {
      throw new I18nConfigError(
        `createI18n: messages missing entry for locale "${locale}"`,
        { missingLocale: locale, locales: config.locales },
      )
    }
  }
  return Object.freeze({ ...config }) as I18n<L>
}

const TOKEN = /\{(\w+)\}/g

function resolveMessage(table: NestedMessages | undefined, key: string): string | undefined {
  if (!table) return undefined
  const direct = table[key]
  if (typeof direct === 'string') return direct
  if (!key.includes('.')) return undefined
  let cur: unknown = table
  for (const part of key.split('.')) {
    if (cur == null || typeof cur !== 'object') return undefined
    cur = (cur as NestedMessages)[part]
  }
  return typeof cur === 'string' ? cur : undefined
}

export function t<L extends string>(
  i18n: I18n<L>,
  locale: L,
  key: string,
  vars?: Record<string, string | number>,
): string {
  const table = i18n.messages[locale] ?? i18n.messages[i18n.defaultLocale]
  let str =
    resolveMessage(table, key) ??
    resolveMessage(i18n.messages[i18n.defaultLocale], key) ??
    key
  if (vars) str = str.replace(TOKEN, (_m, k: string) => (Object.hasOwn(vars, k) ? String(vars[k]) : `{${k}}`))
  return str
}

export function resolveLocale<L extends string>(i18n: I18n<L>, request: Request): L {
  const url = new URL(request.url)
  const seg = url.pathname.split('/')[1]
  if (seg && (i18n.locales as readonly string[]).includes(seg)) return seg as L
  const cookie = request.headers.get('cookie') ?? ''
  const m = cookie.match(/(?:^|;\s*)locale=([^;]+)/)
  if (m && (i18n.locales as readonly string[]).includes(m[1]!)) return m[1] as L
  const al = request.headers.get('accept-language') ?? ''
  for (const part of al.split(',')) {
    const code = part.trim().split(/[-;]/)[0]
    if (code && (i18n.locales as readonly string[]).includes(code)) return code as L
  }
  return i18n.defaultLocale
}
