/**
 * DC-106: Caddyfile-as-code — generate Caddyfile entries from structured JSON
 *
 * Allows building reverse proxy configs programmatically instead of editing
 * raw Caddyfile text. The frontend can present a visual form, send the JSON,
 * and get back a Caddyfile snippet + apply it via the Caddy admin API.
 *
 * POST /api/v1/caddycode/generate — generate Caddyfile block from JSON
 * POST /api/v1/caddycode/validate — validate a generated block
 * GET  /api/v1/caddycode/importers — list supported import formats
 */
const express = require('express');
const { ok, errorResponse } = require('../src/utils/responses');
const { REGEX } = require('../src/utilities/constants');

/**
 * DC-070: Validate the structural config that flows into generateSiteBlock.
 *
 * Threat model: `generateSiteBlock` interpolates user-controlled fields
 * (domain, tls, authService, headers.*, stripPrefix, upstream) DIRECTLY into
 * a Caddyfile text block that is later fed to `caddy.modify()` and the
 * Caddy admin /load endpoint. The /caddycode/generate endpoint is
 * authenticated (forward_auth gated), but the bug class is "compromised
 * middleware / pivot" — a JSON-only payload can be smuggled past any
 * UI-side input checks.
 *
 * Pre-fix, every field was trusted: `lines.push(`${domain} {`)` accepted any
 * string (including newlines that close the block and inject a new site),
 * `headers[key] = "${value}"` accepted arbitrary quotes (which would break
 * the surrounding `"..."` Caddy quoted-string context and inject directives),
 * and `tls`, `authService`, `stripPrefix`, `upstream` had no charset
 * restrictions at all (spaces, braces, semicolons would land verbatim).
 *
 * Post-fix: every field is constrained to a known-safe character class
 * BEFORE interpolation, and CRLF is rejected outright. Quoted-string
 * injection in header values is closed by escaping `\` and `"` per the
 * Caddy quoted-string spec (backslash escapes the next character).
 */
function validateGenerationConfig(config) {
  const errors = [];
  const {
    domain,
    upstream,
    upstreamProtocol = 'http',
    tls = 'auto',
    auth = false,
    authService = null,
    headers = {},
    stripPrefix = null,
  } = config;

  // 1. domain — RFC 1123 hostname. Reject anything with whitespace, brace,
  //    semicolon, newline, or non-printable. REGEX.DOMAIN is
  //    /^[a-z0-9]([a-z0-9.-]{0,251}[a-z0-9])?$/i in constants.js.
  if (typeof domain !== 'string' || !REGEX.DOMAIN.test(domain)) {
    errors.push('domain must be a valid hostname (letters, digits, dots, hyphens)');
  }

  // 2. upstream — `host:port` form (the only shape Caddy's reverse_proxy
  //    directive takes for non-URL upstreams). Reject `://`, whitespace,
  //    braces. Allow optional IPv6 bracket form `[::1]:5000`. Must
  //    include an explicit :port segment — a bare `localhost` would
  //    produce a Caddyfile that fails to reload (port required for
  //    reverse_proxy upstreams). Two regex branches: (a) bare host with
  //    required :port, (b) bracketed IPv6 literal with required :port.
  if (typeof upstream !== 'string'
      || !/^[a-z0-9.\-]+:\d{1,5}$/i.test(upstream)
      && !/^\[[a-z0-9.\-:.]+\]:\d{1,5}$/i.test(upstream)
  ) {
    errors.push('upstream must be host:port (host letters/digits/dots/hyphens, port 1-65535, optional IPv6 brackets)');
  }

  // 3. tls — either the literal strings 'auto' / 'internal' (handled
  //    specially below) OR a CA name like 'letsencrypt' / 'internal' that
  //    must match /^[a-z0-9._-]+$/i. Reject whitespace + braces + quotes.
  if (typeof tls !== 'string' || !/^[a-z0-9._-]+$/i.test(tls)) {
    errors.push('tls must be one of: auto, internal, or a CA name (letters, digits, dots, underscores, hyphens)');
  }

  // 4. authService — only meaningful when auth=true; otherwise ignore. Must
  //    match the existing SSO service-id charset (REGEX.SUBDOMAIN).
  if (auth) {
    if (typeof authService !== 'string' || !REGEX.SUBDOMAIN.test(authService)) {
      errors.push('authService must be a valid subdomain (lowercase, alphanumeric, hyphens)');
    }
  }

  // 5. upstreamProtocol — only 'http' or 'https'. Anything else gets coerced
  //    to 'http' but only after we explicitly accept it; reject obvious
  //    injection vectors here.
  if (upstreamProtocol !== 'http' && upstreamProtocol !== 'https') {
    errors.push('upstreamProtocol must be "http" or "https"');
  }

  // 6. headers — each key must be a valid HTTP header name ([A-Za-z0-9-]+),
  //    each value must be a string with no CR/LF and no unescaped quotes.
  if (headers && typeof headers === 'object') {
    for (const [key, value] of Object.entries(headers)) {
      if (typeof key !== 'string' || !/^[A-Za-z0-9-]+$/.test(key)) {
        errors.push(`header key "${String(key)}" must be HTTP-token chars only ([A-Za-z0-9-])`);
      }
      if (typeof value !== 'string') {
        errors.push(`header "${key}" value must be a string`);
        continue;
      }
      if (/[\r\n]/.test(value)) {
        errors.push(`header "${key}" value must not contain CR or LF`);
      }
    }
  }

  // 7. stripPrefix — must be a leading-slash path with safe chars. Reject
  //    braces, quotes, whitespace, and { } which would let the attacker
  //    open a new Caddyfile block.
  if (stripPrefix != null) {
    if (typeof stripPrefix !== 'string' || !/^\/[A-Za-z0-9._\-/]*$/.test(stripPrefix)) {
      errors.push('stripPrefix must be an absolute path (letters, digits, dots, hyphens, slashes)');
    }
  }

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

/**
 * Escape a string for safe interpolation inside a Caddyfile quoted-string
 * context. Caddy uses the same backslash-escape semantics as JSON-ish
 * contexts — `\` and `"` MUST be escaped, otherwise the attacker breaks out
 * of the quoted string and injects arbitrary directives.
 *
 * @param {string} s raw header value
 * @returns {string} escaped value (no embedded newlines; CR/LF were already
 *   rejected by the validator)
 */
function escapeCaddyQuotedString(s) {
  return String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
}

/**
 * Generate a Caddyfile site block from a structured config.
 *
 * Every interpolated field is now validated by `validateGenerationConfig`
 * first (see DC-070). Quoted-string values are escaped via
 * `escapeCaddyQuotedString` so a `"` in a header value cannot break out.
 *
 * @param {Object} config - Site configuration (already validated)
 * @returns {string} Caddyfile snippet
 */
function generateSiteBlock(config) {
  const {
    domain,
    upstream,
    upstreamProtocol = 'http',
    tls = 'auto',
    websocket = false,
    auth = false,
    authService = null,
    headers = {},
    cors = false,
    rateLimit = null,
    cache = false,
    compress = true,
    stripPrefix = null,
    redirectToHttps = true,
  } = config;

  const lines = [];
  lines.push(`${domain} {`);

  // TLS — only emit a tls directive when explicitly 'internal' or a CA
  // name; 'auto' means Caddy's default behaviour (no directive needed).
  if (tls === 'internal') {
    lines.push(`  tls internal`);
  } else if (tls === 'auto') {
    // Default — Caddy auto-provisions Let's Encrypt
  } else {
    // CA name validated by validateGenerationConfig against
    // /^[a-z0-9._-]+$/i — safe to interpolate verbatim.
    lines.push(`  tls ${tls}`);
  }

  // Redirect HTTP→HTTPS
  if (redirectToHttps) {
    lines.push(`  # Redirect HTTP to HTTPS is automatic in Caddy 2`);
  }

  // Auth gate (DashCaddy forward_auth) — authService validated by
  // validateGenerationConfig against REGEX.SUBDOMAIN — safe to interpolate.
  if (auth && authService) {
    lines.push(`  import dashcaddy_auth ${authService}`);
  }

  // CORS headers
  if (cors) {
    lines.push(`  header {`);
    lines.push(`    Access-Control-Allow-Origin *`);
    lines.push(`    Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"`);
    lines.push(`    Access-Control-Allow-Headers "Content-Type, Authorization"`);
    lines.push(`  }`);
  }

  // Custom headers — keys validated against /^[A-Za-z0-9-]+$/, values
  // escaped via escapeCaddyQuotedString before being placed inside "..."
  if (headers && typeof headers === 'object' && Object.keys(headers).length > 0) {
    lines.push(`  header {`);
    for (const [key, value] of Object.entries(headers)) {
      lines.push(`    ${key} "${escapeCaddyQuotedString(value)}"`);
    }
    lines.push(`  }`);
  }

  // Strip prefix — validated to /^\/[A-Za-z0-9._\-/]*$/ — safe.
  if (stripPrefix) {
    lines.push(`  uri strip_prefix ${stripPrefix}`);
  }

  // Compression
  if (compress) {
    lines.push(`  encode gzip zstd`);
  }

  // Reverse proxy
  const protocol = upstreamProtocol === 'https' ? 'https' : 'http';
  lines.push(`  reverse_proxy ${protocol}://${upstream} {`);
  if (websocket) {
    lines.push(`    # WebSocket support is automatic in Caddy 2`);
  }
  lines.push(`    header_up Host {host}`);
  lines.push(`    transport http {`);
  lines.push(`      read_timeout 5m`);
  lines.push(`      write_timeout 5m`);
  lines.push(`    }`);
  lines.push(`  }`);

  lines.push(`}`);

  return lines.join('\n');
}

module.exports = function({ asyncHandler }) {
  const wrap = asyncHandler || ((fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next));
  const router = express.Router();

  // POST /api/v1/caddycode/generate
  router.post('/caddycode/generate', wrap(async (req, res) => {
    const config = req.body || {};

    if (!config.domain) {
      return errorResponse(res, 400, 'domain is required');
    }
    if (!config.upstream) {
      return errorResponse(res, 400, 'upstream is required (e.g. localhost:8080)');
    }

    // DC-070: structural validation BEFORE interpolation. Every field that
    // flows into the Caddyfile text must satisfy a known-safe charset rule,
    // and CRLF is rejected outright. Run this BEFORE generateSiteBlock so
    // the bad input is rejected with a clean 400 + enumerable error list,
    // not a generated-Caddyfile + 500.
    const validation = validateGenerationConfig(config);
    if (!validation.valid) {
      return errorResponse(res, 400, 'Invalid configuration', {
        code: 'DC-CCD-700',
        errors: validation.errors,
      });
    }

    try {
      const caddyfile = generateSiteBlock(config);
      ok(res, { caddyfile, config });
    } catch (err) {
      errorResponse(res, 500, `Generation failed: ${err.message}`);
    }
  }));

  // POST /api/v1/caddycode/validate
  router.post('/caddycode/validate', wrap(async (req, res) => {
    const { caddyfile } = req.body || {};

    if (!caddyfile) {
      return errorResponse(res, 400, 'caddyfile string is required');
    }

    // Basic validation checks
    const issues = [];

    // Check for balanced braces
    const openBraces = (caddyfile.match(/{/g) || []).length;
    const closeBraces = (caddyfile.match(/}/g) || []).length;
    if (openBraces !== closeBraces) {
      issues.push(`Unbalanced braces: ${openBraces} open vs ${closeBraces} close`);
    }

    // Check for domain in first non-empty line
    const firstLine = caddyfile.trim().split('\n')[0].trim();
    if (!firstLine || firstLine.startsWith('#') || firstLine.startsWith('{')) {
      issues.push('First line should be a domain name');
    }

    // Check for reverse_proxy directive
    if (!caddyfile.includes('reverse_proxy')) {
      issues.push('No reverse_proxy directive found — site will not proxy traffic');
    }

    // Check for common mistakes
    if (caddyfile.includes('tls ')) {
      const tlsLine = caddyfile.split('\n').find(l => l.trim().startsWith('tls '));
      if (tlsLine && tlsLine.includes('auto')) {
        issues.push('tls auto is redundant — Caddy does this by default');
      }
    }

    ok(res, {
      valid: issues.length === 0,
      issues,
      warnings: [],
    });
  }));

  // GET /api/v1/caddycode/templates — preset configs for common patterns
  router.get('/caddycode/templates', wrap(async (req, res) => {
    const templates = {
      'simple-proxy': {
        label: 'Simple Reverse Proxy',
        config: {
          domain: 'app.example.com',
          upstream: 'localhost:8080',
          tls: 'auto',
          websocket: false,
          auth: false,
        },
      },
      'websocket-app': {
        label: 'WebSocket Application',
        config: {
          domain: 'app.example.com',
          upstream: 'localhost:3000',
          websocket: true,
          compress: true,
        },
      },
      'auth-gated': {
        label: 'Auth-Gated Service (DashCaddy SSO)',
        config: {
          domain: 'app.example.com',
          upstream: 'localhost:8096',
          auth: true,
          authService: 'app',
        },
      },
      'cors-api': {
        label: 'API with CORS',
        config: {
          domain: 'api.example.com',
          upstream: 'localhost:3001',
          cors: true,
          compress: true,
        },
      },
      'subdirectory': {
        label: 'Subdirectory Proxy',
        config: {
          domain: 'example.com',
          upstream: 'localhost:8080',
          stripPrefix: '/app',
        },
      },
    };

    ok(res, { templates });
  }));

  return router;
};

// DC-070: export helpers for unit-testing the sanitization surface
// independently of the route handler.
module.exports.__test = {
  validateGenerationConfig,
  escapeCaddyQuotedString,
  generateSiteBlock,
};
