import { logger } from '@/lib/logger';
import { effectiveConsent, endpointEnabled, loadState, saveState } from './state';
import { buildPayload } from './payload';
import { resolveEndpointAllowed } from './endpoint-guard';
import { DEFAULT_ENDPOINT } from './types';

const DAY_MS = 24 * 60 * 60 * 1000;
const JITTER_MS = 2 * 60 * 60 * 1000; // ± 2 hours
const FIRST_DELAY_MS = 60 * 60 * 1000;  // 1 hour after consent

let currentTimer: NodeJS.Timeout | null = null;

function jitteredDelay(base: number): number {
  const j = (Math.random() * 2 - 1) * JITTER_MS;
  return Math.max(60_000, base + j);
}

export async function sendOnce(opts?: { reason?: string }): Promise<{
  ok: boolean;
  status?: number;
  error?: string;
}> {
  const { consent, source, state } = await effectiveConsent();
  if (consent !== 'on') return { ok: false, error: `consent ${consent} (source ${source})` };
  const endpoint = state.endpoint || DEFAULT_ENDPOINT;
  if (!endpointEnabled(endpoint)) return { ok: false, error: 'endpoint blank' };

  // Re-check at fetch time: defeats DNS rebinding, and catches the case
  // where state.json was edited out-of-band to bypass the admin API.
  const guard = await resolveEndpointAllowed(endpoint);
  if (!guard.ok) {
    logger.warn('telemetry: endpoint blocked', { reason: guard.reason });
    return { ok: false, error: `endpoint blocked: ${guard.reason}` };
  }

  const payload = await buildPayload();
  try {
    const res = await fetch(endpoint, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify(payload),
      signal: AbortSignal.timeout(5000),
    });
    const ok = res.ok;
    if (ok) {
      const next = await loadState();
      next.lastSentAt = new Date().toISOString();
      await saveState(next);
    }
    logger.info('telemetry: heartbeat', {
      ok, status: res.status, reason: opts?.reason ?? 'scheduled',
    });
    return { ok, status: res.status };
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    logger.warn('telemetry: heartbeat failed', { error: msg });
    return { ok: false, error: msg };
  }
}

async function scheduleNext(delayMs: number): Promise<void> {
  if (currentTimer) clearTimeout(currentTimer);
  const at = new Date(Date.now() + delayMs).toISOString();
  const state = await loadState();
  state.nextScheduledAt = at;
  await saveState(state);
  currentTimer = setTimeout(() => { void tick(); }, delayMs);
  // Don't keep the process alive just for this.
  currentTimer.unref?.();
}

async function tick(): Promise<void> {
  await sendOnce({ reason: 'scheduled' });
  await scheduleNext(jitteredDelay(DAY_MS));
}

// Called from instrumentation. Idempotent.
export async function startScheduler(): Promise<void> {
  const { consent } = await effectiveConsent();
  if (consent !== 'on') {
    logger.info('telemetry: scheduler not started', { consent });
    return;
  }
  const state = await loadState();
  // If we have a next-scheduled time in the future use it; otherwise schedule
  // FIRST_DELAY_MS out. This means after a restart we don't fire immediately.
  let delay = FIRST_DELAY_MS;
  if (state.nextScheduledAt) {
    const remaining = new Date(state.nextScheduledAt).getTime() - Date.now();
    if (remaining > 0) delay = Math.min(remaining, DAY_MS + JITTER_MS);
  }
  await scheduleNext(delay);
  logger.info('telemetry: scheduler started', {
    nextInMs: delay,
    endpoint: state.endpoint,
  });
}

export async function stopScheduler(): Promise<void> {
  if (currentTimer) clearTimeout(currentTimer);
  currentTimer = null;
}

// Called when consent flips on/off via the UI.
export async function reschedule(): Promise<void> {
  await stopScheduler();
  await startScheduler();
}
