/**
 * Middleware Configuration Module
 * Extracts the entire middleware stack from server.js (Phase 3 refactoring)
 *
 * Configures: CORS, Helmet, body parser, compression, CSRF, request IDs,
 * metrics/access logging, Tailscale auth, TOTP sessions, JWT/API key auth,
 * rate limiting, and audit logging.
 */

const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');
const crypto = require('crypto');
const rateLimit = require('express-rate-limit');
const { createCSRFMiddleware, csrfValidationMiddleware, CSRF_HEADER_NAME } = require('../security/csrf-protection');
const { RATE_LIMITS, LIMITS, APP } = require('./constants');
const { errorResponse, unauthorized, forbidden, validationError } = require('../utils/responses');
const { CACHE_CONFIGS, createCache } = require('./cache-config');

/**
 * Configure all middleware on the Express app.
 *
 * @param {import('express').Express} app
 * @param {Object} deps - Dependencies from server.js
 * @returns {Object} Items that routes and ctx need
 */
module.exports = function configureMiddleware(app, {
  siteConfig, totpConfig, tailscaleConfig,
  metrics, auditLogger, authManager, log, cryptoUtils,
  isValidContainerId, isTailscaleIP, getTailscaleStatus
}) {

  // ── Container ID param validation ──
  app.param('id', (req, res, next, id) => {
    if (req.path.includes('/containers/') && !isValidContainerId(id)) {
      return validationError(res, 'Invalid container ID');
    }
    next();
  });

  // ── CORS (#9: origins derived from config) ──
  const corsOrigins = [`https://${siteConfig.dashboardHost}`];
  if (process.env.NODE_ENV !== 'production') corsOrigins.push('http://localhost:3001');
  app.use(cors({
    origin: corsOrigins,
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
    credentials: true
  }));

  // ── Security headers with Helmet ──
  app.use(helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        styleSrc: ["'self'"],
        scriptSrc: ["'self'"],
        imgSrc: ["'self'", "data:", "https:"],
        connectSrc: ["'self'"],
        fontSrc: ["'self'", "data:"],
        objectSrc: ["'none'"],
        mediaSrc: ["'self'"],
        frameSrc: ["'none'"]
      }
    },
    crossOriginEmbedderPolicy: false,
    crossOriginResourcePolicy: { policy: "cross-origin" }
  }));

  // ── Trust proxy (one hop — Caddy) ──
  app.set('trust proxy', 1);

  // ── JSON body parser (default 1MB limit) ──
  app.use(express.json({ limit: LIMITS.BODY_DEFAULT }));

  // ── Compress responses (gzip/brotli) ──
  app.use(compression());

  // ── CSRF protection (cookie domain set to TLD for cross-subdomain SSO) ──
  const { csrfCookieMiddleware, renewCSRFToken } = createCSRFMiddleware({
    cookieDomain: siteConfig.tld || undefined
  });
  app.use(csrfCookieMiddleware);
  app.use(csrfValidationMiddleware);

  // ── Request ID ──
  app.use((req, res, next) => {
    req.id = crypto.randomUUID();
    res.setHeader('X-Request-ID', req.id);
    next();
  });

  // ── Metrics + access log ──
  app.use((req, res, next) => {
    const start = Date.now();
    res.on('finish', () => {
      const duration = Date.now() - start;
      metrics.recordRequest(req.method, req.path, res.statusCode, duration);
      // Skip noisy per-request logging for probe endpoints — k8s/Docker
      // hit these every few seconds and would flood the audit log.
      const isProbe = req.path === '/health'
        || req.path === '/health/live'
        || req.path === '/health/ready'
        || req.path === '/healthz'
        || req.path === '/readyz';
      if (!isProbe) {
        const level = res.statusCode >= 500 ? 'error' : res.statusCode >= 400 ? 'warn' : 'debug';
        log[level]('http', `${req.method} ${req.path} ${res.statusCode}`, {
          ms: duration, ip: req.ip, id: req.id
        });
      }
    });
    next();
  });

  // ── Tailscale authentication middleware (optional) ──
  const tailscaleAuthMiddleware = async (req, res, next) => {
    if (!tailscaleConfig.enabled || !tailscaleConfig.requireAuth) {
      return next();
    }

    // Probe endpoints bypass Tailscale auth — k8s/Docker healthchecks
    // don't carry a Tailscale identity header.
    if (req.path === '/health'
      || req.path === '/health/live'
      || req.path === '/health/ready'
      || req.path === '/healthz'
      || req.path === '/readyz'
      || req.path.startsWith('/probe/')) {
      return next();
    }

    if (req.path.startsWith('/api/v1/tailscale/')) {
      return next();
    }

    const clientIP = req.ip || req.socket?.remoteAddress || '';
    const forwardedFor = req.headers['x-forwarded-for'];
    const realIP = req.headers['x-real-ip'];

    const ipsToCheck = [clientIP, forwardedFor, realIP].filter(Boolean);
    const fromTailscale = ipsToCheck.some(ip => isTailscaleIP(ip.toString().split(',')[0].trim()));

    if (!fromTailscale) {
      return errorResponse(res, 403, '[DC-120] Access denied. This dashboard requires Tailscale connection.', {
        requiresTailscale: true,
        clientIP: clientIP
      });
    }

    if (tailscaleConfig.allowedTailnet) {
      try {
        const status = await getTailscaleStatus();
        if (status) {
          const clientTailscaleIP = ipsToCheck
            .map(ip => ip.toString().split(',')[0].trim())
            .find(ip => isTailscaleIP(ip));

          if (clientTailscaleIP) {
            const knownIPs = new Set();
            for (const ip of (status.Self?.TailscaleIPs || [])) knownIPs.add(ip);
            for (const peer of Object.values(status.Peer || {})) {
              for (const ip of (peer.TailscaleIPs || [])) knownIPs.add(ip);
            }
            if (!knownIPs.has(clientTailscaleIP)) {
              return errorResponse(res, 403, '[DC-121] Access denied. Device not in allowed tailnet.', {
                requiresTailscale: true,
                clientIP
              });
            }
          }
        }
      } catch (e) {
        log.warn('tailscale', 'Tailnet verification failed, allowing request', { error: e.message });
      }
    }

    next();
  };

  app.use(tailscaleAuthMiddleware);

  // ── TOTP AUTHENTICATION ──

  const SESSION_COOKIE_NAME = 'dashcaddy_session';
  const SESSION_DURATIONS = {
    '15m': 15 * 60 * 1000,
    '30m': 30 * 60 * 1000,
    '1h':  60 * 60 * 1000,
    '2h':  2 * 60 * 60 * 1000,
    '4h':  4 * 60 * 60 * 1000,
    '8h':  8 * 60 * 60 * 1000,
    '12h': 12 * 60 * 60 * 1000,
    '24h': 24 * 60 * 60 * 1000,
    'never': null
  };

  // IP-based session store (solves cross-domain cookie issues with .sami TLD)
  const ipSessions = createCache(CACHE_CONFIGS.ipSessions);

  function getClientIP(req) {
    return req.ip || req.socket?.remoteAddress || '';
  }

  function createIPSession(req, durationKey) {
    const durationMs = SESSION_DURATIONS[durationKey];
    if (!durationMs) {
      log.warn('auth', 'createIPSession: invalid duration, no session created', { durationKey });
      return;
    }
    const ip = getClientIP(req);
    ipSessions.set(ip, { exp: Date.now() + durationMs });
  }

  function verifyIPSession(req) {
    const ip = getClientIP(req);
    const session = ipSessions.get(ip);
    if (!session) return false;
    if (session.exp <= Date.now()) {
      ipSessions.delete(ip);
      return false;
    }
    return true;
  }

  function clearIPSession(req) {
    ipSessions.delete(getClientIP(req));
  }

  function setSessionCookie(res, durationKey) {
    const durationMs = SESSION_DURATIONS[durationKey];
    if (!durationMs) return;
    const maxAge = Math.floor(durationMs / 1000);
    const payload = { v: true, exp: Date.now() + durationMs };
    const payloadB64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
    const key = cryptoUtils.loadOrCreateKey();
    const sig = crypto.createHmac('sha256', key).update(payloadB64).digest('base64url');
    const domainAttr = siteConfig.tld ? `; Domain=${siteConfig.tld}` : '';
    res.setHeader('Set-Cookie',
      `${SESSION_COOKIE_NAME}=${payloadB64}.${sig}${domainAttr}; Max-Age=${maxAge}; Path=/; HttpOnly; Secure; SameSite=Lax`
    );
  }

  function parseCookies(cookieHeader) {
    const cookies = {};
    if (!cookieHeader) return cookies;
    cookieHeader.split(';').forEach(pair => {
      const [name, ...rest] = pair.trim().split('=');
      if (name) cookies[name.trim()] = rest.join('=').trim();
    });
    return cookies;
  }

  function verifySessionCookie(cookieValue) {
    if (!cookieValue) return false;
    const parts = cookieValue.split('.');
    if (parts.length !== 2) return false;
    const [payloadB64, sig] = parts;
    const key = cryptoUtils.loadOrCreateKey();
    const expectedSig = crypto.createHmac('sha256', key).update(payloadB64).digest('base64url');
    try {
      if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expectedSig))) return false;
      const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());
      return payload.v === true && payload.exp > Date.now();
    } catch {
      return false;
    }
  }

  function clearSessionCookie(res) {
    const domainAttr = siteConfig.tld ? `; Domain=${siteConfig.tld}` : '';
    res.setHeader('Set-Cookie',
      `${SESSION_COOKIE_NAME}=; Max-Age=0${domainAttr}; Path=/; HttpOnly; SameSite=Lax`
    );
  }

  function isSessionValid(req) {
    if (verifyIPSession(req)) return true;
    const cookies = parseCookies(req.headers.cookie);
    if (verifySessionCookie(cookies[SESSION_COOKIE_NAME])) {
      const ip = getClientIP(req);
      if (totpConfig.sessionDuration && SESSION_DURATIONS[totpConfig.sessionDuration]) {
        ipSessions.set(ip, { exp: Date.now() + SESSION_DURATIONS[totpConfig.sessionDuration] });
      }
      return true;
    }
    return false;
  }

  // ── Public routes (bypass TOTP and JWT auth) ──
  // Routes here are accessible without authentication. By default the
  // monitoring/health-check endpoints are public so the dashboard can
  // render widgets before the user logs in. Set MONITORING_PUBLIC=false
  // (env var) or `monitoring: { public: false }` (config.json) to require
  // auth for these — useful for internet-exposed deployments where
  // CPU/memory/disk data is sensitive.
  const MONITORING_PUBLIC = (() => {
    if (process.env.MONITORING_PUBLIC === 'false') return false;
    if (process.env.MONITORING_PUBLIC === 'true') return true;
    // Default: check config.json if loaded
    try {
      const cfg = require('../config/site').siteConfig;
      if (cfg && cfg.monitoring && typeof cfg.monitoring.public === 'boolean') {
        return cfg.monitoring.public;
      }
    } catch { /* config not loaded yet, use default */ }
    return true; // default: public (current behavior, dashboard needs it)
  })();

  const PUBLIC_ROUTES = [
    // Health probes — root-level only. See src/app.js for the handler block.
    // Both the explicit (/health/live, /health/ready) and k8s-standard
    // (/healthz, /readyz) aliases are unauthenticated by design.
    { path: '/health',                   exact: true },
    { path: '/health/live',              exact: true },
    { path: '/health/ready',             exact: true },
    { path: '/healthz',                  exact: true },
    { path: '/readyz',                   exact: true },
    { path: '/probe/',                   prefix: true },
    { path: '/api/v1/tailscale/',        prefix: true },
    { path: '/api/v1/totp/config',        exact: true,  method: 'GET' },
    { path: '/api/v1/totp/recovery-info', exact: true,  method: 'GET' },
    { path: '/api/v1/totp/verify',        exact: true },
    { path: '/api/v1/totp/setup',         exact: true,  method: 'POST' },
    { path: '/api/v1/totp/verify-setup',  exact: true,  method: 'POST' },
    { path: '/api/v1/totp/check-session', exact: true },
    { path: '/api/v1/auth/gate/',        prefix: true },
    { path: '/api/v1/auth/app-token/',   prefix: true },
    { path: '/api/v1/auth/login-page',   exact: true,  method: 'GET' },
    { path: '/api/v1/services',          exact: true,  method: 'GET' },
    { path: '/api/v1/ca/info',           exact: true,  method: 'GET' },
    { path: '/api/v1/ca/root.crt',       exact: true,  method: 'GET' },
    { path: '/api/v1/ca/install-script', exact: true,  method: 'GET' },
    { path: '/api/v1/health/ca',         exact: true,  method: 'GET' },
    { path: '/api/v1/ca/cert/',          prefix: true, method: 'GET' },
    { path: '/api/v1/ca/certs',          exact: true,  method: 'GET' },
    { path: '/api/v1/csrf-token',        exact: true,  method: 'GET' },
    { path: '/api/v1/logo',              exact: true,  method: 'GET' },
    { path: '/api/v1/favicon',           exact: true,  method: 'GET' },
    { path: '/api/v1/themes',            exact: true,  method: 'GET' },
    { path: '/api/v1/license/status',    exact: true,  method: 'GET' },
    { path: '/api/v1/license/feature/',  prefix: true, method: 'GET' },
    { path: '/api/v1/config',             exact: true,  method: 'GET' },
    { path: '/api/v1/services/status',    exact: true,  method: 'GET' },
    { path: '/api/v1/health-checks/status', exact: true, method: 'GET' },
    // System Overview widget on the dashboard — needs the flattened CPU/mem
    // data without going through auth. See skill references/totp-and-system-overview-pitfalls.md §3.
    { path: '/api/v1/monitoring/stats',      exact: true, method: 'GET' },
    // Read-only update/version info shown on the dashboard view (verification
    // modal, topbar version, update badges). Mutating actions — update-apply,
    // rollback (POST) — are NOT listed here and stay TOTP-protected.
    { path: '/api/v1/system/version',        exact: true, method: 'GET' },
    { path: '/api/v1/system/update-status',  exact: true, method: 'GET' },
    { path: '/api/v1/system/update-history', exact: true, method: 'GET' },
    { path: '/api/v1/system/update-check',   exact: true, method: 'GET' },
    { path: '/api/v1/updates/available',     exact: true, method: 'GET' },
    { path: '/api/v1/system/update-notify', exact: true, method: 'POST' },
    // Monitoring endpoints — only public if MONITORING_PUBLIC is true
    ...(MONITORING_PUBLIC ? [
      { path: '/api/v1/monitoring/stats',     exact: true, method: 'GET' },
      { path: '/api/v1/health-checks/status', exact: true, method: 'GET' },
    ] : []),
    { path: '/api/v1/version',             exact: true, method: 'GET' },
  ];

  function isPublicRoute(req) {
    return PUBLIC_ROUTES.some(r => {
      if (r.method && req.method !== r.method) return false;
      return r.prefix ? req.path.startsWith(r.path) : req.path === r.path;
    });
  }

  // ── TOTP auth middleware ──
  const totpAuthMiddleware = (req, res, next) => {
    // If TOTP is not enabled at all, skip auth entirely — this is the initial-setup state
    if (!totpConfig.enabled) {
      req.auth = {
        type: 'none',
        scope: ['admin']
      };
      return next();
    }

    // TOTP is enabled — require a valid session, JWT, or API key
    if (isPublicRoute(req)) return next();
    if (isSessionValid(req)) return next();

    return errorResponse(res, 401, '[DC-110] Authentication required', { requiresTotp: true });
  };

  app.use(totpAuthMiddleware);

  // ── JWT/API Key authentication middleware ──
  const jwtApiKeyAuthMiddleware = async (req, res, next) => {
    if (req.totpSessionValid || isSessionValid(req)) {
      req.auth = {
        type: 'session',
        scope: ['admin']
      };
      return next();
    }

    if (isPublicRoute(req)) return next();

    const authHeader = req.headers.authorization;
    if (authHeader && authHeader.startsWith('Bearer ')) {
      const token = authHeader.substring(7);
      const jwtPayload = await authManager.verifyJWT(token);

      if (jwtPayload) {
        req.auth = {
          type: 'jwt',
          userId: jwtPayload.userId,
          scope: jwtPayload.scope || []
        };
        return next();
      }
    }

    const apiKey = req.headers['x-api-key'];
    if (apiKey) {
      const keyData = await authManager.verifyAPIKey(apiKey);

      if (keyData) {
        req.auth = {
          type: 'apikey',
          keyId: keyData.keyId,
          name: keyData.name,
          scope: keyData.scopes || []
        };
        return next();
      }
    }

    // No valid auth — reject
    return errorResponse(res, 401, '[DC-110] Authentication required - provide TOTP session, JWT token, or API key', {
      requiresTotp: totpConfig.enabled
    });
  };

  app.use(jwtApiKeyAuthMiddleware);

  // ── Rate limiting (skipped in test environment) ──
  const isTest = process.env.NODE_ENV === 'test';
  const generalLimiter = rateLimit({
    ...RATE_LIMITS.GENERAL,
    standardHeaders: true,
    legacyHeaders: false,
    skip: (req) => isTest || req.path === '/health' || req.path === '/api/v1/health' || req.path.startsWith('/probe/') || req.path.startsWith('/api/v1/auth/gate/') || req.path === '/api/v1/totp/check-session' || req.path.endsWith('/health-checks/status') || req.path.endsWith('/monitoring/stats') || req.path.endsWith('/csrf-token') || req.path === '/api/v1/dns/logs' || req.path === '/api/v1/license/status' || req.path.startsWith('/api/v1/license/feature/') || req.path === '/api/v1/services' || req.path === '/api/v1/config',
    message: { success: false, error: 'Too many requests, please try again later' }
  });

  const strictLimiter = rateLimit({
    ...RATE_LIMITS.STRICT,
    standardHeaders: true,
    legacyHeaders: false,
    skip: () => isTest,
    message: { success: false, error: 'Too many requests to this endpoint, please try again later' }
  });

  app.use(generalLimiter);
  app.use('/api/v1/dns/credentials', strictLimiter);
  app.use('/api/v1/apps/deploy', strictLimiter);
  app.use('/api/v1/backup/restore', strictLimiter);
  app.use('/api/v1/site', strictLimiter);
  app.use('/api/v1/credentials/rotate-key', strictLimiter);

  const totpLimiter = rateLimit({
    ...RATE_LIMITS.TOTP,
    standardHeaders: true,
    legacyHeaders: false,
    message: { success: false, error: 'Too many TOTP attempts, please try again later' }
  });
  app.use('/api/v1/totp/verify', totpLimiter);
  app.use('/api/v1/totp/verify-setup', totpLimiter);

  // ── Audit logging middleware (logs non-GET API requests) ──
  app.use(auditLogger.middleware());

  // ── Return items that routes and ctx need ──
  return {
    strictLimiter,
    SESSION_DURATIONS,
    getClientIP,
    createIPSession,
    setSessionCookie,
    clearIPSession,
    clearSessionCookie,
    isSessionValid,
    ipSessions,
    renewCSRFToken
  };
};
