/**
 * Express application setup
 * Configures middleware, assembles context, and mounts routes
 */
const express = require('express');
const https = require('https');
const fs = require('fs');

// Configuration
const config = require('./config');
const { assembleContext } = require('./context');
const { createLogger, logError, safeErrorMessage } = require('./utils/logging');
const { fetchT } = require('./utils/http');
const { errorResponse, ok } = require('./utils/responses');
// Note: 3-arg asyncHandler signature (logError, fn, context) preserved per Hermes review
// — 49 route files still use this signature.
const { asyncHandler } = require('./utils/async-handler');

// Managers and utilities
const StateManager = require('managers/state-manager');
const platformPaths = require('../platform-paths');
const { LicenseManager } = require('managers/license-manager');
const credentialManager = require('managers/credential-manager');
const authManager = require('managers/auth-manager');
const dockerSecurity = require('security/docker-security');
const auditLogger = require('security/audit-logger');
const portLockManager = require('managers/port-lock-manager');
const resourceMonitor = require('managers/resource-monitor');
const backupManager = require('utilities/backup-manager');
const healthChecker = require('monitoring/health-checker');
const updateManager = require('managers/update-manager');
const selfUpdater = require('docker/self-updater');
const configureMiddleware = require('utilities/middleware');
const { validateStartupConfig: _validateStartupConfig, syncHealthCheckerServices } = require('utilities/startup-validator');
const { CSRF_HEADER_NAME } = require('security/csrf-protection');
const { resolveServiceUrl } = require('utilities/url-resolver');
const metrics = require('monitoring/metrics');
const { validateURL } = require('security/input-validator');

// Optional modules
let dockerMaintenance, logDigest;
try { dockerMaintenance = require('docker/docker-maintenance'); } catch (_) { /* optional module */ }
try { logDigest = require('security/log-digest'); } catch (_) { /* optional module */ }

// Workflow engine (bundled workflows)
let bundledWorkflowsModule;
let workflowEngine = null;
try { 
  bundledWorkflowsModule = require('recipes/bundled-workflows'); 
} catch (_) { /* optional module */ }

// Templates
const { APP_TEMPLATES, TEMPLATE_CATEGORIES, DIFFICULTY_LEVELS } = require('docker/app-templates');
const { RECIPE_TEMPLATES, RECIPE_CATEGORIES } = require('recipes/recipe-templates');

// Route modules
const healthRoutes = require('../routes/health');
const monitoringRoutes = require('../routes/monitoring');
const updatesRoutes = require('../routes/updates');
const authRoutes = require('../routes/auth');
const configRoutes = require('../routes/config');
const dnsRoutes = require('../routes/dns');
const notificationRoutes = require('../routes/notifications');
const containerRoutes = require('../routes/containers');
const serviceRoutes = require('../routes/services');
const tailscaleRoutes = require('../routes/tailscale');
const sitesRoutes = require('../routes/sites');
const credentialsRoutes = require('../routes/credentials');
const arrRoutes = require('../routes/arr');
const appsRoutes = require('../routes/apps');
const logsRoutes = require('../routes/logs');
const backupsRoutes = require('../routes/backups');
const caRoutes = require('../routes/ca');
const browseRoutes = require('../routes/browse');
const errorLogsRoutes = require('../routes/errorlogs');
const licenseRoutes = require('../routes/license');
const openClawRoutes = require('../routes/openclaw');
const recipesRoutes = require('../routes/recipes');
const themesRoutes = require('../routes/themes');
const dockerResourcesRoutes = require('../routes/docker-resources');
const eventsRoutes = require('../routes/events');
const workflowsRoutes = require('../routes/workflows');
const dependenciesRoutes = require('../routes/dependencies');
const DependencyManager = require('managers/dependency-manager');
const autoRestartRoutes = require('../routes/auto-restart');
const configDriftRoutes = require('../routes/config-drift');
const sslMonitorRoutes = require('../routes/ssl-monitor');
const { AutoRestartManager } = require('managers/auto-restart-manager');
const { ConfigDriftDetector } = require('managers/config-drift-detector');
const SSLMonitor = require('monitoring/ssl-monitor');
const DNSPropagationChecker = require('dns/dns-propagation');

// Constants
const { APP } = require('utilities/constants');

/**
 * Create and configure the Express application
 */
// eslint-disable-next-line require-await -- kept async for API consistency with other factory functions
async function createApp() {
  const app = express();

  // Global request timeout (default 5 minutes — covers slow Docker pulls)
  // Routes that need longer can override per-request with req.setTimeout()
  const REQUEST_TIMEOUT_MS = parseInt(process.env.REQUEST_TIMEOUT_MS, 10) || 5 * 60 * 1000;
  app.use((req, res, next) => {
    req.setTimeout(REQUEST_TIMEOUT_MS);
    res.setTimeout(REQUEST_TIMEOUT_MS);
    next();
  });
  // Disable x-powered-by header for security (don't advertise framework)
  app.disable('x-powered-by');
  // Trust first proxy (Caddy/nginx in front of us) so req.ip works correctly
  app.set('trust proxy', 1);

  // Initialize logging
  const log = createLogger(config.LOG_LEVEL);

  // Load site configuration
  config.loadSiteConfig(config.CONFIG_FILE, log);

  // Create state managers
  const servicesStateManager = new StateManager(config.SERVICES_FILE);
  const configStateManager = new StateManager(config.CONFIG_FILE);

  // Initialize license manager
  const licenseManager = new LicenseManager(credentialManager, config.CONFIG_FILE, console);
  licenseManager.loadSecret(config.LICENSE_SECRET_FILE);

  // HTTPS agent for internal CA
  const CA_CERT_PATH = process.env.CA_CERT_PATH || platformPaths.pkiRootCert;
  let httpsAgent;
  try {
    const caCert = fs.readFileSync(CA_CERT_PATH);
    httpsAgent = new https.Agent({ ca: [...require('tls').rootCertificates, caCert] });
    log.info('server', 'HTTPS agent configured with CA certificate', { path: CA_CERT_PATH });
  } catch {
    httpsAgent = new https.Agent();
    log.warn('server', 'CA cert not found — HTTPS calls may fail', { path: CA_CERT_PATH });
  }

  // TOTP configuration
  const totpConfig = {
    enabled: false,
    sessionDuration: 'never',
    isSetUp: false
  };

  // Load TOTP config from file
  try {
    if (fs.existsSync(config.TOTP_CONFIG_FILE)) {
      const loaded = JSON.parse(fs.readFileSync(config.TOTP_CONFIG_FILE, 'utf8'));
      delete loaded.secret; // secret belongs only in credential-manager
      Object.assign(totpConfig, loaded);
      log.info('config', 'TOTP config loaded', { enabled: totpConfig.enabled });
    }
  } catch (e) {
    log.warn('config', 'Could not load TOTP config', { error: e.message });
  }

  // Tailscale configuration  
  const tailscaleConfig = {
    enabled: false,
    requireAuth: false,
    allowedTailnet: null,
    devices: [],
    oauthConfigured: false,
    tailnet: null,
    syncInterval: 300,
    lastSync: null
  };

  // Helper functions needed by middleware
  function isValidContainerId(id) {
    const CONTAINER_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/;
    return typeof id === 'string' && CONTAINER_ID_RE.test(id);
  }

  function isTailscaleIP(ip) {
    if (!ip) return false;
    const parts = ip.split('.');
    if (parts.length !== 4) return false;
    const first = parseInt(parts[0]);
    const second = parseInt(parts[1]);
    return first === 100 && second >= 64 && second <= 127;
  }

  function isPrivateLan(ip) {
    if (!ip) return false;
    if (ip.startsWith('192.168.') || ip.startsWith('10.')) return true;
    return /^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(ip);
  }

  function collectNetworkInterfaces(osModule) {
    const out = [];
    const interfaces = osModule.networkInterfaces();
    for (const [name, addrs] of Object.entries(interfaces)) {
      for (const addr of addrs) {
        if (addr.internal || addr.family !== 'IPv4') continue;
        out.push({ name, ip: addr.address });
      }
    }
    return out;
  }

  // eslint-disable-next-line require-await -- stub for now, will gain await when wired into context
  async function getTailscaleStatus() {
    // Stub for now - will be populated by context
    return null;
  }

  // Back-compat: reverse-proxy SSO snippets (Caddy forward_auth + per-service
  // auto-login pages) historically call these endpoints under the pre-1.5.0
  // prefix `/api/auth/...`. The canonical mount is `/api/v1`. Hand-maintained
  // Caddyfiles have repeatedly drifted back to the old prefix and 404'd the SSO
  // gate (breaking Plex/Jellyfin/Emby/chat). Transparently rewrite ONLY these two
  // auth paths to the v1 mount so the gate is tolerant of that drift. Must run
  // before configureMiddleware() so CSRF/auth see the canonical path. This is
  // deliberately narrow — NOT a general `/api` -> `/api/v1` alias.
  app.use((req, res, next) => {
    if (req.url.startsWith('/api/auth/gate/') || req.url.startsWith('/api/auth/app-token/')) {
      req.url = '/api/v1' + req.url.slice(4); // '/api'.length === 4
    }
    next();
  });

  // Configure middleware
  const middlewareResult = configureMiddleware(app, {
    siteConfig: config.siteConfig,
    totpConfig,
    tailscaleConfig,
    metrics,
    auditLogger,
    authManager,
    log,
    cryptoUtils: require('security/crypto-utils'),
    isValidContainerId,
    isTailscaleIP,
    getTailscaleStatus,
    RATE_LIMITS: require('utilities/constants').RATE_LIMITS,
    LIMITS: require('utilities/constants').LIMITS,
    APP: require('utilities/constants').APP,
    CACHE_CONFIGS: require('utilities/cache-config').CACHE_CONFIGS,
    createCache: require('utilities/cache-config').createCache,
  });

  const { strictLimiter } = middlewareResult;

  // Helper functions
  async function getServiceById(serviceId) {
    const services = await servicesStateManager.read();
    return services.find(s => s.id === serviceId) || null;
  }

  // eslint-disable-next-line require-await -- may grow awaits as config loading evolves
  async function readConfig() {
    const { readJsonFile } = require('utilities/fs-helpers');
    return readJsonFile(config.CONFIG_FILE, {});
  }

  async function saveConfig(updates) {
    return await configStateManager.update(cfg => Object.assign(cfg, updates));
  }

  async function addServiceToConfig(service) {
    await servicesStateManager.update(services => {
      const existingIndex = services.findIndex(s => s.id === service.id);
      if (existingIndex >= 0) {
        services[existingIndex] = { ...services[existingIndex], ...service };
      } else {
        services.push(service);
      }
      return services;
    });
    log.info('deploy', 'Service added to config', { serviceId: service.id });
  }

  async function saveTotpConfig() {
    try {
      const { writeJsonFile } = require('utilities/fs-helpers');
      await writeJsonFile(config.TOTP_CONFIG_FILE, totpConfig);
    } catch (e) {
      log.error('config', 'Could not save TOTP config', { error: e.message });
    }
  }

  async function loadNotificationConfig() {
    // Stub - will be implemented
  }

  // Forwards the promise from syncHealthCheckerServices — intentionally not
  // `async` since there is no `await` inside. Callers use `.catch()` on it.
  function resyncHealthChecker() {
    return syncHealthCheckerServices({
      log,
      SERVICES_FILE: config.SERVICES_FILE,
      servicesStateManager,
      healthChecker,
      buildServiceUrl: config.buildServiceUrl,
      siteConfig: config.siteConfig,
      APP
    });
  }

  // Create bound logError function (3-arg signature: ctx, err, extra)
  // The unified logger module has its own ERROR_LOG_FILE from process.env,
  // so we just route through its logErrorWrapper.
  const boundLogError = (context, error, additionalInfo) =>
    logError(context, error, additionalInfo);

  // Create bound asyncHandler (3-arg: logError, fn, context)
  const boundAsyncHandler = (fn, context) => asyncHandler(boundLogError, fn, context);

  // Assemble context
  const ctx = assembleContext({
    // Config
    siteConfig: config.siteConfig,
    buildDomain: config.buildDomain,
    buildServiceUrl: config.buildServiceUrl,
    SERVICES_FILE: config.SERVICES_FILE,
    CONFIG_FILE: config.CONFIG_FILE,
    TOTP_CONFIG_FILE: config.TOTP_CONFIG_FILE,
    TAILSCALE_CONFIG_FILE: config.TAILSCALE_CONFIG_FILE,
    NOTIFICATIONS_FILE: config.NOTIFICATIONS_FILE,
    ERROR_LOG_FILE: config.ERROR_LOG_FILE,
    DNS_CREDENTIALS_FILE: config.DNS_CREDENTIALS_FILE,
    CADDYFILE_PATH: config.CADDYFILE_PATH,
    CADDY_ADMIN_URL: config.CADDY_ADMIN_URL,

    // State managers
    servicesStateManager,
    configStateManager,

    // Managers
    credentialManager,
    authManager,
    licenseManager,
    healthChecker,
    updateManager,
    backupManager,
    resourceMonitor,
    auditLogger,
    portLockManager,
    selfUpdater,
    dockerMaintenance,
    logDigest,
    dockerSecurity,

    // Templates
    APP_TEMPLATES,
    TEMPLATE_CATEGORIES,
    DIFFICULTY_LEVELS,
    RECIPE_TEMPLATES,
    RECIPE_CATEGORIES,

    // Helpers
    asyncHandler: boundAsyncHandler,
    errorResponse,
    ok,
    fetchT,
    httpsAgent,
    log,
    logError: boundLogError,
    safeErrorMessage,
    getServiceById,
    readConfig,
    saveConfig,
    addServiceToConfig,
    validateURL,
    strictLimiter,
    totpConfig,
    saveTotpConfig,
    loadSiteConfig: () => config.loadSiteConfig(config.CONFIG_FILE, log),
    loadNotificationConfig,
    resyncHealthChecker,

    // Middleware result
    middlewareResult,

    // App
    app,
  });

  // Initialize workflow engine if bundled-workflows is available
  if (bundledWorkflowsModule && ctx.docker) {
    try {
      const { WorkflowEngine } = bundledWorkflowsModule;
      const workflowCtx = {
        docker: ctx.docker,
        notification: ctx.notification,
        backupManager: ctx.backupManager,
        resourceMonitor: ctx.resourceMonitor,
        servicesStateManager: ctx.servicesStateManager
      };
      workflowEngine = new WorkflowEngine(workflowCtx);
      ctx.workflowEngine = workflowEngine;
      log.info('app', 'Workflow engine initialized');
    } catch (err) {
      log.error('app', 'Failed to initialize workflow engine', { error: err.message });
    }
  }

  // Initialize dependency manager
  const dependencyManager = new DependencyManager({
    servicesStateManager,
    docker: ctx.docker,
    notification: ctx.notification,
    log,
  });
  ctx.dependencyManager = dependencyManager;
  log.info('app', 'Dependency manager initialized');

  // Initialize auto-restart manager
  const autoRestartManager = new AutoRestartManager(ctx);
  ctx.autoRestartManager = autoRestartManager;
  autoRestartManager.start();
  log.info('app', 'Auto-restart manager initialized');

  // Initialize config drift detector
  const driftDetector = new ConfigDriftDetector(ctx);
  ctx.driftDetector = driftDetector;
  driftDetector.startPolling(300000); // 5 min
  log.info('app', 'Config drift detector initialized');

  // Initialize SSL monitor
  const sslMonitor = new SSLMonitor(ctx);
  ctx.sslMonitor = sslMonitor;
  sslMonitor.start(3600000); // 1 hour
  log.info('app', 'SSL monitor initialized');

  // Initialize DNS propagation checker
  const dnsPropagationChecker = new DNSPropagationChecker(ctx);
  ctx.dnsPropagationChecker = dnsPropagationChecker;
  log.info('app', 'DNS propagation checker initialized');

  // Build versioned API router
  const apiRouter = express.Router();

  // Version endpoint — public, no auth required
  // Reads version from package.json at startup so the response always matches the running code
  let appVersion = '0.0.0';
  let appName = 'dashcaddy-api';
  try {
    const pkg = require('../package.json');
    appVersion = pkg.version || appVersion;
    appName = pkg.name || appName;
  } catch { /* package.json unreadable — keep fallback */ }
  apiRouter.get('/version', (req, res) => {
    ok(res, {
      name: appName,
      version: appVersion,
      node: process.version,
      platform: process.platform,
      arch: process.arch,
      uptime: process.uptime(),
      instanceId: process.env.DASHCADDY_INSTANCE_ID || null
    });
  });
  log.info('app', `Version endpoint available at /api/v1/version (v${appVersion})`);

  // Wire up notification listeners for resourceMonitor and backupManager
  if (ctx.notification && ctx.resourceMonitor) {
    ctx.resourceMonitor.on('alert', (alertData) => {
      ctx.notification.sendAlert(alertData).catch(err => {
        log.error('notification', 'Failed to send alert', { error: err.message });
      });
    });
    ctx.resourceMonitor.on('auto-restart', (data) => {
      ctx.notification.sendServiceEvent('auto-restart', data).catch(err => {
        log.error('notification', 'Failed to send auto-restart notification', { error: err.message });
      });
    });
  }

  if (ctx.notification && ctx.backupManager) {
    ctx.backupManager.on('backup-complete', (data) => {
      ctx.notification.send('backup-complete', data).catch(err => {
        log.error('notification', 'Failed to send backup-complete', { error: err.message });
      });
    });
    ctx.backupManager.on('backup-failed', (data) => {
      ctx.notification.send('backup-failed', data).catch(err => {
        log.error('notification', 'Failed to send backup-failed', { error: err.message });
      });
    });
  }

  // Mount route modules
  apiRouter.use(authRoutes(ctx));
  apiRouter.use(configRoutes(ctx));
  apiRouter.use('/dns', dnsRoutes({
    dns: ctx.dns,
    siteConfig: ctx.siteConfig,
    asyncHandler: ctx.asyncHandler,
    log: ctx.log,
    safeErrorMessage: ctx.safeErrorMessage,
    fetchT: ctx.fetchT,
    credentialManager: ctx.credentialManager,
    dnsPropagationChecker: ctx.dnsPropagationChecker
  }));
  apiRouter.use('/notifications', notificationRoutes({
    notification: ctx.notification,
    asyncHandler: ctx.asyncHandler,
    ok: ctx.ok
  }));
  apiRouter.use('/containers', containerRoutes({
    docker: ctx.docker,
    log: ctx.log,
    asyncHandler: ctx.asyncHandler,
    workflowEngine: ctx.workflowEngine
  }));
  apiRouter.use(serviceRoutes({
    servicesStateManager: ctx.servicesStateManager,
    credentialManager: ctx.credentialManager,
    siteConfig: ctx.siteConfig,
    buildServiceUrl: ctx.buildServiceUrl,
    buildDomain: ctx.buildDomain,
    fetchT: ctx.fetchT,
    asyncHandler: ctx.asyncHandler,
    SERVICES_FILE: ctx.SERVICES_FILE,
    log: ctx.log,
    safeErrorMessage: ctx.safeErrorMessage,
    resyncHealthChecker: ctx.resyncHealthChecker,
    caddy: ctx.caddy,
    dns: ctx.dns
  }));
  apiRouter.use(healthRoutes({
    fetchT: ctx.fetchT,
    SERVICES_FILE: ctx.SERVICES_FILE,
    servicesStateManager: ctx.servicesStateManager,
    siteConfig: ctx.siteConfig,
    buildServiceUrl: ctx.buildServiceUrl,
    asyncHandler: ctx.asyncHandler,
    logError: ctx.logError,
    healthChecker: ctx.healthChecker
  }));
  apiRouter.use(monitoringRoutes({
    resourceMonitor: ctx.resourceMonitor,
    docker: ctx.docker,
    asyncHandler: ctx.asyncHandler,
    log: ctx.log,
    notificationManager: ctx.notification
  }));
  apiRouter.use(updatesRoutes({
    updateManager: ctx.updateManager,
    selfUpdater: ctx.selfUpdater,
    asyncHandler: ctx.asyncHandler,
    logError: ctx.logError,
    ok: ctx.ok
  }));
  apiRouter.use('/tailscale', tailscaleRoutes({
    tailscale: ctx.tailscale,
    caddy: ctx.caddy,
    servicesStateManager: ctx.servicesStateManager,
    credentialManager: ctx.credentialManager,
    buildDomain: ctx.buildDomain,
    asyncHandler: ctx.asyncHandler,
    ok: ctx.ok,
    SERVICES_FILE: ctx.SERVICES_FILE,
    log: ctx.log
  }));
  apiRouter.use(sitesRoutes({
    asyncHandler: ctx.asyncHandler,
    ok: ctx.ok,
    caddy: ctx.caddy,
    dns: ctx.dns,
    fetchT: ctx.fetchT,
    buildDomain: ctx.buildDomain,
    addServiceToConfig: ctx.addServiceToConfig,
    siteConfig: ctx.siteConfig,
    log: ctx.log
  }));
  apiRouter.use(credentialsRoutes({
    credentialManager: ctx.credentialManager,
    asyncHandler: ctx.asyncHandler
  }));
  apiRouter.use(arrRoutes(ctx));
  apiRouter.use(appsRoutes(ctx));
  apiRouter.use('/openclaw', openClawRoutes(ctx));
  apiRouter.use(logsRoutes({
    asyncHandler: ctx.asyncHandler,
    ok: ctx.ok,
    docker: ctx.docker,
    logDigest: ctx.logDigest,
    dockerMaintenance: ctx.dockerMaintenance
  }));
  apiRouter.use(backupsRoutes({
    backupManager: ctx.backupManager,
    licenseManager: ctx.licenseManager,
    asyncHandler: ctx.asyncHandler
  }));
  apiRouter.use('/ca', caRoutes(ctx));
  apiRouter.use(browseRoutes({
    asyncHandler: ctx.asyncHandler,
    validateSecurePath: ctx.validateSecurePath,
    auditLogger: ctx.auditLogger,
    docker: ctx.docker
  }));
  apiRouter.use(errorLogsRoutes({
    ERROR_LOG_FILE: ctx.ERROR_LOG_FILE,
    auditLogger: ctx.auditLogger,
    asyncHandler: ctx.asyncHandler
  }));
  apiRouter.use('/license', licenseRoutes({
    licenseManager: ctx.licenseManager,
    asyncHandler: ctx.asyncHandler
  }));
  apiRouter.use('/recipes', recipesRoutes(ctx));
  apiRouter.use(themesRoutes({ asyncHandler: ctx.asyncHandler, log: ctx.log }));
  apiRouter.use('/docker', dockerResourcesRoutes({
    docker: ctx.docker,
    asyncHandler: ctx.asyncHandler
  }));
  apiRouter.use('/events', eventsRoutes({
    resourceMonitor: ctx.resourceMonitor,
    healthChecker: ctx.healthChecker,
    updateManager: ctx.updateManager,
    logError: ctx.logError,
    ok: ctx.ok,
    dependencyManager: ctx.dependencyManager,
    autoRestartManager: ctx.autoRestartManager,
    driftDetector: ctx.driftDetector,
    sslMonitor: ctx.sslMonitor,
    dnsPropagationChecker: ctx.dnsPropagationChecker
  }));
  apiRouter.use('/workflows', workflowsRoutes({
    workflowEngine: ctx.workflowEngine,
    licenseManager: ctx.licenseManager,
    asyncHandler: ctx.asyncHandler,
    ok: ctx.ok
  }));
  apiRouter.use('/dependencies', dependenciesRoutes({
    dependencyManager: ctx.dependencyManager,
    servicesStateManager: ctx.servicesStateManager,
    docker: ctx.docker,
    asyncHandler: ctx.asyncHandler,
    logError: ctx.logError,
    resyncHealthChecker: ctx.resyncHealthChecker,
    log: ctx.log,
  }));
  apiRouter.use(autoRestartRoutes({
    autoRestartManager: ctx.autoRestartManager,
    asyncHandler: ctx.asyncHandler,
    logError: ctx.logError,
  }));
  apiRouter.use(configDriftRoutes({
    driftDetector: ctx.driftDetector,
    asyncHandler: ctx.asyncHandler,
    logError: ctx.logError,
  }));
  apiRouter.use(sslMonitorRoutes({
    sslMonitor: ctx.sslMonitor,
    asyncHandler: ctx.asyncHandler,
    logError: ctx.logError,
  }));

  // Inline API routes (mounted under /api/v1 below)
  // Note: /health lives at root only — see root-level health check below.
  // Probes (/healthz, /readyz, /health/live, /health/ready) also at root only.
  // Do NOT add another /api/v1/health route — it's been consolidated.
  
  apiRouter.get('/csrf-token', (req, res) => {
    ok(res, { token: req.csrfToken, headerName: CSRF_HEADER_NAME });
  });
  
  apiRouter.get('/metrics', (req, res) => {
    ok(res, { metrics: metrics.getSummary() });
  });

  // Mount at /api/v1 (canonical, single version)
  app.use('/api/v1', apiRouter);

  // ===========================================================================
  // Health probes — root-level, no auth, no CSRF, no rate limit.
  //
  // Two semantics, four paths:
  //
  //   LIVENESS  — "is the Node.js process alive?"
  //     /health/live    (explicit, recommended)
  //     /healthz        (k8s/Docker-standard alias)
  //   READINESS — "are critical dependencies reachable?"
  //     /health/ready   (explicit, recommended)
  //     /readyz         (k8s/Docker-standard alias)
  //
  // k8s/Docker/Caddy call these to decide whether to RESTART or ROUTE TRAFFIC.
  // They MUST stay cheap (no DB queries, no logging side effects, no auth).
  //
  // Plain /health is kept for backwards compatibility and returns the same
  // payload as /health/live. Use /health/live or /healthz in new code.
  // ===========================================================================

  // Liveness — pure process check, no deps.
  const livenessHandler = (req, res) => {
    ok(res, { status: 'alive', uptime: process.uptime() });
  };

  // Readiness — checks critical dependencies (config, services file,
  // Docker daemon, Caddy admin). 200 if all OK, 503 if any failed.
  const readinessHandler = boundAsyncHandler(async (req, res) => {
    const checks = {};
    let allOk = true;

    // Check 1: Config file readable
    try {
      const fs = require('fs');
      if (fs.existsSync(config.CONFIG_FILE)) {
        fs.readFileSync(config.CONFIG_FILE, 'utf8');
        checks.configFile = { ok: true };
      } else {
        checks.configFile = { ok: false, error: 'Config file not found' };
        allOk = false;
      }
    } catch (e) {
      checks.configFile = { ok: false, error: e.message };
      allOk = false;
    }

    // Check 2: Services file readable
    try {
      const fs = require('fs');
      if (fs.existsSync(config.SERVICES_FILE)) {
        fs.readFileSync(config.SERVICES_FILE, 'utf8');
        checks.servicesFile = { ok: true };
      } else {
        checks.servicesFile = { ok: false, error: 'Services file not found' };
        allOk = false;
      }
    } catch (e) {
      checks.servicesFile = { ok: false, error: e.message };
      allOk = false;
    }

    // Check 3: Docker daemon reachable
    try {
      const docker = require('dockerode')();
      await docker.ping();
      checks.docker = { ok: true };
    } catch (e) {
      checks.docker = { ok: false, error: e.message };
      allOk = false;
    }

    // Check 4: Caddy admin API reachable
    try {
      const caddyUrl = config.CADDY_ADMIN_URL || 'http://localhost:2019';
      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(), 3000);
      const response = await fetch(`${caddyUrl}/config/`, {
        signal: controller.signal
      });
      clearTimeout(timeout);
      checks.caddy = { ok: response.ok, status: response.status };
      if (!response.ok) allOk = false;
    } catch (e) {
      checks.caddy = { ok: false, error: e.message };
      allOk = false;
    }

    const body = {
      status: allOk ? 'ready' : 'not-ready',
      timestamp: new Date().toISOString(),
      checks
    };
    ok(res, body, allOk ? 200 : 503);
  });

  // Liveness paths
  app.get('/health', livenessHandler);
  app.get('/health/live', livenessHandler);
  app.get('/healthz', livenessHandler);

  // Readiness paths
  app.get('/health/ready', readinessHandler);
  app.get('/readyz', readinessHandler);

  // Lightweight probe endpoint
  app.get('/probe/:id', boundAsyncHandler(async (req, res) => {
    const id = req.params.id;
    const { exists } = require('utilities/fs-helpers');

    let service = null;
    if (id !== 'internet' && await exists(config.SERVICES_FILE)) {
      const data = await servicesStateManager.read();
      const services = Array.isArray(data) ? data : data.services || [];
      service = services.find(s => s.id === id);
    }

    const url = resolveServiceUrl(id, service, config.siteConfig, config.buildServiceUrl);
    const parsed = new URL(url);
    const isHttps = parsed.protocol === 'https:';
    const lib = isHttps ? https : require('http');

    const options = {
      hostname: parsed.hostname,
      port: parsed.port || (isHttps ? 443 : 80),
      path: parsed.pathname + parsed.search,
      method: 'HEAD',
      timeout: 5000,
      agent: isHttps ? httpsAgent : undefined,
      headers: { 'User-Agent': APP.USER_AGENTS.PROBE },
    };

    const makeRequest = (method) => new Promise((resolve, reject) => {
      const reqOpts = { ...options, method };
      const probeReq = lib.request(reqOpts, (response) => {
        response.resume();
        resolve(response.statusCode);
      });
      probeReq.on('error', reject);
      probeReq.on('timeout', () => { probeReq.destroy(); reject(new Error('Timeout')); });
      probeReq.end();
    });

    let statusCode;
    try {
      statusCode = await makeRequest('HEAD');
      if (statusCode === 501 || statusCode === 405) {
        statusCode = await makeRequest('GET');
      }
    } catch {
      // Direct probe failed — try Pylon relay if configured
      const pylonConfig = config.siteConfig?.pylon;
      if (pylonConfig?.url) {
        try {
          const pylonUrl = `${pylonConfig.url}/probe?url=${encodeURIComponent(url)}`;
          const headers = { 'User-Agent': APP.USER_AGENTS.PROBE };
          if (pylonConfig.key) headers['x-pylon-key'] = pylonConfig.key;
          const controller = new AbortController();
          const pylonTimeout = setTimeout(() => controller.abort(), 8000);
          const pylonRes = await fetchT(pylonUrl, { method: 'GET', signal: controller.signal, headers });
          clearTimeout(pylonTimeout);
          if (pylonRes.ok) {
            const data = await pylonRes.json();
            statusCode = data.statusCode || 502;
          }
        } catch {
          // Pylon also failed — fall through to domain fallback
        }
      }

      // Domain-based fallback (last resort)
      if (!statusCode) {
        const fallbackUrl = `https://${config.buildDomain(id)}`;
        const fp = new URL(fallbackUrl);
        statusCode = await new Promise((resolve, reject) => {
          const fReq = https.request({
            hostname: fp.hostname,
            port: 443,
            path: '/',
            method: 'GET',
            timeout: 5000,
            agent: httpsAgent,
            headers: { 'User-Agent': APP.USER_AGENTS.PROBE }
          }, (fRes) => {
            fRes.resume();
            resolve(fRes.statusCode);
          });
          fReq.on('error', reject);
          fReq.on('timeout', () => { fReq.destroy(); reject(new Error('Timeout')); });
          fReq.end();
        }).catch(() => 502);
      }
    }

    res.status(statusCode).send();
  }, 'probe'));

  // Scan OS network interfaces and classify the first LAN + Tailscale IPv4
  // addresses. Extracted to keep the route handler below ESLint's max-depth.
  function detectInterfaceIps() {
    const os = require('os');
    const LAN_RANGE = /^(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)/;
    const all = [];
    let lan = null;
    let tailscale = null;
    const interfaces = os.networkInterfaces();
    for (const [name, addrs] of Object.entries(interfaces)) {
      for (const addr of addrs || []) {
        if (addr.internal || addr.family !== 'IPv4') continue;
        const { address: ip } = addr;
        all.push({ name, ip });
        if (!tailscale && ip.startsWith('100.')) {
          tailscale = ip;
        } else if (!lan && LAN_RANGE.test(ip)) {
          lan = ip;
        }
      }
    }
    return { lan, tailscale, all };
  }

  // Network IPs endpoint
  app.get('/api/v1/network/ips', (req, res) => {
    try {
      const envLan = process.env.HOST_LAN_IP;
      const envTailscale = process.env.HOST_TAILSCALE_IP;

      const result = {
        localhost: '127.0.0.1',
        lan: envLan || null,
        tailscale: envTailscale || null,
        all: []
      };

      if (!envLan || !envTailscale) {
        result.all = collectNetworkInterfaces(os);
        if (!result.tailscale) {
          result.tailscale = result.all.find(i => isTailscaleIP(i.ip))?.ip || null;
        }
        if (!result.lan) {
          result.lan = result.all.find(i => isPrivateLan(i.ip))?.ip || null;
        }
      }

      ok(res, result);
    } catch (error) {
      errorResponse(res, 500, safeErrorMessage(error));
    }
  });

  // API Documentation
  app.get('/api/v1/docs', (req, res) => {
    res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline' https://unpkg.com; style-src 'self' 'unsafe-inline' https://unpkg.com; img-src 'self' data: https:; connect-src 'self'; font-src 'self' data: https://unpkg.com;");
    res.send(`<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8"/>
  <title>DashCaddy API Documentation</title>
  <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css"/>
  <style>body{margin:0} .swagger-ui .topbar{display:none}</style>
</head>
<body>
  <div id="swagger-ui"></div>
  <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
  <script>SwaggerUIBundle({url:'/api/v1/docs/spec',dom_id:'#swagger-ui',deepLinking:true})</script>
</body>
</html>`);
  });

  app.get('/api/v1/docs/spec', boundAsyncHandler(async (req, res) => {
    const path = require('path');
    const { exists } = require('utilities/fs-helpers');
    const fsp = require('fs').promises;
    
    const specPath = path.join(__dirname, '../openapi.yaml');
    if (await exists(specPath)) {
      const yaml = await fsp.readFile(specPath, 'utf8');
      res.type('text/yaml').send(yaml);
    } else {
      errorResponse(res, 404, 'OpenAPI spec not found');
    }
  }, 'api-docs-spec'));

  // Error handlers (MUST be last)
  const { notFoundHandler, errorMiddleware } = require('utilities/error-handler');
  app.use('/api', notFoundHandler);
  app.use(errorMiddleware);

  return { app, log, config: config.siteConfig, licenseManager };
}

module.exports = { createApp };
