/**
 * @wrnexus/i18n — deterministic locale loading, fallback resolution, SSR
 * translation markers, browser translation helpers, and UI language controls.
 */

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

export { formatNumber, formatCurrency, formatDate, formatRelativeTime, plural } from "./format.ts";
export {
  extractTranslationKeys,
  extractTranslationKeysFromFiles,
  flattenMessageKeys,
  auditLocaleKeys,
  pseudoLocalize,
  createPseudoLocale,
} from "./tooling.ts";
export type { ExtractedTranslationKey } from "./tooling.ts";

export type Messages = Record<string, unknown>;

export interface LocaleLoadOptions {
  /** Throw on invalid JSON instead of warning and continuing. */
  strict?: boolean;
  /** Maximum JSON file size. Default 1 MiB. */
  maxFileBytes?: number;
}

export interface I18nCookieConfig {
  name?: string;
  maxAge?: number;
  path?: string;
  sameSite?: "Strict" | "Lax" | "None";
  secure?: boolean;
}

export interface I18nConfig {
  default?: string;
  locales?: string[];
  /** Human-readable locale names used by package language controls. */
  labels?: Record<string, string>;
  /** Per-locale fallback override. Example: `{ "fr-CA": ["fr", "en"] }`. */
  fallbacks?: Record<string, string[]>;
  /** Locale direction overrides. Arabic/Hebrew/Persian/Urdu are RTL automatically. */
  direction?: Record<string, "ltr" | "rtl">;
  cookie?: I18nCookieConfig;
  strict?: boolean;
}

export interface ResolvedI18n {
  default: string;
  langs: string[];
  messages: Record<string, Messages>;
  fallbacks: Record<string, string[]>;
  direction: Record<string, "ltr" | "rtl">;
  labels: Record<string, string>;
  cookie: Required<I18nCookieConfig>;
}

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

const UNSAFE_KEYS = new Set(["__proto__", "prototype", "constructor"]);
const RTL_LANGS = new Set(["ar", "dv", "fa", "he", "ku", "ps", "sd", "ug", "ur", "yi"]);

export function normalizeLocale(locale: string): string {
  const value = locale.trim().replace(/_/g, "-");
  if (!value || !/^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/.test(value)) return "";
  const parts = value.split("-");
  return parts
    .map((part, index) => {
      if (index === 0) return part.toLowerCase();
      if (part.length === 2) return part.toUpperCase();
      if (part.length === 4) return part[0]!.toUpperCase() + part.slice(1).toLowerCase();
      return part;
    })
    .join("-");
}

function safeObject(value: unknown, path: string): Messages {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    throw new TypeError(`Locale file '${path}' must contain a JSON object.`);
  }
  const output: Messages = Object.create(null) as Messages;
  for (const [key, child] of Object.entries(value)) {
    if (UNSAFE_KEYS.has(key)) throw new TypeError(`Unsafe translation key '${key}' in ${path}.`);
    output[key] =
      child && typeof child === "object" && !Array.isArray(child)
        ? safeObject(child, `${path}.${key}`)
        : child;
  }
  return output;
}

function mergeMessages(target: Messages, source: Messages): Messages {
  for (const [key, value] of Object.entries(source)) {
    if (UNSAFE_KEYS.has(key)) continue;
    const current = target[key];
    target[key] =
      value && typeof value === "object" && !Array.isArray(value)
        ? mergeMessages(
            current && typeof current === "object" && !Array.isArray(current)
              ? (current as Messages)
              : (Object.create(null) as Messages),
            value as Messages,
          )
        : value;
  }
  return target;
}

function localeFiles(dir: string): string[] {
  if (!existsSync(dir)) return [];
  const files: string[] = [];
  const visit = (current: string) => {
    for (const entry of readdirSync(current, { withFileTypes: true })) {
      const path = join(current, entry.name);
      if (entry.isDirectory()) visit(path);
      else if (entry.isFile() && entry.name.endsWith(".json")) files.push(path);
    }
  };
  visit(dir);
  return files.sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
}

/**
 * Load both supported layouts:
 * - `locales/en.json`
 * - `locales/en/common.json`, `locales/en/auth.json`
 *
 * Namespaced files become `messages.en.common` and `messages.en.auth`.
 */
export function loadLocales(
  dir: string,
  options: LocaleLoadOptions = {},
): Record<string, Messages> {
  const output: Record<string, Messages> = Object.create(null) as Record<string, Messages>;
  const maxFileBytes = Math.max(1_024, options.maxFileBytes ?? 1_048_576);
  for (const file of localeFiles(dir)) {
    const rel = relative(dir, file).split(sep);
    const rootFile = rel.length === 1;
    const rawLocale = rootFile ? basename(file, extname(file)) : rel[0]!;
    const locale = normalizeLocale(rawLocale);
    if (!locale) {
      const error = new TypeError(`Invalid locale name '${rawLocale}' in ${file}.`);
      if (options.strict) throw error;
      console.warn(`[wrnexus] ${error.message}`);
      continue;
    }
    try {
      const info = statSync(file);
      if (info.size > maxFileBytes) throw new Error(`Locale file exceeds ${maxFileBytes} bytes.`);
      const parsed = safeObject(JSON.parse(readFileSync(file, "utf8")), file);
      const messages = (output[locale] ??= Object.create(null) as Messages);
      if (rootFile) mergeMessages(messages, parsed);
      else {
        const namespace = rel
          .slice(1)
          .join("/")
          .replace(/\.json$/, "")
          .replace(/\//g, ".");
        const parts = namespace.split(".").filter(Boolean);
        let node = messages;
        for (const part of parts.slice(0, -1)) {
          const current = node[part];
          if (!current || typeof current !== "object" || Array.isArray(current)) {
            node[part] = Object.create(null) as Messages;
          }
          node = node[part] as Messages;
        }
        const leaf = parts.at(-1);
        if (leaf) {
          const current = node[leaf];
          const target =
            current && typeof current === "object" && !Array.isArray(current)
              ? (current as Messages)
              : (Object.create(null) as Messages);
          node[leaf] = mergeMessages(target, parsed);
        }
      }
    } catch (error) {
      if (options.strict) throw error;
      console.warn(`[wrnexus] failed to load locale '${locale}' from ${file}`, error);
    }
  }
  return output;
}

export function localeDirection(
  locale: string,
  overrides: Record<string, "ltr" | "rtl"> = {},
): "ltr" | "rtl" {
  const normalized = normalizeLocale(locale);
  return (
    overrides[normalized] ??
    overrides[normalized.split("-")[0]!] ??
    (RTL_LANGS.has(normalized.split("-")[0]!) ? "rtl" : "ltr")
  );
}

export function resolveI18n(
  messages: Record<string, Messages>,
  config: I18nConfig = {},
): ResolvedI18n {
  const normalizedMessages: Record<string, Messages> = Object.create(null) as Record<
    string,
    Messages
  >;
  for (const [locale, value] of Object.entries(messages)) {
    const normalized = normalizeLocale(locale);
    if (normalized) normalizedMessages[normalized] = value;
  }
  const configured = (config.locales ?? Object.keys(normalizedMessages))
    .map(normalizeLocale)
    .filter((locale, index, values) => locale && values.indexOf(locale) === index);
  const langs = configured.filter((locale) => normalizedMessages[locale]);
  const fallback = langs[0] ?? (normalizeLocale(config.default ?? "en") || "en");
  const requestedDefault = normalizeLocale(config.default ?? "");
  const defaultLocale =
    requestedDefault && normalizedMessages[requestedDefault] ? requestedDefault : fallback;
  if (config.strict && !normalizedMessages[defaultLocale]) {
    throw new Error(`WRN-I18N-DEFAULT-MISSING: ${defaultLocale}`);
  }
  const fallbacks: Record<string, string[]> = Object.create(null) as Record<string, string[]>;
  const direction: Record<string, "ltr" | "rtl"> = Object.create(null) as Record<
    string,
    "ltr" | "rtl"
  >;
  const labels: Record<string, string> = Object.create(null) as Record<string, string>;
  for (const locale of langs) {
    const custom = config.fallbacks?.[locale] ?? config.fallbacks?.[locale.toLowerCase()] ?? [];
    const base = locale.split("-")[0]!;
    fallbacks[locale] = [
      ...new Set([
        locale,
        ...(base !== locale ? [base] : []),
        ...custom.map(normalizeLocale),
        defaultLocale,
      ]),
    ].filter((entry) => entry && normalizedMessages[entry]);
    direction[locale] = localeDirection(locale, config.direction);
    labels[locale] =
      config.labels?.[locale] ?? config.labels?.[locale.toLowerCase()] ?? locale.toUpperCase();
  }
  return {
    default: defaultLocale,
    langs,
    messages: normalizedMessages,
    fallbacks,
    direction,
    labels,
    cookie: {
      name: config.cookie?.name ?? LANG_COOKIE,
      maxAge: config.cookie?.maxAge ?? 31_536_000,
      path: config.cookie?.path ?? "/",
      sameSite: config.cookie?.sameSite ?? "Lax",
      secure: config.cookie?.sameSite === "None" ? true : (config.cookie?.secure ?? false),
    },
  };
}

export function lookupMessage(messages: Messages | undefined, key: string): string | undefined {
  if (!messages || !key) return undefined;
  let node: unknown = messages;
  for (const part of key.split(".")) {
    if (UNSAFE_KEYS.has(part)) return undefined;
    if (node && typeof node === "object" && !Array.isArray(node) && part in (node as Messages)) {
      node = (node as Messages)[part];
    } else return undefined;
  }
  return typeof node === "string" ? node : undefined;
}

export function interpolate(message: string, params?: Record<string, string | number>): string {
  if (!params) return message;
  return message.replace(/\{([A-Za-z0-9_.-]+)\}/g, (_match, name: string) =>
    Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : `{${name}}`,
  );
}

export function translationChain(i18n: ResolvedI18n, locale: string): string[] {
  const normalized = normalizeLocale(locale);
  return (
    i18n.fallbacks[normalized] ??
    [...new Set([normalized, normalized.split("-")[0], i18n.default])].filter(
      (entry) => entry && i18n.messages[entry],
    )
  );
}

export function makeT(i18n: ResolvedI18n, lang: string): TFunction {
  const chain = translationChain(i18n, lang);
  return (key, params) => {
    for (const locale of chain) {
      const message = lookupMessage(i18n.messages[locale], key);
      if (message !== undefined) return interpolate(message, params);
    }
    return key;
  };
}

/** Deeply apply tenant-specific translations without mutating the shared locale bundle. */
export function withTenantMessages(
  i18n: ResolvedI18n,
  overrides: Record<string, Messages>,
): ResolvedI18n {
  const messages: Record<string, Messages> = Object.create(null) as Record<string, Messages>;
  for (const [locale, value] of Object.entries(i18n.messages))
    messages[locale] = mergeMessages(
      safeObject(structuredClone(value), `tenant:${locale}`),
      overrides[locale]
        ? safeObject(structuredClone(overrides[locale]), `tenant:${locale}:override`)
        : {},
    );
  return { ...i18n, messages };
}

/** Load only common and route-specific messages for one locale. */
export function loadRouteMessages(directory: string, locale: string, route: string): Messages {
  const normalized = normalizeLocale(locale);
  if (!normalized) throw new Error("WRN-I18N-ROUTE-LOCALE: invalid locale.");
  const cleanRoute = route.replace(/^\/+|\/+$/g, "").replace(/\[[^\]]+\]/g, "_") || "index";
  if (!/^[A-Za-z0-9_/-]+$/.test(cleanRoute) || cleanRoute.includes(".."))
    throw new Error("WRN-I18N-ROUTE-PATH: invalid route namespace.");
  const result = Object.create(null) as Messages;
  const candidates = [
    join(directory, `${normalized}.json`),
    join(directory, normalized, "common.json"),
    join(directory, normalized, "routes", `${cleanRoute}.json`),
  ];
  for (const file of candidates) {
    if (!existsSync(file)) continue;
    mergeMessages(result, safeObject(JSON.parse(readFileSync(file, "utf8")), file));
  }
  return result;
}

export function parseAcceptLanguage(value: string | null): string[] {
  return (value ?? "")
    .split(",")
    .map((part, index) => {
      const [tag, ...params] = part.trim().split(";");
      const quality = params.map((entry) => /^q=([0-9.]+)$/i.exec(entry.trim())?.[1]).find(Boolean);
      const normalizedTag = tag?.trim() === "*" ? "*" : normalizeLocale(tag ?? "");
      return { locale: normalizedTag, quality: quality ? Number(quality) : 1, index };
    })
    .filter(
      (entry) =>
        entry.locale && Number.isFinite(entry.quality) && entry.quality > 0 && entry.quality <= 1,
    )
    .sort((left, right) => right.quality - left.quality || left.index - right.index)
    .map((entry) => entry.locale);
}

export function resolveLang(
  i18n: ResolvedI18n,
  cookieValue: string | undefined,
  acceptLanguage: string | null,
): string {
  const cookie = normalizeLocale(cookieValue ?? "");
  if (cookie && i18n.langs.includes(cookie)) return cookie;
  for (const locale of parseAcceptLanguage(acceptLanguage)) {
    if (locale === "*") return i18n.default;
    if (i18n.langs.includes(locale)) return locale;
    const base = locale.split("-")[0]!;
    const match = i18n.langs.find(
      (supported) => supported === base || supported.startsWith(`${base}-`),
    );
    if (match) return match;
  }
  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;");
}

export function translateHtml(html: string, t: TFunction): string {
  if (!html.includes("data-t=") && !html.includes("t:")) return html;
  let output = html.replace(
    /\bt:([A-Za-z][A-Za-z0-9:_-]*)=(?:"([^"]*)"|'([^']*)')/g,
    (_match, attr: string, doubleKey: string | undefined, singleKey: string | undefined) => {
      const key = doubleKey ?? singleKey ?? "";
      const translated = t(key);
      return translated === key ? `${attr}=""` : `${attr}="${attrEscape(translated)}"`;
    },
  );
  output = output.replace(
    /<([A-Za-z][A-Za-z0-9-]*)\b([^>]*?)\sdata-t=(?:"([^"]*)"|'([^']*)')([^>]*)>([\s\S]*?)<\/\1>/g,
    (
      _match,
      tag: string,
      before: string,
      doubleKey: string | undefined,
      singleKey: string | undefined,
      after: string,
      content: string,
    ) => {
      const key = doubleKey ?? singleKey ?? "";
      const translated = t(key);
      return `<${tag}${before} data-t="${attrEscape(key)}"${after}>${
        translated === key ? content : htmlEscape(translated)
      }</${tag}>`;
    },
  );
  return output;
}

function safeJson(value: unknown): string {
  return JSON.stringify(value)
    .replace(/</g, "\\u003c")
    .replace(/>/g, "\\u003e")
    .replace(/&/g, "\\u0026")
    .replace(/\u2028/g, "\\u2028")
    .replace(/\u2029/g, "\\u2029");
}

export function renderI18nData(i18n: ResolvedI18n, lang: string): string {
  const active = i18n.langs.includes(lang) ? lang : i18n.default;
  return `window.__wireI18n=${safeJson({
    lang: active,
    langs: i18n.langs,
    default: i18n.default,
    messages: i18n.messages[active] ?? {},
    fallbackMessages: active === i18n.default ? {} : (i18n.messages[i18n.default] ?? {}),
    direction: i18n.direction[active] ?? "ltr",
    directions: i18n.direction,
    labels: i18n.labels,
    cookie: i18n.cookie,
  })};`;
}

export const I18N_RUNTIME = String.raw`
(function () {
  function lookup(messages, key) {
    var node = messages;
    var parts = String(key || "").split(".");
    for (var i = 0; i < parts.length; i++) {
      if (parts[i] === "__proto__" || parts[i] === "prototype" || parts[i] === "constructor") return undefined;
      if (!node || typeof node !== "object" || !Object.prototype.hasOwnProperty.call(node, parts[i])) return undefined;
      node = node[parts[i]];
    }
    return typeof node === "string" ? node : undefined;
  }
  function interpolate(message, params) {
    return String(message).replace(/\{([A-Za-z0-9_.-]+)\}/g, function (_, name) {
      return params && Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : "{" + name + "}";
    });
  }
  function state() { return window.__wireI18n || {}; }
  function t(key, params) {
    var current = state();
    return interpolate(lookup(current.messages, key) || lookup(current.fallbackMessages, key) || key, params);
  }
  function set(lang) {
    var current = state();
    if (current.langs && current.langs.indexOf(lang) === -1) return false;
    var cookie = current.cookie || {};
    var secure = cookie.secure || location.protocol === "https:";
    document.cookie = encodeURIComponent(cookie.name || "${LANG_COOKIE}") + "=" + encodeURIComponent(lang) +
      ";path=" + (cookie.path || "/") + ";max-age=" + (cookie.maxAge || 31536000) +
      ";samesite=" + (cookie.sameSite || "Lax") + (secure ? ";secure" : "");
    document.documentElement.lang = lang;
    document.documentElement.dir = (current.directions && current.directions[lang]) || "ltr";
    window.dispatchEvent(new CustomEvent("wrnexus:language-change", { detail: { locale: lang } }));
    location.reload();
    return true;
  }
  function bind(root) {
    var current = state();
    var currentLang = current.lang || document.documentElement.lang;
    document.documentElement.lang = currentLang || current.default || "en";
    if (current.direction) document.documentElement.dir = current.direction;
    var scope = root || document;
    var elements = [];
    if (scope.nodeType === 1) elements.push(scope);
    scope.querySelectorAll("*").forEach(function (node) { elements.push(node); });
    elements.forEach(function (node) {
      Array.from(node.attributes || []).forEach(function (attribute) {
        if (attribute.name.indexOf("t:") !== 0) return;
        var target = attribute.name.slice(2);
        if (!target) return;
        var translated = t(attribute.value);
        node.setAttribute(target, translated === attribute.value ? "" : translated);
        node.removeAttribute(attribute.name);
      });
    });
    scope.querySelectorAll("[data-t]").forEach(function (node) {
      var key = node.getAttribute("data-t");
      if (key) node.textContent = t(key);
    });
    function updateResponsiveLabels() {
      var short = window.matchMedia && window.matchMedia("(max-width: 640px)").matches;
      scope.querySelectorAll("option[data-wire-lang-option]").forEach(function (option) {
        option.textContent = option.getAttribute(short ? "data-label-short" : "data-label-long") || option.value;
      });
    }
    updateResponsiveLabels();
    if (!window.__wireI18nResponsiveBound && window.matchMedia) {
      window.__wireI18nResponsiveBound = true;
      window.matchMedia("(max-width: 640px)").addEventListener("change", function () { bind(document); });
    }
    scope.querySelectorAll("[data-wrn-preferences]").forEach(function (switcher) {
      switcher.setAttribute("data-current-language", currentLang);
      var label = switcher.querySelector(".wire-preferences__current-language");
      if (label) label.textContent = String(currentLang || "en").toUpperCase();
    });
    scope.querySelectorAll("[data-wire-lang-set]").forEach(function (node) {
      node.setAttribute("aria-pressed", String(node.getAttribute("data-wire-lang-set") === currentLang));
      if (node.__wireLangBound) return; node.__wireLangBound = 1;
      node.addEventListener("click", function () { set(node.getAttribute("data-wire-lang-set")); });
    });
    scope.querySelectorAll("select[data-wire-lang]").forEach(function (node) {
      node.value = currentLang;
      if (node.__wireLangBound) return; node.__wireLangBound = 1;
      node.addEventListener("change", function () { set(node.value); });
    });
  }
  window.__wireLang = { set: set, t: t, bind: bind, get lang() { return state().lang; } };
  window.__wireI18n = Object.assign(state(), { t: t, set: set });
  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", function () { bind(document); });
  else bind(document);
})();
`.trim();

export {
  flattenMessages,
  localeFallbacks,
  translationCoverage,
  createLocaleFormatter,
  formatMessage,
} from "./advanced.ts";
export type { LocaleFormatter } from "./advanced.ts";
export { i18nPlugin, i18nComponentsDir } from "./plugin.ts";
export type { I18nPluginOptions } from "./plugin.ts";
