/**
 * 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 { dirname, extname, join, resolve } from "node:path";
import { createRequire } from "node:module";
import { pathToFileURL } from "node:url";
import type { PerformanceBudgets, SecurityConfig, SeoConfig } from "@wrnexus/core";
import type { PluginInput, PluginPermission } from "@wrnexus/plugin";
import type { StorageConfig } from "@wrnexus/uploader";
import type { ThemeConfig } from "./theme.ts";
import type { FontConfig } from "./fonts.ts";
import { fontCspSources } from "./fonts.ts";
import {
  isCompatibilityDate,
  resolveCompatibility,
  type CompatibilityPolicy,
} from "./compatibility.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;
  /** Original application entry when a package-aware wrapper was generated. */
  originalEntryPath?: string | null;
  /** Package component/style directories that processors should scan. */
  sources?: string[];
  /** Package-owned CSS entries automatically imported into the application bundle. */
  entries?: string[];
  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;
  /** Include the complete @wrnexus/ui component catalog stylesheet. Default true. */
  includeUi?: boolean;
  /**
   * 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>;
  /** Automatically append package scan sources to custom processor input. Default true. */
  includePackageSources?: boolean;
  /** Production defaults to throw; development defaults to best-effort fallback. */
  failureMode?: "throw" | "fallback";
}

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;
  /** Ordered URL rules for runtime caching. Patterns are regular-expression source strings. */
  runtimeCaching?: Array<{
    pattern: string;
    strategy: "network-first" | "cache-first" | "stale-while-revalidate";
    cacheName?: string;
    methods?: string[];
  }>;
  /** Background Sync tag used by the offline mutation queue. */
  backgroundSyncTag?: string;
}

export type DevToolbarPosition = "bottom-center" | "bottom-left" | "bottom-right";

export interface DevToolbarConfig {
  enabled?: boolean;
  position?: DevToolbarPosition;
  defaultOpen?: boolean;
  keyboardShortcut?: string;
  scanOnNavigation?: boolean;
  scanOnHmr?: boolean;
  openEditor?: boolean;
  editor?: string;
  rules?: Partial<Record<string, boolean>>;
  severity?: Partial<Record<string, "error" | "warning" | "info" | "suggestion">>;
  ignoredRules?: string[];
  ignoredPaths?: string[];
  slowRequestMs?: number;
  largeImageBytes?: number;
  veryLargeImageBytes?: number;
}

export interface ExperimentalConfig {
  serverComponents?: boolean;
  streaming?: boolean;
  partialHydration?: boolean;
  typedRpc?: boolean;
  pluginTransforms?: boolean;
  [feature: string]: boolean | undefined;
}

export interface PerformanceConfig {
  budgets?: PerformanceBudgets;
  /** `warn` reports budget violations; `error` fails production builds. */
  enforcement?: "off" | "warn" | "error";
  analyze?: boolean;
}

export interface ObservabilityConfig {
  enabled?: boolean;
  serviceName?: string;
  serverTiming?: boolean;
  sampleRate?: number;
  exporter?: "console" | "otlp" | "none";
  endpoint?: string;
  /** Collect privacy-preserving Core Web Vitals from real browsers. */
  webVitals?: boolean;
  /** Same-origin endpoint receiving Web Vitals. */
  webVitalsEndpoint?: string;
}

export interface TenancyConfig {
  mode?: "subdomain" | "domain" | "path" | "custom";
  required?: boolean;
  rootDomains?: string[];
  pathPrefix?: string;
}

export interface BuildConfig {
  cache?: boolean;
  cacheDir?: string;
  sourceMaps?: boolean;
  report?: boolean;
  adapter?: "bun" | "node" | "static" | "serverless" | "edge" | string;
}

export interface NavigationConfig {
  /**
   * `auto` (default) omits navigation JavaScript from fully static pages and
   * progressively enhances routes that already need browser behavior.
   * `client` always enhances same-origin links with in-place page swaps.
   * `document` keeps normal browser navigation so every route performs a fresh
   * server-rendered document request.
   */
  mode?: "auto" | "client" | "document";
}

export interface ImportsConfig {
  mode?: "legacy" | "compatible" | "explicit";
  autoImport?: boolean;
  aliases?: Record<string, string>;
}

export interface TypesConfig {
  strict?: boolean;
  noImplicitAny?: boolean;
  strictNullChecks?: boolean;
  checkTemplates?: boolean;
  checkComponentProps?: boolean;
  generateDeclarations?: boolean;
  globalTypes?: string;
}

export interface FunctionsConfig {
  legacyDefaultRuntime?: "current" | "client" | "server" | "shared";
}

export interface StoresConfig {
  strictMutations?: boolean;
  persistence?: boolean;
}

export interface CompatibilityConfig {
  legacyEmit?: boolean;
  legacyEventProps?: boolean;
  legacyComponentDiscovery?: boolean;
  stringLayouts?: boolean;
}

export interface AppConfig extends CompatibilityPolicy {
  /** Ordered reusable configuration layers; the application always has final precedence. */
  extends?: string | string[];
  /** Compiler/dev/build plugins, resolved in deterministic pre/normal/post order. */
  plugins?: PluginInput;
  /** Optional least-privilege enforcement for automatically discovered packages. */
  pluginPermissions?: {
    enforce?: boolean;
    grants?: Record<string, PluginPermission[]>;
  };
  /** WRN v0.6 explicit import and compatibility resolution. */
  imports?: ImportsConfig;
  /** TypeScript-backed .wrn type checking and declaration generation. */
  types?: TypesConfig;
  /** Legacy function runtime behavior for existing applications. */
  functions?: FunctionsConfig;
  /** Typed global/page store behavior. */
  stores?: StoresConfig;
  /** Temporary v0.5 syntax compatibility switches. */
  compatibility?: CompatibilityConfig;
  /** Opt-in APIs that are not yet covered by stable compatibility guarantees. */
  experimental?: ExperimentalConfig;
  /** Route and asset budgets plus build analyzer behavior. */
  performance?: PerformanceConfig;
  /** Request tracing, Server-Timing, and exporter configuration. */
  observability?: ObservabilityConfig;
  /** First-class tenant resolution defaults. */
  tenancy?: TenancyConfig;
  /** Build cache, source map, report, and deployment adapter settings. */
  build?: BuildConfig;
  /** Page navigation strategy. Defaults to progressive client navigation. */
  navigation?: NavigationConfig;
  /** Development-only page diagnostics toolbar. Enabled by default in development. */
  devToolbar?: boolean | DevToolbarConfig;
  /** 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[];
    labels?: Record<string, string>;
    fallbacks?: Record<string, string[]>;
    direction?: Record<string, "ltr" | "rtl">;
    cookie?: {
      name?: string;
      maxAge?: number;
      path?: string;
      sameSite?: "Strict" | "Lax" | "None";
      secure?: boolean;
    };
    strict?: boolean;
  };
  /** 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, path = ""): 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)) {
    const currentPath = path ? `${path}.${key}` : key;
    if (
      key in out &&
      ["plugins", "head"].includes(currentPath) &&
      (Array.isArray(out[key]) || Array.isArray(value))
    ) {
      const before = Array.isArray(out[key]) ? out[key] : out[key] == null ? [] : [out[key]];
      const after = Array.isArray(value) ? value : value == null ? [] : [value];
      out[key] = [...before, ...after];
    } else {
      out[key] = key in out ? deepMerge(out[key], value, currentPath) : value;
    }
  }
  return out as T;
}

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

function layerSpecifiers(config: AppConfig): string[] {
  return config.extends ? (Array.isArray(config.extends) ? config.extends : [config.extends]) : [];
}

function resolveLayerFile(specifier: string, declaringRoot: string, appRoot: string): string {
  const local =
    specifier.startsWith(".") || specifier.startsWith("/") || /^[A-Za-z]:[\\/]/.test(specifier);
  if (local) {
    const candidate = resolve(declaringRoot, specifier);
    if (extname(candidate) && existsSync(candidate)) return candidate;
    for (const name of LAYER_CONFIG_NAMES) {
      const file = join(candidate, name);
      if (existsSync(file)) return file;
    }
    throw new Error(`WRN-CONFIG-LAYER-NOT-FOUND: ${specifier} from ${declaringRoot}.`);
  }
  const require = createRequire(join(appRoot, "package.json"));
  try {
    const packageFile = require.resolve(`${specifier}/package.json`);
    const packageRoot = dirname(packageFile);
    const pkg = JSON.parse(readFileSync(packageFile, "utf8")) as {
      wrnexus?: { layer?: string };
    };
    if (pkg.wrnexus?.layer) {
      const file = resolve(packageRoot, pkg.wrnexus.layer);
      if (existsSync(file)) return file;
    }
    for (const name of LAYER_CONFIG_NAMES) {
      const file = join(packageRoot, name);
      if (existsSync(file)) return file;
    }
  } catch (error) {
    throw new Error(`WRN-CONFIG-LAYER-NOT-FOUND: package ${specifier} from ${appRoot}.`, {
      cause: error,
    });
  }
  throw new Error(`WRN-CONFIG-LAYER-ENTRY: ${specifier} has no wrnexus layer entry.`);
}

async function importConfigFile(file: string): Promise<AppConfig> {
  const mod = (await import(pathToFileURL(file).href)) as { default?: AppConfig };
  if (!mod.default || !isPlainObject(mod.default)) {
    throw new Error(`WRN-CONFIG-LAYER-SHAPE: ${file} must export a configuration object.`);
  }
  return mod.default;
}

export interface ResolvedConfigLayers {
  config: AppConfig;
  sources: string[];
}

export async function resolveConfigLayers(
  appRoot: string,
  application: AppConfig,
): Promise<ResolvedConfigLayers> {
  const sources: string[] = [];
  const visiting: string[] = [];
  const resolvedFiles = new Set<string>();
  const visit = async (config: AppConfig, declaringRoot: string): Promise<AppConfig> => {
    let merged: AppConfig = {};
    for (const specifier of layerSpecifiers(config)) {
      const file = resolveLayerFile(specifier, declaringRoot, appRoot);
      if (visiting.includes(file)) {
        throw new Error(`WRN-CONFIG-LAYER-CYCLE: ${[...visiting, file].join(" -> ")}`);
      }
      visiting.push(file);
      const layer = await importConfigFile(file);
      const resolvedLayer = await visit(layer, dirname(file));
      visiting.pop();
      merged = deepMerge(merged, resolvedLayer);
      if (!resolvedFiles.has(file)) {
        resolvedFiles.add(file);
        sources.push(file);
      }
    }
    const own = { ...config };
    delete own.extends;
    return deepMerge(merged, own);
  };
  return { config: await visit(application, appRoot), sources };
}

/** 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 active = profile ?? resolveProfile();
  // Configuration modules commonly read DATABASE_URL and other settings during
  // module evaluation. Load the profile cascade before importing the module so
  // development and production behave consistently.
  loadEnv(appRoot, active);
  const raw = await loadRawConfig(appRoot);
  const { config: base } = await resolveConfigLayers(appRoot, raw);
  const override = base.profiles?.[active];
  const merged: AppConfig = override ? deepMerge(base, override) : { ...base };
  delete merged.profiles;
  delete merged.extends;
  applyFontCsp(merged);
  const issues = validateAppConfig(merged);
  const errors = issues.filter((issue) => issue.severity === "error");
  if (errors.length) {
    throw new Error(
      `Invalid wrnexus.config: ${errors.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`,
    );
  }
  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`, and the
 * static host into `connect-src` for service-worker fetch interception). 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);
    extend("connect-src", ["'self'", "ws:", "wss:"], 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;
}

export interface ConfigIssue {
  path: string;
  severity: "error" | "warning";
  message: string;
}

export function defineConfig(config: AppConfig): AppConfig {
  return config;
}

export function validateAppConfig(config: AppConfig): ConfigIssue[] {
  const issues: ConfigIssue[] = [];
  if (config.compatibilityDate !== undefined && !isCompatibilityDate(config.compatibilityDate)) {
    issues.push({
      path: "compatibilityDate",
      severity: "error",
      message: "must be a real ISO calendar date in YYYY-MM-DD format",
    });
  }
  if (
    config.frameworkBehaviour !== undefined &&
    (!Number.isInteger(config.frameworkBehaviour) || config.frameworkBehaviour < 1)
  ) {
    issues.push({
      path: "frameworkBehaviour",
      severity: "error",
      message: "must be a positive integer",
    });
  }
  const compatibility = resolveCompatibility(config);
  if (compatibility.future) {
    issues.push({
      path: "compatibilityDate",
      severity: "error",
      message: "targets framework behavior newer than this version supports",
    });
  }
  const sampleRate = config.observability?.sampleRate;
  if (
    sampleRate !== undefined &&
    (!Number.isFinite(sampleRate) || sampleRate < 0 || sampleRate > 1)
  ) {
    issues.push({
      path: "observability.sampleRate",
      severity: "error",
      message: "must be between 0 and 1",
    });
  }
  if (config.observability?.exporter === "otlp") {
    const endpoint = config.observability.endpoint;
    const validEndpoint = (() => {
      try {
        return Boolean(endpoint && ["http:", "https:"].includes(new URL(endpoint).protocol));
      } catch {
        return false;
      }
    })();
    if (!validEndpoint) {
      issues.push({
        path: "observability.endpoint",
        severity: "error",
        message: "must be an absolute HTTP(S) URL when exporter is otlp",
      });
    }
  }
  if (config.observability?.serviceName !== undefined && !config.observability.serviceName.trim()) {
    issues.push({
      path: "observability.serviceName",
      severity: "error",
      message: "must not be empty",
    });
  }
  const budgets = config.performance?.budgets;
  if (budgets) {
    for (const [name, value] of Object.entries(budgets)) {
      if (value !== undefined && (!Number.isFinite(value) || value < 0)) {
        issues.push({
          path: `performance.budgets.${name}`,
          severity: "error",
          message: "must be a non-negative finite number",
        });
      }
    }
  }
  if (config.tenancy?.mode === "path" && !config.tenancy.pathPrefix) {
    issues.push({
      path: "tenancy.pathPrefix",
      severity: "warning",
      message: "is recommended when tenancy.mode is path",
    });
  }
  return issues;
}

export interface ExplainedConfig {
  profile: string;
  config: AppConfig;
  issues: ConfigIssue[];
  sources: string[];
}

export async function explainAppConfig(
  appRoot: string,
  profile?: string,
): Promise<ExplainedConfig> {
  const active = profile ?? resolveProfile();
  const config = await loadAppConfig(appRoot, active);
  const appSources = CONFIG_NAMES.filter((name) => existsSync(join(appRoot, name)));
  const raw = await loadRawConfig(appRoot);
  const layerSources = (await resolveConfigLayers(appRoot, raw)).sources;
  const envSources = [".env", ".env.local", `.env.${active}`, `.env.${active}.local`].filter(
    (name) => existsSync(join(appRoot, name)),
  );
  return {
    profile: active,
    config,
    issues: validateAppConfig(config),
    sources: [...layerSources, ...appSources, ...envSources],
  };
}

/** 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;
}
