/**
 * db/installer-smoke.js — installer_smoke_runs + installer_smoke_tokens persistence.
 *
 * Owns: installer_smoke_runs (read/write), installer_smoke_tokens (read/write).
 * Does NOT own: triggering smoke tests, Docker orchestration, HTTP polling
 *               (those live in scripts/smoke-test-installer.sh and routes/admin-agents.js).
 */

'use strict';

const pool = require('./index');
const crypto = require('crypto');

// ── Tokens ────────────────────────────────────────────────────────────────────

/**
 * Issue a one-shot 15-min smoke install token.
 * Returns { token, expiresAt }.
 */
async function issueToken({ createdBy = null } = {}) {
  const token = crypto.randomBytes(24).toString('hex');
  await pool.query(
    `INSERT INTO installer_smoke_tokens (token, created_by)
     VALUES (:p1, :p2)`,
    { p1: token, p2: createdBy }
  );
  const { rows } = await pool.query(
    `SELECT token, expires_at FROM installer_smoke_tokens WHERE token = :p1`,
    { p1: token }
  );
  return { token: rows[0].token, expiresAt: rows[0].expires_at };
}

/**
 * Validate + redeem a smoke token. Returns the token row or null on invalid/expired.
 * Marks used_at = NOW() and records run_id so each token is truly one-shot.
 */
async function redeemToken(token, runId) {
  const res = await pool.query(
    `UPDATE installer_smoke_tokens
     SET used_at = CURRENT_TIMESTAMP, run_id = :p2
     WHERE token = :p1
       AND used_at IS NULL
       AND expires_at > CURRENT_TIMESTAMP`,
    { p1: token, p2: runId }
  );
  if (!res.rowCount) return null;
  const { rows } = await pool.query(
    `SELECT * FROM installer_smoke_tokens WHERE token = :p1`,
    { p1: token }
  );
  return rows[0] || null;
}

/**
 * Delete expired/used tokens older than 2h (called by purge cron or startup).
 */
async function purgeStaleTokens() {
  await pool.query(`
    DELETE FROM installer_smoke_tokens
    WHERE expires_at < CURRENT_TIMESTAMP - INTERVAL '2' HOUR
  `);
}

// ── Runs ──────────────────────────────────────────────────────────────────────

/**
 * Insert a pending smoke run row. Returns the created row.
 * @param {object} p
 * @param {string} p.run_id  - UUID shared across all OS containers in one orchestrator run
 * @param {string} p.os      - 'ubuntu22' | 'ol8'
 * @param {string} [p.trigger_source] - 'manual' | 'github_actions' | 'post_deploy'
 */
async function insertRun({ run_id, os, trigger_source = 'manual' }) {
  const ins = await pool.query(
    `INSERT INTO installer_smoke_runs (run_id, os, trigger_source)
     VALUES (:p1, :p2, :p3)
     RETURNING id INTO :out_id`,
    { p1: run_id, p2: os, p3: trigger_source, out_id: pool.outNumber() }
  );
  const { rows } = await pool.query(
    `SELECT * FROM installer_smoke_runs WHERE id = :p1`,
    { p1: ins.outBinds.out_id[0] }
  );
  return rows[0];
}

/**
 * Update a run row with step results and overall outcome.
 * @param {number} id
 * @param {object} data - any subset of run columns
 */
async function updateRun(id, data) {
  const allowed = [
    'finished_at', 'overall',
    'step_install_ms', 'step_install_ok', 'step_install_err',
    'step_register_ms', 'step_register_ok', 'step_register_err',
    'step_heartbeat_ms', 'step_heartbeat_ok', 'step_heartbeat_err',
    'step_systemd_ms', 'step_systemd_ok', 'step_systemd_err',
    'step_command_ms', 'step_command_ok', 'step_command_err',
    'failure_log', 'results_json', 'agent_version', 'install_sha',
    'duration_total_ms',
  ];

  const BOOL = new Set([
    'step_install_ok', 'step_register_ok', 'step_heartbeat_ok',
    'step_systemd_ok', 'step_command_ok',
  ]);

  const updates = [];
  const binds   = {};
  let   idx     = 1;

  for (const key of allowed) {
    if (key in data) {
      const p = `p${idx++}`;
      updates.push(`${key} = :${p}`);
      let v = data[key];
      if (BOOL.has(key)) v = pool.bool(v);
      else if (key === 'results_json') v = v == null ? null : JSON.stringify(v);
      else if (key === 'failure_log') v = pool.clob(v);
      binds[p] = v;
    }
  }

  if (updates.length === 0) return null;

  binds.p_id = id;
  const res = await pool.query(
    `UPDATE installer_smoke_runs
     SET ${updates.join(', ')}
     WHERE id = :p_id`,
    binds
  );
  if (!res.rowCount) return null;
  const { rows } = await pool.query(
    `SELECT * FROM installer_smoke_runs WHERE id = :p_id`,
    { p_id: id }
  );
  return rows[0] || null;
}

/**
 * Get the last N smoke runs, newest first. Used by the Installer Health card.
 * @param {number} [limit=20]
 */
async function getRecentRuns(limit = 20) {
  const { rows } = await pool.query(
    `SELECT id, run_id, os, trigger_source, started_at, finished_at,
            overall, duration_total_ms,
            step_install_ok, step_register_ok, step_heartbeat_ok,
            step_systemd_ok, step_command_ok,
            step_install_err, step_register_err, step_heartbeat_err,
            step_systemd_err, step_command_err,
            agent_version, install_sha
     FROM installer_smoke_runs
     WHERE overall != 'pending'
     ORDER BY started_at DESC
     FETCH FIRST :p1 ROWS ONLY`,
    { p1: limit }
  );
  return rows;
}

/**
 * Find a pending run by id (used by the report-back endpoint).
 */
async function getRunById(id) {
  const { rows } = await pool.query(
    `SELECT * FROM installer_smoke_runs WHERE id = :p1`,
    { p1: id }
  );
  return rows[0] || null;
}

/**
 * Mark stale pending runs (started > 15 min ago) as error. Called at startup.
 */
async function markStaleRunsAsError() {
  await pool.query(`
    UPDATE installer_smoke_runs
    SET overall = 'error',
        step_install_err = 'Run timed out — no result received',
        finished_at = CURRENT_TIMESTAMP
    WHERE overall = 'pending'
      AND started_at < CURRENT_TIMESTAMP - INTERVAL '15' MINUTE
  `);
}

module.exports = {
  issueToken,
  redeemToken,
  purgeStaleTokens,
  insertRun,
  updateRun,
  getRecentRuns,
  getRunById,
  markStaleRunsAsError,
};
