/**
 * routes/agent-config.js — remote edit of ONE allowlisted agent.env key.
 *
 * Owns: PATCH /api/connections/:id/agent-config  { key, value }
 * Does NOT own: reading the config (it arrives on the agent poll and is stored on
 *               oracle_connections.agent_config by routes/agent.js), credentials
 *               (ebs_credentials / rotate-key), or OS commands (routes/os-exec.js).
 *
 * This is the first cloud->agent WRITE primitive in TuneVault: everything else the
 * cloud can ask an agent to do is a read or a scoped service control. Three
 * consequences drive the design:
 *
 *  1. THE AGENT IS THE SECURITY BOUNDARY, not this file. oracle-proxy.py keeps its
 *     own allowlist and its own validation and will refuse anything outside them.
 *     The allowlist here is defence in depth and a better error message — never
 *     the thing standing between a caller and agent.env.
 *  2. SECRETS ARE NOT SETTABLE. TUNEVAULT_API_KEY / APPS_PWD / WEBLOGIC_PWD have
 *     their own paths; a generic config writer must not become a second way to set
 *     a credential. Rejected here AND by the agent.
 *  3. A BAD VALUE CAN TAKE THE AGENT OFFLINE, and the only channel to fix it is the
 *     agent you just broke. The agent validates before writing and keeps a .bak
 *     (`tunevault-agent rollback-config`), but the residual risk is real and is why
 *     this needs senior_dba rather than any authenticated user.
 */

'use strict';

const express = require('express');
const pool    = require('../db/index');
const channel = require('../services/agent-channel');
const { requireAuth, requireConnectionOwner, requireRole } = require('../middleware/auth');

const router = express.Router();

// Mirrors _CONFIG_SETTABLE_KEYS in oracle-proxy.py. If these ever disagree the
// agent wins, and the caller gets the agent's 403 instead of ours.
const SETTABLE_KEYS = new Set([
  'ORACLE_HOME', 'ORACLE_SID', 'ORACLE_SIDS', 'SERVER_TYPE',
  'APPS_ENV_FILE', 'APPS_USER', 'EBS_DB_HOST', 'INSTANCE_NAME',
]);
const SECRET_KEYS = new Set(['TUNEVAULT_API_KEY', 'APPS_PWD', 'WEBLOGIC_PWD']);

// /api/config/set landed in proxy 3.23.0. Older agents 404 the path, which would
// surface as an opaque failure, so gate on the reported version and say what to do.
const MIN_PROXY_VERSION = '3.23.0';

function versionAtLeast(actual, min) {
  if (!actual) return false;
  const a = String(actual).split('.').map(n => parseInt(n, 10) || 0);
  const b = min.split('.').map(n => parseInt(n, 10) || 0);
  for (let i = 0; i < 3; i++) {
    if ((a[i] || 0) > (b[i] || 0)) return true;
    if ((a[i] || 0) < (b[i] || 0)) return false;
  }
  return true;
}

async function audit(row) {
  try {
    await pool.query(
      `INSERT INTO agent_config_changes
         (connection_id, user_id, config_key, old_value, new_value, applied, error_message)
       VALUES (:p1, :p2, :p3, :p4, :p5, :p6, :p7)`,
      { p1: row.connectionId, p2: row.userId, p3: row.key,
        p4: row.oldValue == null ? null : String(row.oldValue).slice(0, 512),
        p5: row.newValue == null ? null : String(row.newValue).slice(0, 512),
        p6: pool.bool(row.applied), p7: row.error ? String(row.error).slice(0, 1000) : null }
    );
  } catch (e) {
    // Never fail the request because the audit insert failed — but say so loudly.
    console.error('[agent-config] audit insert failed:', e.message);
  }
}

// PATCH /api/connections/:id/agent-config
router.patch('/connections/:id/agent-config',
  requireAuth, requireConnectionOwner, requireRole('senior_dba'),
  async (req, res) => {
    const connectionId = parseInt(req.params.id, 10);
    const key   = String(req.body?.key || '').trim();
    const value = String(req.body?.value ?? '').trim();

    if (!key || !value) {
      return res.status(400).json({ error: 'key and value are required' });
    }
    if (SECRET_KEYS.has(key)) {
      await audit({ connectionId, userId: req.user.id, key, applied: false,
                    error: 'rejected: credential key' });
      return res.status(403).json({
        error: `${key} is a credential and cannot be set here.`,
        hint: key === 'TUNEVAULT_API_KEY'
          ? 'Rotate it from the connection page, or run: tunevault-agent rotate-key <key>'
          : 'Set EBS/WebLogic passwords in Servers → Edit.',
      });
    }
    if (!SETTABLE_KEYS.has(key)) {
      await audit({ connectionId, userId: req.user.id, key, applied: false,
                    error: 'rejected: not allowlisted' });
      return res.status(403).json({
        error: `${key} is not an editable key.`,
        settable: [...SETTABLE_KEYS],
      });
    }
    // Match the agent's own check exactly — CR, LF or NUL. The newline is the
    // dangerous one: it would inject a second assignment into agent.env, which is
    // how one allowlisted key becomes any key. The agent rejects this too; doing it
    // here saves a round trip and keeps the audit row honest. Spaces are legal in a
    // path, so this side never rejects something the agent would have accepted.
    if (/[\r\n\u0000]/.test(value) || value.length > 512) {
      await audit({ connectionId, userId: req.user.id, key, newValue: value, applied: false,
                    error: 'rejected: multi-line or over-long value' });
      return res.status(400).json({ error: 'value must be a single line under 512 characters' });
    }

    try {
      const { rows } = await pool.query(
        `SELECT proxy_version, connection_type FROM oracle_connections WHERE id = :p1`,
        { p1: connectionId }
      );
      const conn = rows[0] || {};
      if (conn.connection_type !== 'proxy') {
        return res.status(400).json({
          error: 'Only agent (proxy) connections have an agent.env to edit.' });
      }
      if (!versionAtLeast(conn.proxy_version, MIN_PROXY_VERSION)) {
        return res.status(409).json({
          error: `Agent upgrade required — remote config editing needs ${MIN_PROXY_VERSION}.`,
          agent_version: conn.proxy_version || 'unknown',
          hint: `Meanwhile, on the server: sudo tunevault-agent set ${key} <value>`,
        });
      }
      if (!(await channel.isAgentConnected(connectionId))) {
        return res.status(503).json({
          error: 'Agent is offline — it must be polling to receive a config change.' });
      }

      const resp = await channel.sendToAgent(
        connectionId, { method: 'POST', path: '/api/config/set', body: { key, value } }, 30_000);
      const body = resp?.body || {};

      if (!body.ok) {
        await audit({ connectionId, userId: req.user.id, key, newValue: value,
                      applied: false, error: body.error || 'agent refused' });
        // Pass the agent's own status through — it is the authority on what it
        // will accept, and its message names the actual reason.
        return res.status(resp?.statusCode && resp.statusCode >= 400 ? resp.statusCode : 400)
          .json({ error: body.error || 'Agent refused the change' });
      }

      await audit({ connectionId, userId: req.user.id, key,
                    oldValue: body.old, newValue: body.new, applied: true });

      // The agent restarts a couple of seconds after replying, so the next poll is
      // the one that reports the new value back into oracle_connections.agent_config.
      res.json({
        ok: true, key, old: body.old, new: body.new,
        restart_scheduled: !!body.restart_scheduled,
        note: body.restart_scheduled
          ? 'Agent is restarting; the new value appears here after its next poll.'
          : 'Saved, but the agent could not schedule a restart — restart it manually.',
      });
    } catch (err) {
      await audit({ connectionId, userId: req.user.id, key, newValue: value,
                    applied: false, error: err.message });
      console.error('[agent-config] PATCH failed:', err.message);
      const timedOut = /timed out/i.test(err.message);
      res.status(timedOut ? 504 : 500).json({
        error: timedOut
          ? 'Agent did not respond in time. The change may or may not have applied — check the Agent Config panel after its next poll.'
          : 'Failed to apply config change',
      });
    }
  });

module.exports = router;
