/**
 * 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 { csrfCookieMiddleware, csrfValidationMiddleware, CSRF_HEADER_NAME } = require('./csrf-protection');
const { RATE_LIMITS, LIMITS, APP } = require('./constants');
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 res.status(400).json({ success: false, error: '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 ──
  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);
      if (req.path !== '/health' && req.path !== '/api/health') {
        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();
    }

    if (req.path === '/health' || req.path === '/api/health' || req.path.startsWith('/probe/')) {
      return next();
    }

    if (req.path.startsWith('/api/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 res.status(403).json({
        success: false,
        error: '[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 res.status(403).json({
                success: false,
                error: '[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');
    res.setHeader('Set-Cookie',
      `${SESSION_COOKIE_NAME}=${payloadB64}.${sig}; 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) {
    res.setHeader('Set-Cookie',
      `${SESSION_COOKIE_NAME}=; Max-Age=0; 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) ──
  const PUBLIC_ROUTES = [
    { path: '/health',                exact: true },
    { path: '/api/health',            exact: true },
    { path: '/probe/',                prefix: true },
    { path: '/api/tailscale/',        prefix: true },
    { path: '/api/totp/config',        exact: true,  method: 'GET' },
    { path: '/api/totp/verify',        exact: true },
    { path: '/api/totp/setup',         exact: true,  method: 'POST' },
    { path: '/api/totp/verify-setup',  exact: true,  method: 'POST' },
    { path: '/api/totp/check-session', exact: true },
    { path: '/api/auth/gate/',        prefix: true },
    { path: '/api/auth/app-token/',   prefix: true },
    { path: '/api/services',          exact: true,  method: 'GET' },
    { path: '/api/ca/info',           exact: true,  method: 'GET' },
    { path: '/api/ca/root.crt',      exact: true,  method: 'GET' },
    { path: '/api/ca/install-script', exact: true,  method: 'GET' },
    { path: '/api/health/ca',         exact: true,  method: 'GET' },
    { path: '/api/ca/cert/',          prefix: true,  method: 'GET' },
    { path: '/api/ca/certs',          exact: true,  method: 'GET' },
    { path: '/api/csrf-token',        exact: true,  method: 'GET' },
    { path: '/api/logo',              exact: true,  method: 'GET' },
    { path: '/api/favicon',           exact: true,  method: 'GET' },
    { path: '/api/themes',            exact: true,  method: 'GET' },
    { path: '/api/license/status',    exact: true,  method: 'GET' },
    { path: '/api/license/feature/',  prefix: true, method: 'GET' },
    { path: '/api/config',             exact: true,  method: 'GET' },
    { path: '/api/services/status',    exact: true,  method: 'GET' },
  ];

  function isPublicRoute(req) {
    // Normalize /api/v1/... to /api/... so public routes work with both
    const p = req.path.replace(/^\/api\/v1\//, '/api/');
    return PUBLIC_ROUTES.some(r => {
      if (r.method && req.method !== r.method) return false;
      return r.prefix ? p.startsWith(r.path) : p === r.path;
    });
  }

  // ── TOTP auth middleware ──
  const totpAuthMiddleware = (req, res, next) => {
    if (!totpConfig.enabled || totpConfig.sessionDuration === 'never') {
      return next();
    }
    if (isPublicRoute(req)) return next();
    if (isSessionValid(req)) return next();

    return res.status(401).json({ success: false, error: '[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();
      }
    }

    if (!totpConfig.enabled || totpConfig.sessionDuration === 'never') {
      req.auth = {
        type: 'none',
        scope: ['admin']
      };
      return next();
    }

    return res.status(401).json({
      success: false,
      error: '[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/health' || req.path.startsWith('/probe/') || req.path.startsWith('/api/auth/gate/') || req.path === '/api/totp/check-session' || req.path.endsWith('/health-checks/status') || req.path.endsWith('/csrf-token') || req.path === '/api/v1/dns/logs' || req.path === '/api/license/status' || req.path.startsWith('/api/license/feature/') || req.path === '/api/services' || req.path === '/api/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/dns/credentials', strictLimiter);
  app.use('/api/apps/deploy', strictLimiter);
  app.use('/api/backup/restore', strictLimiter);
  app.use('/api/site', strictLimiter);
  app.use('/api/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/totp/verify', totpLimiter);
  app.use('/api/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
  };
};
