/**
 * WeeklyGovPushService — Monday 06:00 server-local push of the weekly
 * Ministry-style aggregate report to one or more government endpoints.
 *
 * What it does
 *   Every minute, checks if today is Monday at or after 06:00 server-local
 *   time AND the last successful push was for a previous ISO week. If so,
 *   rebuilds the weekly report via buildWeeklyReport() and POSTs the
 *   payload to every URL in WEEKLY_GOV_PUSH_URLS, with bounded retries
 *   (1s, 4s, 16s backoff). On success or final failure, writes an
 *   audit_logs row and persists the ISO week in system_settings so the
 *   push is idempotent across restarts.
 *
 * Why not a real cron lib
 *   Matches the existing convention in ScheduleAutoPublish.js (single-
 *   process in-memory setInterval with an env-var kill switch). The
 *   comment block there explains the trade-offs in detail — short
 *   version: no restart survival beyond an immediate runOnce() on boot,
 *   which is fine for a daily-or-weekly ministry push.
 *
 * Env knobs
 *   WEEKLY_GOV_PUSH=0                       disable entirely (default: enabled)
 *   WEEKLY_GOV_PUSH_URLS=url1,url2          comma-separated target URLs
 *   WEEKLY_GOV_PUSH_AUTH=Bearer xxx         Authorization header (full header value)
 *   WEEKLY_GOV_PUSH_HMAC_SECRET=...         optional HMAC-SHA256 secret for X-Signature
 *   WEEKLY_GOV_PUSH_TIMEOUT_MS=15000        per-request timeout (default 15000)
 *   WEEKLY_GOV_PUSH_RETRIES=3               max attempts incl. first (default 3)
 *   WEEKLY_GOV_PUSH_INTERVAL_MS=60000       tick interval (default 60s)
 *   WEEKLY_GOV_PUSH_SCHOOL_ID=...           optional school identifier in envelope
 *
 * Idempotency
 *   Reads system_settings row 'weekly_gov_push_last_week'. If it equals
 *   the current ISO week, the push is skipped. On success the row is
 *   updated to the current ISO week.
 *
 * Test mode
 *   runOnce() is exported so callers (or tests) can trigger a single
 *   push without waiting for Monday. By default it skips the idempotency
 *   guard unless `force` is true (then it pushes regardless of last-week).
 *
 * Configuration sources (DB overrides env):
 *   The four runtime values \u2014 urls, auth, hmac_secret, school_id \u2014 are
 *   read from the system_settings table (keys weekly_gov_push_*);
 *   if a key is missing or empty, the corresponding env-var fallback is
 *   used. Results are cached for 60 seconds so the per-tick cron
 *   doesn't hammer the DB. The kill switch (WEEKLY_GOV_PUSH=0) and the
 *   retry/timeout knobs stay env-only — admins shouldn't change cron
 *   behaviour from the UI.
 */

const path = require('path');
const crypto = require('crypto');
const axios = require('axios');

const LAST_WEEK_KEY = 'weekly_gov_push_last_week';

let tickHandle = null;
let tickIntervalMs = 60_000;

const isEnabled = () => process.env.WEEKLY_GOV_PUSH !== '0';

const openDb = () => {
  const Database = require('better-sqlite3');
  const dbPath = process.env.DB_PATH || path.join(__dirname, '../../data/school.db');
  const db = new Database(dbPath);
  db.pragma('foreign_keys = ON');
  return db;
};

// Push config: DB row beats env-var. Cache for 60s so the cron tick
// doesn't re-read the DB on every run.
const CONFIG_TTL_MS = 60_000;
const cachedConfig = { value: null, expiresAt: 0 };

const readPushConfig = () => {
  if (cachedConfig.value && cachedConfig.expiresAt > Date.now()) {
    return cachedConfig.value;
  }
  const db = openDb();
  let out;
  try {
    const rows = db.prepare(
      "SELECT key, value FROM system_settings WHERE key LIKE 'weekly_gov_push_%' AND is_deleted = 0"
    ).all();
    const dbConfig = Object.fromEntries(rows.map((r) => [r.key, r.value]));
    out = {
      urls: dbConfig.weekly_gov_push_urls || process.env.WEEKLY_GOV_PUSH_URLS || '',
      auth: dbConfig.weekly_gov_push_auth || process.env.WEEKLY_GOV_PUSH_AUTH || '',
      hmacSecret: dbConfig.weekly_gov_push_hmac_secret || process.env.WEEKLY_GOV_PUSH_HMAC_SECRET || '',
      schoolId: dbConfig.weekly_gov_push_school_id || process.env.WEEKLY_GOV_PUSH_SCHOOL_ID || '',
    };
  } finally {
    db.close();
  }
  cachedConfig.value = out;
  cachedConfig.expiresAt = Date.now() + CONFIG_TTL_MS;
  return out;
};

// Invalidate the cache (called by the route PUT endpoint after a save).
const invalidatePushConfigCache = () => {
  cachedConfig.value = null;
  cachedConfig.expiresAt = 0;
};

// Back-compat getters — still used by runOnce / pushToEndpoint below.
const getUrls = () =>
  readPushConfig().urls
    .split(',')
    .map((s) => s.trim())
    .filter(Boolean);

const getAuthHeader = () => readPushConfig().auth.trim();

const getHmacSecret = () => readPushConfig().hmacSecret.trim();

const getTimeoutMs = () =>
  parseInt(process.env.WEEKLY_GOV_PUSH_TIMEOUT_MS || '15000', 10) || 15000;

const getMaxRetries = () =>
  parseInt(process.env.WEEKLY_GOV_PUSH_RETRIES || '3', 10) || 3;

const getSchoolId = () => readPushConfig().schoolId.trim() || null;

function readLastPushedWeek(db) {
  try {
    const row = db.prepare("SELECT value FROM system_settings WHERE key = ?").get(LAST_WEEK_KEY);
    return row && row.value ? String(row.value) : null;
  } catch (e) {
    return null;
  }
}

function writeLastPushedWeek(db, iso) {
  try {
    db.prepare(`
      INSERT INTO system_settings (key, value, category, updated_at)
      VALUES (?, ?, 'integration', datetime('now', 'localtime'))
      ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = datetime('now', 'localtime')
    `).run(LAST_WEEK_KEY, iso);
  } catch (e) {
    // system_settings may not have ON CONFLICT support on older schemas —
    // fall back to a manual upsert.
    const existing = db.prepare('SELECT id FROM system_settings WHERE key = ?').get(LAST_WEEK_KEY);
    if (existing) {
      db.prepare('UPDATE system_settings SET value = ?, updated_at = datetime(\'now\', \'localtime\') WHERE key = ?').run(iso, LAST_WEEK_KEY);
    } else {
      db.prepare('INSERT INTO system_settings (key, value, category, created_at, updated_at) VALUES (?, ?, \'integration\', datetime(\'now\', \'localtime\'), datetime(\'now\', \'localtime\'))').run(LAST_WEEK_KEY, iso, 'integration');
    }
  }
}

function signBody(body, secret) {
  if (!secret) return null;
  return 'sha256=' + crypto.createHmac('sha256', secret).update(body).digest('hex');
}

function writeAudit(db, action, url, payload) {
  try {
    db.prepare(`
      INSERT INTO audit_logs (action, entity_type, entity_id, new_data, created_at)
      VALUES (?, 'gov_endpoint', ?, ?, datetime('now', 'localtime'))
    `).run(action, url, JSON.stringify(payload));
  } catch (e) {
    console.warn('[weekly-gov-push] audit write failed:', e.message);
  }
}

async function pushToEndpoint(url, envelope, opts) {
  const body = JSON.stringify(envelope);
  const headers = { 'Content-Type': 'application/json' };
  if (opts.authHeader) headers.Authorization = opts.authHeader;
  const sig = signBody(body, opts.hmacSecret);
  if (sig) headers['X-Signature'] = sig;

  const maxRetries = opts.maxRetries;
  let lastErr = null;
  let lastStatus = null;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const res = await axios.post(url, envelope, {
        headers,
        timeout: opts.timeoutMs,
        validateStatus: () => true,
        maxRedirects: 0,
      });
      lastStatus = res.status;
      if (res.status >= 200 && res.status < 300) {
        return { ok: true, status: res.status, attempts: attempt };
      }
      lastErr = new Error(`HTTP ${res.status}`);
    } catch (e) {
      lastErr = e;
    }
    if (attempt < maxRetries) {
      const backoffMs = 1000 * Math.pow(4, attempt - 1); // 1s, 4s, 16s
      await new Promise((r) => setTimeout(r, backoffMs));
    }
  }
  return {
    ok: false,
    status: lastStatus,
    error: lastErr ? (lastErr.message || String(lastErr)) : 'unknown error',
    attempts: maxRetries,
  };
}

/**
 * Run a single push cycle. Returns a summary object.
 *   { skipped: bool, reason?: string, results: [{ url, ok, status, attempts, error? }] }
 *
 * If `force` is true, the idempotency guard is bypassed (useful for tests).
 */
async function runOnce({ force = false } = {}) {
  if (!isEnabled()) {
    return { skipped: true, reason: 'disabled', results: [] };
  }
  const urls = getUrls();
  if (urls.length === 0) {
    return { skipped: true, reason: 'no URLs configured', results: [] };
  }

  const db = openDb();
  let report;
  try {
    const { buildWeeklyReport } = require('../controllers/reports.controller');
    report = buildWeeklyReport(db);
  } catch (e) {
    db.close();
    console.warn('[weekly-gov-push] buildWeeklyReport failed:', e.message);
    return { skipped: true, reason: 'build failed: ' + e.message, results: [] };
  }

  const lastWeek = readLastPushedWeek(db);
  if (!force && lastWeek === report.week) {
    db.close();
    return { skipped: true, reason: 'already pushed this week (' + report.week + ')', results: [] };
  }

  const envelope = {
    school_id: getSchoolId(),
    generated_at: report.generated_at,
    week: report.week,
    payload: {
      attendance: report.attendance,
      enrollment: report.enrollment,
      gradeOutliers: report.gradeOutliers,
    },
  };

  const opts = {
    authHeader: getAuthHeader(),
    hmacSecret: getHmacSecret(),
    timeoutMs: getTimeoutMs(),
    maxRetries: getMaxRetries(),
  };

  console.log(`[weekly-gov-push] pushing week ${report.week} to ${urls.length} endpoint(s)`);

  const results = [];
  let allOk = true;
  for (const url of urls) {
    const r = await pushToEndpoint(url, envelope, opts);
    results.push({ url, ok: r.ok, status: r.status, attempts: r.attempts, error: r.error });
    if (r.ok) {
      console.log(`[weekly-gov-push] ok ${url} (status ${r.status}, attempt ${r.attempts})`);
      writeAudit(db, 'gov_push_ok', url, { week: report.week, status: r.status, attempts: r.attempts });
    } else {
      allOk = false;
      console.warn(`[weekly-gov-push] failed ${url} (status ${r.status}, error ${r.error})`);
      writeAudit(db, 'gov_push_failed', url, { week: report.week, status: r.status, error: r.error, attempts: r.attempts });
    }
  }

  if (allOk) {
    writeLastPushedWeek(db, report.week);
  }

  db.close();
  return { skipped: false, week: report.week, results };
}

function tick() {
  const now = new Date();
  if (now.getDay() !== 1) return;          // Monday only
  if (now.getHours() < 6) return;          // before 06:00
  runOnce().catch((e) => console.warn('[weekly-gov-push] tick error:', e.message));
}

const start = () => {
  if (tickHandle) return;                   // idempotent
  if (!isEnabled()) {
    console.log('[weekly-gov-push] disabled (WEEKLY_GOV_PUSH=0)');
    return;
  }
  tickIntervalMs = parseInt(process.env.WEEKLY_GOV_PUSH_INTERVAL_MS || '60000', 10) || 60000;
  tickHandle = setInterval(tick, tickIntervalMs);
  if (tickHandle.unref) tickHandle.unref();
  console.log(`[weekly-gov-push] started (interval ${tickIntervalMs}ms, target ${getUrls().length || 0} URL(s))`);
  // Catch-up: if the server restarted on Monday after 06:00 and we never
  // pushed this week, run once on boot. If it wasn't Monday, no-op.
  runOnce().catch((e) => console.warn('[weekly-gov-push] boot runOnce error:', e.message));
};

const stop = () => {
  if (tickHandle) {
    clearInterval(tickHandle);
    tickHandle = null;
    console.log('[weekly-gov-push] stopped');
  }
};

module.exports = { start, stop, runOnce, invalidatePushConfigCache };
