/**
 * Tailscale admin & settings routes
 *
 * Two distinct surfaces, both gated by DashCaddy's TOTP auth:
 *
 *   GET    /api/v1/tailscale/settings
 *     Returns { configured, tailnetName, deviceCount, keyValidatedAt }
 *     NEVER returns the raw API token.
 *
 *   PUT    /api/v1/tailscale/settings
 *     body: { apiToken: 'tskey-api-...' }
 *     Validates by pinging /api/v2/tailnet/-/preferences. On success,
 *     stores the token encrypted and writes tailscale-config.json metadata.
 *     Returns the same shape as GET (without the token).
 *
 *   DELETE /api/v1/tailscale/settings
 *     Clears the stored token and metadata.
 *
 *   POST   /api/v1/tailscale/settings/test
 *     body: { apiToken?: 'tskey-api-...' }   // optional; defaults to stored
 *     Pings Tailscale with the given token (or stored one) and returns
 *     { valid: bool, tailnetName?, error? }. Does NOT save anything.
 *
 *   GET    /api/v1/tailscale/admin/devices
 *     Lists all devices in the tailnet via the coord API. 503 if not configured.
 *
 *   GET    /api/v1/tailscale/admin/users
 *     Lists tailnet users.
 *
 *   GET    /api/v1/tailscale/admin/keys
 *     Lists pre-auth keys (metadata only, never the secret).
 *
 *   POST   /api/v1/tailscale/admin/keys
 *     body: { reusable?, ephemeral?, preauthorized?, tags?, description?, expirySeconds? }
 *     Creates a new pre-auth key. Returns { id, key } — the `key` is the
 *     ONLY time the secret is available, callers must show it to the user
 *     immediately and not store it.
 *
 *   DELETE /api/v1/tailscale/admin/keys/:id
 *     Revokes a pre-auth key.
 *
 *   DELETE /api/v1/tailscale/admin/devices/:id
 *     Revokes a device from the tailnet.
 *
 * # DC-080 input validation
 *
 * Three coupled gaps in the route layer pre-fix:
 *
 *   (a) PUT /settings validated `apiToken.startsWith('tskey-api-')` but had
 *       no length cap — body-parser limit was the only ceiling. A 1 MB
 *       string starting with `tskey-api-` would be `.trim()`-ed, sent to
 *       Tailscale's /devices endpoint, and waste server-side CPU on a
 *       request that will always 401.
 *   (b) POST /settings/test accepted `apiToken` from the body with NO
 *       validation at all. The PUT route's prefix check is bypassed on
 *       the test path — an operator could submit any string and have the
 *       container ping Tailscale's API with it (low impact, but inconsistent
 *       with PUT and surfaces fingerprinting via the 401 timing).
 *   (c) POST /admin/keys validated `tags` as Array but NOT per-element
 *       type — `tags: ['tag:guest', null, 123, {injection: true}]` would be
 *       forwarded to Tailscale verbatim. Tailscale's API is JSON-strict
 *       and would 400 the request, but the bad shape reached the wire.
 *       Similarly `description` had no length cap (Tailscale caps at 120
 *       chars per their docs).
 *
 * All three are gated by TOTP — this is a logged-in-operator / phished-
 * session threat surface, not anonymous-unauth. The fix is defense-in-
 * depth: a bug in the auth path (TOTP bypass, session theft, future
 * route handler trust-boundary drift) should not turn these endpoints
 * into a "submit anything and forward to Tailscale" relay.
 */

const express = require('express');
const { ok, errorResponse } = require('../src/utils/responses');
const { TailscaleCoordError } = require('../src/managers/tailscale-coord');

// DC-080: shared validation helpers for the Tailscale admin surface.
// Tailscale API tokens follow the form `tskey-<kind>-<opaque>` where
// `<kind>` is one of a small set of values (`api`, `auth`, `partner`,
// `cli`). Real tokens observed in the wild are 40..80 chars; we cap at
// 256 to leave headroom for future Tailscale key formats without giving
// an unbounded buffer to validate+forward.
const TAILSCALE_TOKEN_PREFIX = 'tskey-api-';
const TAILSCALE_TOKEN_MAX_LEN = 256;
const TAG_KEY_MAX_LEN = 64;
const TAGS_MAX_LEN = 32;
const DESCRIPTION_MAX_LEN = 120;

// Tailscale tags are lowercased identifiers with optional colons
// (e.g. `tag:server`, `tag:guest-plex`). Reject whitespace, CR/LF,
// control chars, JSON metacharacters, and any character that could
// enable header-injection through the Tailscale coord client.
//
// DC-080 round-2 polish: Tailscale's tag spec requires `tag:` followed by
// ≥1 identifier char — bare `tag:` (empty name) is rejected by their API.
// We split the pattern in two so the error message names which form failed
// instead of dumping a generic regex.
const TAG_KEY_RE = /^tag:[a-z0-9][a-z0-9_-]{0,62}$/;

function _validateApiToken(token, fieldName = 'apiToken') {
  if (typeof token !== 'string' || !token) {
    return `${fieldName} is required and must be a string`;
  }
  if (!token.startsWith(TAILSCALE_TOKEN_PREFIX)) {
    return `${fieldName} must start with ${TAILSCALE_TOKEN_PREFIX}`;
  }
  if (token.length > TAILSCALE_TOKEN_MAX_LEN) {
    return `${fieldName} exceeds maximum length of ${TAILSCALE_TOKEN_MAX_LEN} characters`;
  }
  return null;
}

function _validateTags(tags) {
  if (tags === undefined || tags === null) return null;
  if (!Array.isArray(tags)) {
    return 'tags must be an array of strings';
  }
  if (tags.length > TAGS_MAX_LEN) {
    return `tags exceeds maximum length of ${TAGS_MAX_LEN} entries`;
  }
  for (let i = 0; i < tags.length; i += 1) {
    const t = tags[i];
    if (typeof t !== 'string' || !t) {
      return `tags[${i}] must be a non-empty string`;
    }
    if (t.length > TAG_KEY_MAX_LEN) {
      return `tags[${i}] exceeds maximum length of ${TAG_KEY_MAX_LEN} characters`;
    }
    if (!TAG_KEY_RE.test(t)) {
      return `tags[${i}] must match ${TAG_KEY_RE} (lowercase alnum + :_-)`;
    }
  }
  return null;
}

function _validateDescription(description) {
  if (description === undefined || description === null) return null;
  if (typeof description !== 'string') {
    return 'description must be a string';
  }
  if (description.length > DESCRIPTION_MAX_LEN) {
    return `description exceeds maximum length of ${DESCRIPTION_MAX_LEN} characters`;
  }
  return null;
}

// Exported for direct unit testing in __tests__/routes/tailscale-admin.test.js
// (the validator functions are otherwise unreachable from outside the factory
// closure; direct tests assert edge cases without supertest overhead).
const _validators = {
  validateApiToken: _validateApiToken,
  validateTags: _validateTags,
  validateDescription: _validateDescription,
  TAILSCALE_TOKEN_PREFIX,
  TAILSCALE_TOKEN_MAX_LEN,
  TAG_KEY_MAX_LEN,
  TAGS_MAX_LEN,
  DESCRIPTION_MAX_LEN,
  TAG_KEY_RE,
};

module.exports = function({
  tailscaleCoord,
  asyncHandler,
  log,
  logError: _logError,
}) {
  const router = express.Router();

  // ---------- Settings ----------

  router.get('/settings', asyncHandler(
    // eslint-disable-next-line require-await
    async (req, res) => {
    const meta = tailscaleCoord.loadMetadata();
    if (!meta.configured) {
      return ok(res, { configured: false });
    }
    return ok(res, {
      configured: true,
      tailnetName: meta.tailnetName || null,
      deviceCount: typeof meta.deviceCount === 'number' ? meta.deviceCount : null,
      keyValidatedAt: meta.keyValidatedAt || null,
      lastUsedAt: meta.lastUsedAt || null,
    });
  }));

  router.put('/settings', asyncHandler(async (req, res) => {
    const token = req.body && req.body.apiToken;
    // DC-080: validate prefix + length cap. The pre-fix code only checked
    // the prefix — a 1 MB string starting with `tskey-api-` would have been
    // sent to Tailscale's /devices endpoint and wasted server-side CPU
    // before the inevitable 401.
    const tokenErr = _validateApiToken(token);
    if (tokenErr) return errorResponse(res, 400, tokenErr);

    // Validate before storing
    const client = new (require('../src/managers/tailscale-coord').TailscaleCoordClient)({ apiToken: token });
    let prefs;
    try {
      prefs = await client.ping();
    } catch (e) {
      if (e instanceof TailscaleCoordError) {
        if (e.code === 'unauthorized') {
          return errorResponse(res, 401, 'Tailscale rejected this API token (401 unauthorized)');
        }
        return errorResponse(res, 502, 'Tailscale API error: ' + e.message);
      }
      throw e;
    }

    // Get device count for the metadata
    let deviceCount = null;
    try {
      const devs = await client.listDevices();
      deviceCount = devs.length;
    } catch (_e) { /* non-fatal */ }

    // Persist token (encrypted) + metadata (plaintext)
    await tailscaleCoord.setApiToken(token);
    tailscaleCoord.saveMetadata({
      configured: true,
      tailnetName: prefs.domain || null,
      deviceCount,
      keyValidatedAt: new Date().toISOString(),
      lastUsedAt: new Date().toISOString(),
    });

    if (log && log.info) log.info('tailscale-coord', 'API token configured', { tailnetName: prefs.domain, deviceCount });

    return ok(res, {
      configured: true,
      tailnetName: prefs.domain || null,
      deviceCount,
      keyValidatedAt: new Date().toISOString(),
    });
  }));

  router.delete('/settings', asyncHandler(async (req, res) => {
    await tailscaleCoord.setApiToken(null);
    tailscaleCoord.saveMetadata({ configured: false });
    if (log && log.info) log.info('tailscale-coord', 'API token cleared');
    return ok(res, { configured: false });
  }));

  router.post('/settings/test', asyncHandler(async (req, res) => {
    const token = (req.body && req.body.apiToken) || null;
    // DC-080: validate any caller-provided token before it reaches the
    // Tailscale API. Pre-fix the test endpoint accepted any string — the
    // PUT route's prefix check did NOT extend to this path. An operator
    // could submit arbitrary junk and the container would still call
    // /devices on the Tailscale API with it (DoS-reflection + fingerprint
    // timing for a future attacker probing whether this API token format
    // is accepted at all).
    if (token !== null && token !== undefined) {
      const tokenErr = _validateApiToken(token);
      if (tokenErr) return errorResponse(res, 400, tokenErr);
    }
    const client = await tailscaleCoord.getClient();
    if (token) {
      // Caller provided a fresh token to test — don't save it
      client.setApiToken(token);
    }
    if (!client.isConfigured()) {
      return ok(res, { valid: false, error: 'No Tailscale API token configured' });
    }
    try {
      const prefs = await client.ping({ skipCache: true });
      return ok(res, { valid: true, tailnetName: prefs.domain });
    } catch (e) {
      if (e instanceof TailscaleCoordError && e.code === 'unauthorized') {
        return ok(res, { valid: false, error: 'Tailscale rejected the token (unauthorized)' });
      }
      return ok(res, { valid: false, error: e.message });
    }
  }));

  // ---------- Admin: devices ----------

  router.get('/admin/devices', asyncHandler(async (req, res) => {
    const client = await tailscaleCoord.getClient();
    if (!client.isConfigured()) {
      return errorResponse(res, 503, 'Tailscale API token not configured');
    }
    try {
      const devices = await client.listDevices();
      return ok(res, { devices, count: devices.length });
    } catch (e) {
      if (e instanceof TailscaleCoordError && e.code === 'unauthorized') {
        return errorResponse(res, 401, 'Tailscale rejected the configured token');
      }
      throw e;
    }
  }));

  router.delete('/admin/devices/:id', asyncHandler(async (req, res) => {
    const client = await tailscaleCoord.getClient();
    if (!client.isConfigured()) {
      return errorResponse(res, 503, 'Tailscale API token not configured');
    }
    const id = req.params.id;
    try {
      await client.deleteDevice(id);
      if (log && log.info) log.info('tailscale-coord', 'Device deleted', { deviceId: id });
      return ok(res, { success: true, deviceId: id });
    } catch (e) {
      if (e instanceof TailscaleCoordError) {
        if (e.code === 'unauthorized') return errorResponse(res, 401, 'Unauthorized');
        if (e.code === 'not_found') return errorResponse(res, 404, 'Device not found');
      }
      throw e;
    }
  }));

  // ---------- Admin: users ----------

  router.get('/admin/users', asyncHandler(async (req, res) => {
    const client = await tailscaleCoord.getClient();
    if (!client.isConfigured()) {
      return errorResponse(res, 503, 'Tailscale API token not configured');
    }
    const users = await client.listUsers();
    return ok(res, { users, count: users.length });
  }));

  // ---------- Admin: pre-auth keys ----------

  router.get('/admin/keys', asyncHandler(async (req, res) => {
    const client = await tailscaleCoord.getClient();
    if (!client.isConfigured()) {
      return errorResponse(res, 503, 'Tailscale API token not configured');
    }
    const keys = await client.listAuthKeys();
    return ok(res, { keys, count: keys.length });
  }));

  router.post('/admin/keys', asyncHandler(async (req, res) => {
    const client = await tailscaleCoord.getClient();
    if (!client.isConfigured()) {
      return errorResponse(res, 503, 'Tailscale API token not configured');
    }
    const opts = req.body || {};
    // Reject obviously-bad input early.
    // DC-080: pre-fix the route only checked `Array.isArray(opts.tags)`.
    // A `tags: ['tag:guest', null, 123, {injection: true}]` payload would
    // be forwarded to Tailscale verbatim — Tailscale's API is JSON-strict
    // and would 400 the request, but the bad shape reached the wire and
    // would silently pass through the dashboard's JSON.stringify() flow.
    const tagsErr = _validateTags(opts.tags);
    if (tagsErr) return errorResponse(res, 400, tagsErr);
    const descErr = _validateDescription(opts.description);
    if (descErr) return errorResponse(res, 400, descErr);
    if (opts.expirySeconds !== undefined && (!Number.isInteger(opts.expirySeconds) || opts.expirySeconds <= 0)) {
      return errorResponse(res, 400, 'expirySeconds must be a positive integer');
    }
    try {
      const result = await client.createAuthKey(opts);
      if (log && log.info) log.info('tailscale-coord', 'Auth key created', { id: result.id, description: opts.description, tags: opts.tags });
      return ok(res, result);
    } catch (e) {
      if (e instanceof TailscaleCoordError) {
        if (e.code === 'unauthorized') return errorResponse(res, 401, 'Unauthorized');
        return errorResponse(res, 502, 'Tailscale API error: ' + e.message);
      }
      throw e;
    }
  }));

  router.delete('/admin/keys/:id', asyncHandler(async (req, res) => {
    const client = await tailscaleCoord.getClient();
    if (!client.isConfigured()) {
      return errorResponse(res, 503, 'Tailscale API token not configured');
    }
    const id = req.params.id;
    try {
      await client.deleteAuthKey(id);
      if (log && log.info) log.info('tailscale-coord', 'Auth key deleted', { keyId: id });
      return ok(res, { success: true, keyId: id });
    } catch (e) {
      if (e instanceof TailscaleCoordError) {
        if (e.code === 'unauthorized') return errorResponse(res, 401, 'Unauthorized');
        if (e.code === 'not_found') return errorResponse(res, 404, 'Key not found');
      }
      throw e;
    }
  }));

  return router;
};

// DC-080: validators exported for direct unit testing in
// __tests__/routes/tailscale-admin.test.js — the route factory closes
// over the same functions, so the validators are exercised end-to-end via
// supertest AND in isolation here.
module.exports._validators = _validators;