/**
 * App configuration loader (`wrnexus.config.ts`).
 *
 * The config is optional. It lets an app inject arbitrary `<head>` HTML (ideal
 * for CDN-delivered CSS frameworks like Bootstrap or the Tailwind Play CDN) and
 * customise the global stylesheet pipeline (entry file or a custom processor for
 * Tailwind / PostCSS / Sass).
 */

import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import type { SecurityConfig, SeoConfig } from "@wrnexus/core";
import type { StorageConfig } from "@wrnexus/uploader";
import type { ThemeConfig } from "./theme.ts";
import type { FontConfig } from "./fonts.ts";
import { fontCspSources } from "./fonts.ts";

export type Mode = "development" | "production";

export interface StyleProcessContext {
  /** Resolved absolute path to the CSS entry, or null if there is none. */
  entryPath: string | null;
  appDir: string;
  appRoot: string;
  mode: Mode;
}

export interface StylesConfig {
  /** Path to the CSS entry, relative to the app root. Default: app/styles/global.css */
  entry?: string;
  /**
   * Optional custom processor. Return the final CSS string. Use this to run
   * Tailwind, PostCSS, Sass, etc. When omitted, the built-in Bun CSS bundler is
   * used (which already resolves `@import`, including from node_modules).
   */
  process?: (ctx: StyleProcessContext) => string | Promise<string>;
}

export interface MobileConfig {
  enabled?: boolean;
  /** Mobile renderer. `webview` uses Capacitor; `native` scaffolds an Expo/React Native app. */
  mode?: "webview" | "native";
  appId?: string;
  appName?: string;
  serverUrl?: string;
  userAgent?: string;
  layout?: string;
  backgroundColor?: string;
  icon?: string;
  errorTitle?: string;
  errorMessage?: string;
  /** Base URL used by a fully native client for WrNexus API and realtime requests. */
  apiUrl?: string;
  /** URL scheme used for native deep links (defaults to a slug of appName). */
  scheme?: string;
  /** Advanced Expo app config fields merged into generated app.config.ts. */
  expo?: Record<string, unknown>;
  /** Advanced CapacitorConfig fields merged into generated capacitor.config.ts. */
  capacitor?: Record<string, unknown>;
}

export interface PwaScreenshot {
  src: string;
  sizes: string;
  type?: string;
  formFactor?: "wide" | "narrow";
  label?: string;
}

export interface PwaShortcut {
  name: string;
  shortName?: string;
  description?: string;
  url: string;
  icons?: Array<{ src: string; sizes: string; type?: string; purpose?: string }>;
}

export interface PwaConfig {
  enabled?: boolean;
  id?: string;
  name?: string;
  shortName?: string;
  description?: string;
  startUrl?: string;
  scope?: string;
  lang?: string;
  display?: "standalone" | "fullscreen" | "minimal-ui" | "browser";
  orientation?:
    | "any"
    | "natural"
    | "landscape"
    | "landscape-primary"
    | "landscape-secondary"
    | "portrait"
    | "portrait-primary"
    | "portrait-secondary";
  themeColor?: string;
  backgroundColor?: string;
  icons?: Array<{ src: string; sizes: string; type?: string; purpose?: string }>;
  categories?: string[];
  screenshots?: PwaScreenshot[];
  shortcuts?: PwaShortcut[];
  /** Disable service-worker registration while keeping the web manifest. */
  serviceWorker?: boolean;
  /** Navigation shown when both the network and requested page cache are unavailable. */
  offlineUrl?: string;
  /** Additional same-origin URLs precached during service-worker installation. */
  cacheUrls?: string[];
  /** Service-worker cache key. Change it to invalidate existing PWA caches. */
  cacheName?: string;
}

export interface AppConfig {
  /** Raw HTML appended to every page's `<head>` (e.g. CDN stylesheet links). */
  head?: string | string[];
  /** Global SEO defaults merged with every page's exported `meta`. */
  seo?: SeoConfig;
  /** Framework security headers and optional CORS policy. */
  security?: SecurityConfig;
  styles?: StylesConfig;
  /** Capacitor/native shell defaults and mobile-only page rendering. */
  mobile?: MobileConfig;
  /** Progressive Web App metadata. Enabled by default unless set to false. */
  pwa?: PwaConfig | false;
  /**
   * Fonts. Declare Google Fonts (subsetted + preconnect + `font-display`) and/or
   * self-hosted `@font-face` (with preload), and set `sans`/`mono`/`serif` family
   * stacks. Google Fonts auto-extend the CSP so they load under the default policy.
   */
  fonts?: FontConfig;
  /** Design-token themes (deep-merged over the built-in light/dark). */
  theme?: ThemeConfig;
  /** i18n: default language + supported locales (strings live in app/locales/*.json). */
  i18n?: { default?: string; locales?: string[] };
  /** Default database connection (driver + url); reached with `getDb()`. */
  db?: { driver: "sqlite" | "postgres" | "mysql" | "mongo"; url: string };
  /**
   * File-upload storage. Declare named stores (local dir or S3-compatible),
   * upload with `handleUpload`/`upload` from `@wrnexus/uploader`, and serve
   * files back. Each store is `access: "public" | "private"`.
   */
  storage?: StorageConfig;
  /**
   * Additional named databases, reached with `getDb("<name>")`. Each has its own
   * migrations/queries under `app/db/<name>/`. Connect to as many as you like and
   * read/write to any of them per request.
   *
   *   databases: { analytics: { driver: "postgres", url: "…" } }
   */
  databases?: Record<string, { driver: "sqlite" | "postgres" | "mysql" | "mongo"; url: string }>;
  /**
   * Realtime scaling. When `scale` is true (or `redisUrl` is set), room
   * broadcasts are bridged over Redis pub/sub so they reach clients on **every**
   * app process/instance — realtime that works with multiple running apps.
   */
  realtime?: { scale?: boolean; redisUrl?: string };
  /** Default server port. */
  port?: number;
  /**
   * Named config profiles (dev, prod, uat, test, …). When a profile is active
   * its overrides are DEEP-MERGED over the base config. Select with
   * `--profile=<name>` or the `WRNEXUS_PROFILE` env var.
   */
  profiles?: Record<string, Partial<Omit<AppConfig, "profiles">>>;
}

const CONFIG_NAMES = ["wrnexus.config.ts", "wrnexus.config.js", "wrnexus.config.mjs"];

/**
 * Resolve the active profile name: explicit argument > `WRNEXUS_PROFILE` env var
 * > a mode-based default ("production" in prod, else "development").
 */
export function resolveProfile(options: { explicit?: string; mode?: Mode } = {}): string {
  const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process
    ?.env;
  return (
    options.explicit ||
    env?.WRNEXUS_PROFILE ||
    (options.mode === "production" ? "production" : "development")
  );
}

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

/** Deep-merge `override` onto `base` (objects merge; arrays/scalars replace). */
function deepMerge<T>(base: T, override: unknown): T {
  if (!isPlainObject(base) || !isPlainObject(override)) return (override ?? base) as T;
  const out: Record<string, unknown> = { ...base };
  for (const [key, value] of Object.entries(override)) {
    out[key] = key in out ? deepMerge(out[key], value) : value;
  }
  return out as T;
}

/** Load the raw `wrnexus.config.*` (with the `profiles` map intact), or `{}`. */
export async function loadRawConfig(appRoot: string): Promise<AppConfig> {
  for (const name of CONFIG_NAMES) {
    const file = join(appRoot, name);
    if (existsSync(file)) {
      const mod = (await import(pathToFileURL(file).href)) as { default?: AppConfig };
      return mod.default ?? {};
    }
  }
  return {};
}

/** Load `wrnexus.config.*`, applying the active profile's overrides. */
export async function loadAppConfig(appRoot: string, profile?: string): Promise<AppConfig> {
  const base = await loadRawConfig(appRoot);
  const active = profile ?? resolveProfile();
  const override = base.profiles?.[active];
  const merged: AppConfig = override ? deepMerge(base, override) : { ...base };
  delete merged.profiles;
  applyFontCsp(merged);
  return merged;
}

/**
 * Auto-extend the CSP so configured Google Fonts load under the default policy
 * (their CSS host into `style-src`, the static host into `font-src`). No-op when
 * the app disabled CSP (`security.contentSecurityPolicy: false`) or uses no
 * Google Fonts. Local (self-hosted) fonts are served from `'self'` and need nothing.
 */
function applyFontCsp(config: AppConfig): void {
  const add = fontCspSources(config.fonts);
  if (!add.style.length && !add.font.length) return;
  const security = (config.security ??= {});
  if (security.contentSecurityPolicy === false) return;
  const csp = security.contentSecurityPolicy ?? {};
  security.contentSecurityPolicy = csp;
  const dirs = csp.directives ?? {};
  csp.directives = dirs;
  const extend = (name: string, base: string[], adds: string[]) => {
    const cur = Array.isArray(dirs[name]) ? (dirs[name] as string[]) : base;
    dirs[name] = Array.from(new Set([...cur, ...adds]));
  };
  if (add.style.length) extend("style-src", ["'self'", "'unsafe-inline'"], add.style);
  if (add.font.length) extend("font-src", ["'self'", "data:"], add.font);
}

/**
 * Load the `.env` cascade for a profile into `process.env`, WITHOUT clobbering
 * variables already set in the real environment (which always win). Order, low
 * → high precedence: `.env` < `.env.<profile>` < `.env.local` < `.env.<profile>.local`.
 * Returns the variables it loaded.
 */
export function loadEnv(appRoot: string, profile: string): Record<string, string> {
  const proc = (globalThis as { process?: { env: Record<string, string | undefined> } }).process;
  const env = proc?.env ?? {};
  const realKeys = new Set(Object.keys(env));
  const loaded: Record<string, string> = {};
  for (const name of [".env", `.env.${profile}`, ".env.local", `.env.${profile}.local`]) {
    const file = join(appRoot, name);
    if (!existsSync(file)) continue;
    for (const [key, value] of Object.entries(parseDotenv(readFileSync(file, "utf8")))) {
      loaded[key] = value;
      if (!realKeys.has(key)) env[key] = value; // never override real env
    }
  }
  return loaded;
}

function parseDotenv(content: string): Record<string, string> {
  const out: Record<string, string> = {};
  for (const line of content.split(/\r?\n/)) {
    const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
    if (!m) continue;
    let value = m[2]!.trim();
    if (
      (value.startsWith('"') && value.endsWith('"')) ||
      (value.startsWith("'") && value.endsWith("'"))
    ) {
      const quoted = value.slice(1, -1);
      value = value[0] === '"' ? quoted.replace(/\\n/g, "\n").replace(/\\t/g, "\t") : quoted;
    } else {
      const comment = value.indexOf(" #"); // strip trailing comments on unquoted values
      if (comment >= 0) value = value.slice(0, comment).trim();
    }
    out[m[1]!] = value;
  }
  return out;
}

/** Flatten a head config into a single HTML string. */
export function headToString(head?: string | string[]): string {
  if (!head) return "";
  return Array.isArray(head) ? head.filter(Boolean).join("\n    ") : head;
}
