/**
 * Security middleware for Fastify
 * Adds security headers, removes version information, and validates secrets.
 * Operator guide: docs/GATEWAY-SECRETS-M3.md
 */

import { FastifyRequest, FastifyReply } from 'fastify';
import { env, createLogger } from '@4runr/shared';
import { isMemoryMode } from '../config/persistence.js';
import { signingSecretProductionError } from '../security/agent-token.js';

const logger = createLogger('Gateway:Security');

/** Must match `redis-credentials-store.ts` */
const DEFAULT_DEV_ENCRYPTION_KEY = 'default-dev-key-change-in-production';

const INSECURE_SECRET_SUBSTRINGS = [
  'secret',
  'change-me',
  'change-in-production',
  'test-secret',
  'default-dev',
  'jwt-secret',
];

export type SecretsValidationResult = {
  valid: boolean;
  errors: string[];
  warnings: string[];
};

function looksInsecure(value: string): boolean {
  const lower = value.toLowerCase();
  return INSECURE_SECRET_SUBSTRINGS.some((p) => lower.includes(p));
}

function validateKekBase64(): string | null {
  const b64 = process.env['KEK_BASE64'];
  if (!b64?.trim()) {
    return 'KEK_BASE64 is required in production when using encrypted credentials (MFA, tool credentials, envelope crypto)';
  }
  try {
    const buf = Buffer.from(b64, 'base64');
    if (buf.length < 32) {
      return 'KEK_BASE64 must decode to at least 32 bytes (use e.g. openssl rand -base64 32)';
    }
  } catch {
    return 'KEK_BASE64 must be valid base64';
  }
  return null;
}

function validateSigningSecret(): string | null {
  return signingSecretProductionError(process.env['SIGNING_SECRET']);
}

function validateEncryptionKey(): string | null {
  const k = process.env['ENCRYPTION_KEY'] ?? env.ENCRYPTION_KEY;
  if (!k || k.length < 32) {
    return 'ENCRYPTION_KEY must be at least 32 characters in production (Redis AI provider credentials)';
  }
  if (k === DEFAULT_DEV_ENCRYPTION_KEY || looksInsecure(k)) {
    return 'ENCRYPTION_KEY must not use the default dev value in production';
  }
  return null;
}

/**
 * Set security headers on all responses
 */
export async function setSecurityHeaders(
  request: FastifyRequest,
  reply: FastifyReply
): Promise<void> {
  // Remove server version header (if present)
  reply.removeHeader('X-Powered-By');
  reply.removeHeader('Server');

  // Content Security Policy
  reply.header(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' ws: wss:; frame-ancestors 'none';"
  );

  // Strict Transport Security (HSTS) - only if HTTPS
  if (request.protocol === 'https' || request.headers['x-forwarded-proto'] === 'https') {
    reply.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
  }

  // X-Content-Type-Options
  reply.header('X-Content-Type-Options', 'nosniff');

  // X-Frame-Options
  reply.header('X-Frame-Options', 'DENY');

  // X-XSS-Protection
  reply.header('X-XSS-Protection', '1; mode=block');

  // Referrer Policy
  reply.header('Referrer-Policy', 'strict-origin-when-cross-origin');

  // Permissions Policy (formerly Feature-Policy)
  reply.header(
    'Permissions-Policy',
    'geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), speaker=()'
  );

  // Cross-Origin Embedder Policy
  reply.header('Cross-Origin-Embedder-Policy', 'require-corp');

  // Cross-Origin Opener Policy
  reply.header('Cross-Origin-Opener-Policy', 'same-origin');

  // Cross-Origin Resource Policy
  reply.header('Cross-Origin-Resource-Policy', 'same-origin');

  // Don't expose internal headers
  reply.header('X-Gateway', '4Runr'); // Generic name, no version
}

/**
 * Validate that required secrets are present and not default values.
 * Aligns with runtime checks in `redis-credentials-store`, `mfa-storage`, and `tool-proxy`.
 */
export function validateSecrets(): SecretsValidationResult {
  const errors: string[] = [];
  const warnings: string[] = [];
  const isProduction = process.env['NODE_ENV'] === 'production';
  const memoryMode = isMemoryMode();
  const authMode = process.env['AUTH_MODE'] ?? env.AUTH_MODE;
  const authEnabled =
    process.env['AUTH_ENABLED'] === undefined
      ? env.AUTH_ENABLED
      : process.env['AUTH_ENABLED'] === 'true';
  const databaseUrl = process.env['DATABASE_URL'] ?? env.DATABASE_URL;
  const jwtSecret = process.env['JWT_SECRET'] ?? env.JWT_SECRET;

  if (isProduction && !memoryMode && !databaseUrl) {
    errors.push('DATABASE_URL is required in production (unless GATEWAY_PERSISTENCE=memory)');
  }

  if ((authMode === 'jwt' || authMode === 'mixed') && !jwtSecret) {
    if (isProduction) {
      errors.push('JWT_SECRET is required when AUTH_MODE is jwt or mixed');
    } else {
      warnings.push('JWT_SECRET is not set; JWT auth will fail when AUTH_ENABLED=true');
    }
  }

  if (jwtSecret) {
    if (jwtSecret.length < 32) {
      errors.push('JWT_SECRET is too short (minimum 32 characters)');
    } else if (looksInsecure(jwtSecret)) {
      errors.push('JWT_SECRET appears to use an insecure default value');
    }
  }

  if (isProduction) {
    const signingErr = validateSigningSecret();
    if (signingErr) errors.push(signingErr);

    if (!memoryMode) {
      const encErr = validateEncryptionKey();
      if (encErr) errors.push(encErr);

      const kekErr = validateKekBase64();
      if (kekErr) errors.push(kekErr);
    } else {
      warnings.push(
        'GATEWAY_PERSISTENCE=memory: ENCRYPTION_KEY and KEK_BASE64 are not required at startup; MFA/tool credential encryption still requires KEK_BASE64 if those routes are used'
      );
    }

    if (authEnabled && (authMode === 'api_key' || authMode === 'mixed')) {
      if (!process.env['API_KEY'] && !process.env['API_KEYS']) {
        warnings.push(
          'No API_KEY/API_KEYS in environment; production api_key auth expects keys in the database or env'
        );
      }
    }
  } else {
    if (!process.env['SIGNING_SECRET'] || process.env['SIGNING_SECRET'].length < 32) {
      warnings.push('SIGNING_SECRET is unset or short; dev agent-token signing uses a default secret');
    }
    if (!memoryMode && !process.env['ENCRYPTION_KEY'] && !env.ENCRYPTION_KEY) {
      warnings.push('ENCRYPTION_KEY is unset; Redis credential store will use a dev default outside production');
    }
    if (!process.env['KEK_BASE64']) {
      warnings.push('KEK_BASE64 is unset; MFA and tool credentials store plaintext in non-production');
    }
  }

  return {
    valid: errors.length === 0,
    errors,
    warnings,
  };
}

/**
 * Log security validation on startup; refuse to start in production when invalid.
 */
export function logSecurityValidation(): void {
  const validation = validateSecrets();

  if (validation.warnings.length > 0) {
    logger.warn('Security validation warnings', { warnings: validation.warnings });
  }

  if (!validation.valid) {
    logger.error('Security validation failed', { errors: validation.errors });
    if (process.env['NODE_ENV'] === 'production') {
      throw new Error(
        `Gateway refused to start: invalid secrets configuration — ${validation.errors.join('; ')}`
      );
    }
    return;
  }

  logger.info('Security validation passed');
}

