/**
 * Caddy upstreams routes
 *
 * Exposes:
 *   GET    /api/v1/caddy/upstreams                       — full snapshot
 *   GET    /api/v1/caddy/upstreams/incidents             — open dead-upstream incidents (via healthChecker)
 *   POST   /api/v1/caddy/upstreams/mute                  — body { host, muted: true|false }
 *   POST   /api/v1/caddy/upstreams/:host/mute            — body { muted: true|false } OR query ?muted=true
 *   POST   /api/v1/caddy/upstreams/:host/unmute          — clears the mute
 *
 * Auth: same as the rest of /api/v1 — handled by the global middleware
 * (the router is mounted under the auth-gated apiRouter in app.js).
 *
 * @module routes/caddy-upstreams
 */

const express = require('express');
const { success, errorResponse } = require('../src/utils/responses');
const { ValidationError } = require('../src/utilities/errors');

/**
 * DC-073: shared mute helper — used by all three mute endpoints so the
 * host-validation logic can't drift.
 *
 * Pre-fix, only the bare `/caddy/upstreams/mute` body-style endpoint
 * rejected unknown hosts (with a "not a known upstream" 400). The
 * path-style `/:host/mute` and `/:host/unmute` endpoints skipped that
 * check entirely, so an authenticated operator could POST
 * `/caddy/upstreams/phantom.test:12345/mute` and the watcher would
 * silently add `phantom.test:12345` to its muted Set and `_saveState()`
 * would persist it to disk. The phantom entry then survives container
 * restarts, pollutes the snapshot view (the muted Set is iterated in
 * places like the dashboard's "muted upstreams" badge), and would
 * silently disable any future probe that happened to resolve to the
 * same string.
 *
 * Post-fix, every mute path runs through this helper so:
 *   (1) host format is well-formed (rejects injection / `:` / `?` / etc.)
 *   (2) host is in `caddyUpstreamWatcher.upstreams` (the live registry
 *       populated by `scanSites()` reading every `reverse_proxy` from
 *       /etc/caddy/sites/*. A phantom host cannot reach setMuted.)
 *   (3) the muted Set never holds entries the scanner doesn't know.
 *
 * @param {Object} watcher caddyUpstreamWatcher instance
 * @param {string} host raw host string from the request
 * @param {boolean} wantMuted true to mute, false to unmute
 * @returns {{host: string, muted: boolean}} the result of setMuted
 * @throws {ValidationError} on invalid format or unknown host
 */
function validateAndMuteHost(watcher, host, wantMuted) {
  if (typeof host !== 'string' || host.length === 0 || host.length > 253) {
    throw new ValidationError('host must be a non-empty string up to 253 chars');
  }
  if (!/^[a-z0-9._:-]+$/i.test(host)) {
    throw new ValidationError('host must be a valid host[:port] string');
  }
  if (!watcher || !watcher.upstreams || !watcher.upstreams.has(host)) {
    throw new ValidationError(`host ${host} is not a known upstream (run scan first)`);
  }
  return watcher.setMuted(host, wantMuted);
}

module.exports = function({ asyncHandler, caddyUpstreamWatcher, healthChecker }) {
  const router = express.Router();

  router.get('/caddy/upstreams', asyncHandler(async (req, res) => {
    if (!caddyUpstreamWatcher) {
      // DC-062: errorResponse(res, statusCode, message) — statusCode-first per
      // src/utils/responses.js:66. The prior (res, message, statusCode) call
      // order passed a STRING as the status code, which made
      // res.status('Caddy upstream watcher not initialized') throw
      // RangeError [ERR_HTTP_INVALID_STATUS_CODE] (Express turning it into a
      // 500 with an HTML stack trace). All four `!caddyUpstreamWatcher`
      // guards had the same latent bug — fixed to canonical order.
      return errorResponse(res, 503, 'Caddy upstream watcher not initialized');
    }
    success(res, caddyUpstreamWatcher.snapshot());
  }, 'caddy-upstreams-list'));

  router.get('/caddy/upstreams/incidents', asyncHandler(async (req, res) => {
    if (!healthChecker) {
      return success(res, { incidents: [] });
    }
    // Filter the in-memory incidents array to caddy-upstream-dead entries.
    const all = Array.isArray(healthChecker.incidents) ? healthChecker.incidents : [];
    const open = all
      .filter((i) => i && i.type === 'caddy-upstream-dead' && i.status === 'open')
      .map((i) => ({
        id: i.id,
        serviceId: i.serviceId,
        type: i.type,
        message: i.message,
        severity: i.severity,
        createdAt: i.createdAt,
        lastOccurrence: i.lastOccurrence,
        occurrences: i.occurrences,
        details: i.details
      }));
    success(res, { incidents: open });
  }, 'caddy-upstreams-incidents'));

  // Bare /mute with JSON body {host, muted}. Default mutes when muted is
  // absent or unparseable; require muted === false explicitly to unmute.
  // DC-073: now routes through validateAndMuteHost so the unknown-host
  // check applies (was already correct here pre-fix, but path-style
  // was missing it — see validateAndMuteHost docblock).
  router.post('/caddy/upstreams/mute', asyncHandler(async (req, res) => {
    if (!caddyUpstreamWatcher) {
      return errorResponse(res, 503, 'Caddy upstream watcher not initialized');
    }
    const { host, muted } = req.body || {};
    // Explicit boolean coercion — string 'false' should NOT mute.
    const wantMuted = muted === undefined ? true : muted === true;
    const result = validateAndMuteHost(caddyUpstreamWatcher, host, wantMuted);
    success(res, result);
  }, 'caddy-upstreams-mute-bare'));

  // Path-style /:host/mute — body { muted: true|false } OR query ?muted=true|false.
  // DC-073: now also rejects unknown hosts (was the bug — see docblock).
  router.post('/caddy/upstreams/:host/mute', asyncHandler(async (req, res) => {
    if (!caddyUpstreamWatcher) {
      return errorResponse(res, 503, 'Caddy upstream watcher not initialized');
    }
    let wantMuted;
    if (typeof req.body?.muted === 'boolean') wantMuted = req.body.muted;
    else if (typeof req.query.muted === 'string') wantMuted = req.query.muted === 'true';
    else wantMuted = true; // bare POST = mute
    const result = validateAndMuteHost(caddyUpstreamWatcher, req.params.host, wantMuted);
    success(res, result);
  }, 'caddy-upstreams-mute'));

  // DC-073: path-style /:host/unmute now also rejects unknown hosts.
  router.post('/caddy/upstreams/:host/unmute', asyncHandler(async (req, res) => {
    if (!caddyUpstreamWatcher) {
      return errorResponse(res, 503, 'Caddy upstream watcher not initialized');
    }
    const result = validateAndMuteHost(caddyUpstreamWatcher, req.params.host, false);
    success(res, result);
  }, 'caddy-upstreams-unmute'));

  return router;
};

// Export the helper for unit tests so the validation surface can be
// exercised without spinning up a full Express app.
module.exports.__test = { validateAndMuteHost };