/**
 * @wrnexus/i18n — translations for pages and API responses.
 *
 * Locales live in `app/locales/<lang>.json`. Per request the active language is
 * resolved from the `wire-lang` cookie, then Accept-Language, then the default.
 * `ctx.t(key, params)` translates on the server; in `.wrn` views `{t:key}` and
 * `t:attr="key"` markers are resolved by `translateHtml` before the HTML is sent.
 */

import { existsSync, readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import type { TFunction } from "@wrnexus/core";

export { formatNumber, formatCurrency, formatDate, formatRelativeTime, plural } from "./format.ts";

export type Messages = Record<string, unknown>;

/** Load `<dir>/<lang>.json` files into a `{ lang: messages }` map. */
export function loadLocales(dir: string): Record<string, Messages> {
  const out: Record<string, Messages> = {};
  if (!existsSync(dir)) return out;
  for (const file of readdirSync(dir)) {
    if (!file.endsWith(".json")) continue;
    const lang = file.replace(/\.json$/, "");
    try {
      out[lang] = JSON.parse(readFileSync(join(dir, file), "utf8")) as Messages;
    } catch (err) {
      console.warn(`[wrnexus] failed to load locale '${lang}'`, err);
    }
  }
  return out;
}

export interface I18nConfig {
  /** Default language, used as the fallback and when nothing else matches. */
  default?: string;
  /** Explicit set of supported languages (defaults to the loaded locale names). */
  locales?: string[];
}

export interface ResolvedI18n {
  default: string;
  langs: string[];
  messages: Record<string, Messages>;
}

export const LANG_COOKIE = "wire-lang";
export const I18N_JS_HREF = "/__wrnexus/i18n.js";

/** Merge loaded locale messages + config into a resolved i18n bundle. */
export function resolveI18n(messages: Record<string, Messages>, config?: I18nConfig): ResolvedI18n {
  const langs = config?.locales ?? Object.keys(messages);
  const fallback = langs[0] ?? "en";
  const def = config?.default && messages[config.default] ? config.default : fallback;
  return { default: def, langs, messages };
}

/** Look up a possibly-dotted key in a messages object. */
function lookup(messages: Messages | undefined, key: string): string | undefined {
  if (!messages) return undefined;
  if (key in messages && typeof messages[key] === "string") return messages[key] as string;
  let node: unknown = messages;
  for (const part of key.split(".")) {
    if (node && typeof node === "object" && part in (node as Record<string, unknown>)) {
      node = (node as Record<string, unknown>)[part];
    } else {
      return undefined;
    }
  }
  return typeof node === "string" ? node : undefined;
}

/** Interpolate `{param}` placeholders in a message. */
function interpolate(message: string, params?: Record<string, string | number>): string {
  if (!params) return message;
  return message.replace(/\{(\w+)\}/g, (_m, name: string) =>
    name in params ? String(params[name]) : `{${name}}`,
  );
}

/** Build a `t()` for a language: current → default → the key itself. */
export function makeT(i18n: ResolvedI18n, lang: string): TFunction {
  return (key, params) => {
    const message =
      lookup(i18n.messages[lang], key) ?? lookup(i18n.messages[i18n.default], key) ?? key;
    return interpolate(message, params);
  };
}

/** Resolve the active language from a cookie, Accept-Language, then default. */
export function resolveLang(
  i18n: ResolvedI18n,
  cookieValue: string | undefined,
  acceptLanguage: string | null,
): string {
  if (cookieValue && i18n.langs.includes(cookieValue)) return cookieValue;
  for (const part of (acceptLanguage ?? "").split(",")) {
    const tag = part.split(";")[0]!.trim().toLowerCase();
    if (!tag) continue;
    if (i18n.langs.includes(tag)) return tag;
    const base = tag.split("-")[0]!;
    if (i18n.langs.includes(base)) return base;
  }
  return i18n.default;
}

function attrEscape(value: string): string {
  return value
    .replace(/&/g, "&amp;")
    .replace(/"/g, "&quot;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
}

function htmlEscape(value: string): string {
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

/**
 * Resolve translation markers in rendered HTML:
 *   t:<attr>="key"        → <attr>="<translation>"   (e.g. t:placeholder, t:aria-label)
 *   <tag data-t="key">…</tag> → element text becomes the translation
 * Only runs when the HTML actually contains a marker.
 */
export function translateHtml(html: string, t: TFunction): string {
  if (!html.includes("data-t=") && !html.includes("t:")) return html;

  let out = html.replace(
    /\bt:([A-Za-z][A-Za-z0-9:_-]*)="([^"]*)"/g,
    (_m, attr: string, key: string) => `${attr}="${attrEscape(t(key))}"`,
  );

  out = out.replace(
    /<([A-Za-z][A-Za-z0-9-]*)\b([^>]*\bdata-t="([^"]*)"[^>]*)>([\s\S]*?)<\/\1>/g,
    (_m, tag: string, attrs: string, key: string) =>
      `<${tag}${attrs}>${htmlEscape(t(key))}</${tag}>`,
  );

  return out;
}

/** `window.__wireI18n = { lang, langs }` for the client language switcher. */
export function renderI18nData(i18n: ResolvedI18n, lang: string): string {
  return `window.__wireI18n=${JSON.stringify({ lang, langs: i18n.langs, default: i18n.default })};`;
}

/**
 * Client runtime: binds `[data-wire-lang-set="es"]` elements to set the
 * `wire-lang` cookie and reload, so the server re-renders in the new language.
 */
export const I18N_RUNTIME = String.raw`
(function () {
  var COOKIE = "${LANG_COOKIE}";
  function set(lang) {
    document.cookie = COOKIE + "=" + encodeURIComponent(lang) + ";path=/;max-age=31536000;samesite=lax";
    location.reload();
  }
  function bind(root) {
    (root || document).querySelectorAll("[data-wire-lang-set]").forEach(function (n) {
      if (n.__wireLangBound) return; n.__wireLangBound = 1;
      n.addEventListener("click", function () { set(n.getAttribute("data-wire-lang-set")); });
    });
    (root || document).querySelectorAll("select[data-wire-lang]").forEach(function (n) {
      if (n.__wireLangBound) return; n.__wireLangBound = 1;
      n.addEventListener("change", function () { set(n.value); });
    });
  }
  window.__wireLang = { set: set };
  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", function () { bind(document); });
  else bind(document);
})();
`.trim();
