/**
 * services/adop-gate.js — the ADOP refusal, shared by every op route.
 *
 * Owns: deciding whether a specific op on a specific connection must be refused
 *       right now because an ADOP patch cycle is in flight, and what to say.
 * Does NOT own: the gate rule (lib/ebs/adop-state.js isCatalogOpBlockedDuringAdop),
 *               reading/persisting state (db/ebs-adop-state.js), or sending the
 *               response — callers render it.
 *
 * Why this exists: four routes need this and each had (or would have grown) its
 * own copy. Four copies of a safety check drift, and drift is the whole reason
 * the gate sat unenforced for so long. One implementation, four call sites:
 *   routes/ebs-ops.js        POST /api/ebs-ops/middleware-run
 *   routes/db-ops.js         POST /api/db-ops/run
 *   routes/ebs-middleware.js POST /api/ebs-middleware/run + /rolling-bounce
 *   routes/ebs-concurrent.js POST /api/ebs-concurrent/run
 *
 * Returns an object rather than sending one, because callers differ: most reply
 * with 409 JSON, but rolling-bounce must refuse BEFORE it opens its SSE stream.
 */

'use strict';

const adopDb = require('../db/ebs-adop-state');
const { isCatalogOpBlockedDuringAdop } = require('../lib/ebs/adop-state');

/**
 * @param {number|string} connectionId
 * @param {string} opKey                  e.g. 'adcmctl_start', 'ebs.allnodes.stop'
 * @param {{destructive?:boolean}} opDef  the op's catalog entry — the flag is the rule
 * @param {string} [adopSessionId]        override: the live ADOP session id
 * @param {string} [logTag]               log prefix, e.g. 'db-ops'
 * @returns {Promise<object|null>}  409 body when the op must be refused; null to proceed.
 */
async function adopRefusal(connectionId, opKey, opDef, adopSessionId, logTag = 'adop-gate') {
  if (!isCatalogOpBlockedDuringAdop(opKey, opDef)) return null;

  let state = null;
  try {
    state = await adopDb.getAdopPatchingFlag(parseInt(connectionId, 10));
  } catch (err) {
    // Fails open, deliberately. A DB blip must not freeze every destructive op
    // across the product; the red banner surfaces an active cycle independently.
    console.error(`[${logTag}/adop-gate] getAdopPatchingFlag error:`, err.message);
    return null;
  }

  // No row = not an EBS connection (the detector skips non-EBS), or never checked.
  if (!state || !state.patching) return null;

  if (adopSessionId && state.session_id && String(adopSessionId) === String(state.session_id)) {
    console.log('[%s/adop-gate] override accepted op=%s conn=%s session=%s',
      logTag, opKey, connectionId, state.session_id);
    return null;
  }

  console.log('[%s/adop-gate] blocked op=%s conn=%s phase=%s',
    logTag, opKey, connectionId, state.phase || 'unknown');

  return {
    error: `Blocked during ADOP patch cycle (phase: ${state.phase || 'unknown'}). Finalize the patch session first.`,
    adop_blocked:      true,
    adop_phase:        state.phase || null,
    override_required: true,
    override_hint:     state.session_id
      ? `To override, re-send with adop_session_id: ${state.session_id}`
      : 'Override requires the ADOP session ID.',
  };
}

module.exports = { adopRefusal };
