'use strict';

//
// AutoPilot — API for the global /autopilot page (connection chosen via dropdown).
//   GET  /api/autopilot/:connectionId/status    — schedule + remediation + log
//   POST /api/autopilot/:connectionId/save       — persist settings (tier-gated)
//   GET  /api/autopilot/:connectionId/log        — paginated activity log
//
// The page routes themselves (GET /autopilot serve + GET /autopilot/:connId 302
// redirect) live in server.js. API param is :connectionId (NOT :connId) because
// requireConnectionOwner reads req.params.id || req.params.connectionId. The
// router uses ABSOLUTE paths, so server.js mounts it ONCE at '/'.
//

const express = require('express');
const pool    = require('../db/index');
const channel = require('../services/agent-channel');
const { decrypt } = require('../crypto-utils');
const { requireAuth, requireConnectionOwner, ADMIN_EMAILS } = require('../middleware/auth');
const { HEARTBEAT_SERVICES, RESTARTABLE_OPS } = require('../services/auto-remediation');

const router = express.Router();

// Auto-remediation is Business+ (Business, Enterprise, Managed Services).
const REMEDIATION_TIERS = ['business', 'enterprise', 'managed_services'];

// req.user has no is_admin field (requireAuth attaches id/email/name/... only);
// admin in this codebase is ADMIN_EMAILS membership, same check requireAdmin and
// the other routes use. This is the real admin bypass for the tier gate.
function isAdmin(req) {
  return ADMIN_EMAILS.has(((req.user && req.user.email) || '').toLowerCase());
}

async function userTier(userId) {
  const { rows } = await pool.query(
    `SELECT plan_tier FROM user_credits WHERE user_id = $1`, [userId]
  );
  return (rows[0] && rows[0].plan_tier) || 'free';
}

// ── server_type scoping of heartbeat rows ────────────────────────────────────
// A db-tier connection must never surface EBS app-tier services and vice-versa,
// even though the heartbeat currently polls every op for every connection.
// (managed-server rows are the discovered names oacore_*/forms_*/oafm_*.)
const EBS_OP_PREFIXES = ['adapcctl', 'adopmnctl', 'adalnctl', 'adnodemgrctl', 'wls_admin', 'oacore', 'forms', 'oafm'];
const DB_OP_PREFIXES  = ['db_listener', 'db_startup'];
function startsWithAny(key, prefixes) {
  const k = String(key || '').toLowerCase();
  return prefixes.some(p => k.startsWith(p));
}
function scopeHeartbeatRows(rows, serverType) {
  if (serverType === 'db')   return rows.filter(r => !startsWithAny(r.op_key, EBS_OP_PREFIXES));
  if (serverType === 'apps') return rows.filter(r => !startsWithAny(r.op_key, DB_OP_PREFIXES));
  return rows; // 'both' (or unknown) → all rows
}

// ── GET status ───────────────────────────────────────────────────────────────
router.get('/api/autopilot/:connectionId/status', requireAuth, requireConnectionOwner, async (req, res) => {
  const connId = parseInt(req.params.connectionId, 10);
  try {
    const [schedRow, connRow, recentLog, heartbeatState, tier] = await Promise.all([
      pool.query(
        `SELECT enabled, cadence_minutes, alert_email, severity_threshold,
                auto_create_tickets, snoozed_until, next_run_at, last_run_at,
                auto_remediate, remediate_threshold, remediate_services
         FROM connection_schedules WHERE connection_id = $1`,
        [connId]
      ),
      pool.query(
        `SELECT id, name, server_type, connection_type, apps_pwd_enc, weblogic_pwd_enc, discovered_services
         FROM oracle_connections WHERE id = $1`,
        [connId]
      ),
      pool.query(
        `SELECT trigger_path, triggered_by, consecutive_count, op_key, op_label,
                outcome, exit_code, stdout_tail, tuneops_ticket_id, executed_at
         FROM auto_remediation_log
         WHERE connection_id = $1
         ORDER BY executed_at DESC LIMIT 20`,
        [connId]
      ),
      pool.query(
        `SELECT op_key, consecutive_down, last_checked_at, last_status
         FROM service_heartbeat_state WHERE connection_id = $1`,
        [connId]
      ),
      userTier(req.user.id),
    ]);

    const sched = schedRow.rows[0] || {};
    const conn  = connRow.rows[0];
    if (!conn) return res.status(404).json({ error: 'Connection not found' });

    // Latest completed health check score (status value is 'completed', not 'complete').
    const { rows: hcRows } = await pool.query(
      `SELECT overall_score, completed_at FROM health_checks
       WHERE connection_id = $1 AND status = 'completed'
       ORDER BY completed_at DESC NULLS LAST LIMIT 1`,
      [connId]
    );

    res.json({
      connection: { id: conn.id, name: conn.name, server_type: conn.server_type },
      // Credential presence — drives the "password not configured" warnings on the
      // managed-server (WebLogic) and Concurrent Manager (APPS) toggles.
      has_apps_pwd: !!(conn.apps_pwd_enc && conn.apps_pwd_enc !== ''),
      has_weblogic_pwd: !!(conn.weblogic_pwd_enc && conn.weblogic_pwd_enc !== ''),
      // Discovered services (from GET /discover); null = never run, array = probed set.
      discovered_services: Array.isArray(conn.discovered_services) ? conn.discovered_services : null,
      tier_ok: isAdmin(req) || REMEDIATION_TIERS.includes(tier),
      schedule: {
        enabled: !!sched.enabled,
        cadence_minutes: sched.cadence_minutes || 60,
        alert_email: sched.alert_email || '',
        severity_threshold: sched.severity_threshold || 'amber',
        auto_create_tickets: sched.auto_create_tickets !== false,
        snoozed_until: sched.snoozed_until || null,
        // next_run_at is maintained on connection_schedules by the scheduler
        // (advanceSchedule) and set on enable by the save route below. last_run_at
        // is the last autonomous run; fall back to the latest completed health
        // check so it aligns with the score shown and isn't blank after a manual run.
        next_run_at: sched.next_run_at || null,
        last_run_at: sched.last_run_at || (hcRows[0] && hcRows[0].completed_at) || null,
        last_score: (hcRows[0] && hcRows[0].overall_score != null) ? hcRows[0].overall_score : null,
      },
      remediation: {
        enabled: !!sched.auto_remediate,
        threshold: sched.remediate_threshold || 3,
        // Per-service opt-in list (JSONB array of start op_keys); [] when unset.
        services: Array.isArray(sched.remediate_services) ? sched.remediate_services : [],
      },
      heartbeat_state: scopeHeartbeatRows(heartbeatState.rows, conn.server_type),
      recent_activity: recentLog.rows,
      supported_services: HEARTBEAT_SERVICES.map(s => ({ statusOp: s.statusOp, label: s.label, scope: s.scope })),
    });
  } catch (err) {
    console.error('[autopilot/status]', err.message);
    res.status(500).json({ error: 'Failed to load AutoPilot status' });
  }
});

// ── GET discover ─────────────────────────────────────────────────────────────
// Probe which auto-restartable services are actually configured on the server via
// the agent, cache the result on oracle_connections.discovered_services, and return
// it. Discovery only shapes the UI checklist — never HEARTBEAT_SERVICES/RESTARTABLE_OPS.
//   apps/both: adapcctl/adopmnctl/adalnctl/adnodemgrctl/adcmctl (present when
//              body.success===true) + oacore/forms/oafm (present when success AND the
//              stdout is not TV_GROUP_DISABLED / NO_SERVERS_FOUND).
//   db/both:   db_listener (present when success).
const DISCOVERY_PROBES = {
  apps: [
    { statusOp: 'adapcctl_status',     op: 'adapcctl_start'     },
    { statusOp: 'adopmnctl_status',    op: 'adopmnctl_start'    },
    { statusOp: 'adalnctl_status',     op: 'adalnctl_start'     },
    { statusOp: 'adnodemgrctl_status', op: 'adnodemgrctl_start' },
    { statusOp: 'adcmctl_status',      op: 'adcmctl_start'      },
    { statusOp: 'oacore_status',       op: 'oacore', group: true },
    { statusOp: 'forms_status',        op: 'forms',  group: true },
    { statusOp: 'oafm_status',         op: 'oafm',   group: true },
    // wls_admin_status is a slow check → longer probe timeout so it isn't false-negatived.
    { statusOp: 'wls_admin_status',    op: 'wls_admin_start', timeout: 75000 },
  ],
  db: [
    { statusOp: 'db_listener_status',  op: 'db_listener_start' },
  ],
};

async function probeService(connectionId, statusOp, appsPwd, weblogicPwd, timeoutMs) {
  try {
    const r = await channel.sendToAgent(connectionId, {
      method: 'POST', path: '/api/ebs-ctrl',
      body: { op: statusOp, apps_pwd: appsPwd, weblogic_pwd: weblogicPwd },
    }, timeoutMs || 20000);
    const body = (r && r.body) || {};
    return { ok: !!(r && r.statusCode === 200 && body.success === true), stdout: body.stdout || '' };
  } catch (e) {
    return { ok: false, stdout: '' };
  }
}

router.get('/api/autopilot/:connectionId/discover', requireAuth, requireConnectionOwner, async (req, res) => {
  const connId = parseInt(req.params.connectionId, 10);
  try {
    const online = await channel.isAgentConnected(connId).catch(() => false);
    if (!online) return res.json({ discovered: null, reason: 'agent_offline' });

    const { rows } = await pool.query(
      `SELECT apps_pwd_enc, weblogic_pwd_enc, server_type FROM oracle_connections WHERE id = $1`,
      [connId]
    );
    const conn = rows[0];
    if (!conn) return res.status(404).json({ error: 'Connection not found' });

    const appsPwd     = conn.apps_pwd_enc     ? decrypt(conn.apps_pwd_enc)     : '';
    const weblogicPwd = conn.weblogic_pwd_enc ? decrypt(conn.weblogic_pwd_enc) : '';
    const st = conn.server_type;

    let probes = [];
    if (st === 'apps' || st === 'both') probes = probes.concat(DISCOVERY_PROBES.apps);
    if (st === 'db'   || st === 'both') probes = probes.concat(DISCOVERY_PROBES.db);

    // Sequential (NOT parallel): the agent can't reliably service many simultaneous
    // ebs-ctrl requests — some time out and get wrongly marked "not present". One at
    // a time (20s each, 75s for the slow wls_admin_status) is slower but correct.
    // probeService never throws.
    const discovered = [];
    for (const p of probes) {
      const r = await probeService(connId, p.statusOp, appsPwd, weblogicPwd, p.timeout);
      let present = r.ok;
      // For grouped managed servers, a disabled/absent group answers success but says so.
      if (p.group) present = r.ok && r.stdout.indexOf('TV_GROUP_DISABLED') === -1 && r.stdout.indexOf('NO_SERVERS_FOUND') === -1;
      if (present) discovered.push(p.op);
    }

    // DB Instance Startup has no independent status op (it uses the listener as its
    // signal) but always applies to a db-tier node — offer it so the checkbox appears.
    // The two-step gate (listener down + SQL probe) in the engine is the safety.
    if (st === 'db' && discovered.indexOf('db_startup_open') === -1) discovered.push('db_startup_open');

    await pool.query(
      `UPDATE oracle_connections SET discovered_services = $1::jsonb WHERE id = $2`,
      [JSON.stringify(discovered), connId]
    );

    res.json({ discovered, cached: false });
  } catch (err) {
    console.error('[autopilot/discover]', err.message);
    res.status(500).json({ error: 'Discovery failed' });
  }
});

// ── POST save ────────────────────────────────────────────────────────────────
router.post('/api/autopilot/:connectionId/save', requireAuth, requireConnectionOwner, async (req, res) => {
  const connId = parseInt(req.params.connectionId, 10);
  const {
    schedule_enabled, cadence_minutes, alert_email, severity_threshold,
    auto_create_tickets, auto_remediate, remediate_threshold, remediate_services,
  } = req.body;

  // Tier gate — only enabling auto_remediate is gated; turning it off is always
  // allowed, and admins (ADMIN_EMAILS) bypass the tier requirement entirely.
  if (auto_remediate && !isAdmin(req)) {
    const tier = await userTier(req.user.id);
    if (!REMEDIATION_TIERS.includes(tier)) {
      return res.status(402).json({ error: 'Auto-remediation requires Business plan or higher' });
    }
  }

  const VALID_CADENCES = [5, 15, 30, 60, 360, 720, 1440];
  const safeCadence   = VALID_CADENCES.includes(parseInt(cadence_minutes, 10)) ? parseInt(cadence_minutes, 10) : 60;
  const safeThreshold = Math.max(2, Math.min(10, parseInt(remediate_threshold, 10) || 3));

  // remediate_services: JSONB array of start op_keys, filtered to ops the engine
  // can actually fire (RESTARTABLE_OPS) so junk / non-actionable ops (e.g. OACore
  // managed servers) can never be persisted. Deduped. NULL when nothing opted in.
  const safeServices = Array.isArray(remediate_services)
    ? [...new Set(remediate_services.filter(op => RESTARTABLE_OPS.has(op)))]
    : [];
  const servicesJson = safeServices.length ? JSON.stringify(safeServices) : null;

  // Set next_run_at when enabling so getDueSchedules (WHERE next_run_at <= NOW())
  // actually picks the schedule up — omitting it left it NULL and the scheduler
  // never fired. Mirrors db/schedules.js upsertSchedule: NOW()+cadence when
  // enabled, NULL when disabled.
  const nextRunAt = schedule_enabled ? new Date(Date.now() + safeCadence * 60 * 1000) : null;

  try {
    await pool.query(
      `INSERT INTO connection_schedules
         (connection_id, user_id, enabled, cadence_minutes, alert_email,
          severity_threshold, auto_create_tickets, auto_remediate,
          remediate_threshold, remediate_services, next_run_at)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11)
       ON CONFLICT (connection_id) DO UPDATE SET
         enabled = $3, cadence_minutes = $4, alert_email = $5,
         severity_threshold = $6, auto_create_tickets = $7,
         auto_remediate = $8, remediate_threshold = $9, remediate_services = $10::jsonb,
         next_run_at = CASE WHEN $3 THEN $11 ELSE NULL END,
         updated_at = NOW()`,
      [
        connId, req.user.id, !!schedule_enabled, safeCadence,
        alert_email || '', severity_threshold || 'amber',
        auto_create_tickets !== false, !!auto_remediate,
        safeThreshold, servicesJson, nextRunAt,
      ]
    );
    res.json({ ok: true });
  } catch (err) {
    console.error('[autopilot/save]', err.message);
    res.status(500).json({ error: 'Failed to save AutoPilot settings' });
  }
});

// ── GET log (paginated) ──────────────────────────────────────────────────────
router.get('/api/autopilot/:connectionId/log', requireAuth, requireConnectionOwner, async (req, res) => {
  const connId = parseInt(req.params.connectionId, 10);
  const limit  = Math.min(parseInt(req.query.limit, 10) || 50, 200);
  const offset = parseInt(req.query.offset, 10) || 0;
  try {
    const { rows } = await pool.query(
      `SELECT trigger_path, triggered_by, consecutive_count, op_key, op_label,
              outcome, exit_code, stdout_tail, tuneops_ticket_id, executed_at
       FROM auto_remediation_log
       WHERE connection_id = $1
       ORDER BY executed_at DESC LIMIT $2 OFFSET $3`,
      [connId, limit, offset]
    );
    res.json({ log: rows });
  } catch (err) {
    console.error('[autopilot/log]', err.message);
    res.status(500).json({ error: 'Failed to load log' });
  }
});

module.exports = router;
