/**
 * Deep health check (P2-10).
 *
 * The shallow /api/health route only proves the DB is reachable. That's
 * not enough to load-balance against — a healthy DB with a wedged worker
 * still returns 200 there. /api/health/deep adds:
 *  - DB query latency budget (default 100 ms)
 *  - "Has any check run recently?" — proves the worker / cron loop is alive
 *  - Outbox backlog depth — proves notifications aren't piling up
 *  - SMTP TCP connectivity (best-effort; skipped if not configured)
 *
 * Any failing check returns 503 with structured details. Operators wire
 * this into their orchestrator's liveness probe.
 *
 * PR-B.4 (May 2026): orchestrator probes auth via `Authorization:
 * Bearer $CRON_SECRET`. The endpoint is excluded from the NextAuth
 * middleware (so no redirect to /login) but enforces its own auth
 * because the response reveals operational signals (outbox depth,
 * worker activity, SMTP reachability) that should not be public.
 */
import { NextRequest, NextResponse } from 'next/server';
import net from 'net';
import { prisma } from '@/lib/prisma';
import { requireCronSecret } from '@/lib/api-helpers/cron-auth';

export const dynamic = 'force-dynamic';

interface CheckResult {
    name: string;
    ok: boolean;
    latencyMs?: number;
    detail?: string;
}

const DB_LATENCY_BUDGET_MS = 200;
const STALE_CHECK_WINDOW_MS = 5 * 60 * 1000; // 5 minutes
const OUTBOX_DEPTH_BUDGET = 500;
const SMTP_CONNECT_TIMEOUT_MS = 2_000;

async function checkDb(): Promise<CheckResult> {
    const start = Date.now();
    try {
        await prisma.$queryRaw`SELECT 1`;
        const latencyMs = Date.now() - start;
        return {
            name: 'db',
            ok: latencyMs < DB_LATENCY_BUDGET_MS,
            latencyMs,
            detail: latencyMs < DB_LATENCY_BUDGET_MS ? undefined : `over ${DB_LATENCY_BUDGET_MS}ms budget`,
        };
    } catch (err) {
        return { name: 'db', ok: false, detail: (err as Error).message };
    }
}

async function checkWorkerLiveness(): Promise<CheckResult> {
    try {
        // Most recent successful lastCheckAt across any active monitor.
        const latest = await prisma.monitor.findFirst({
            where: { active: true, deletedAt: null },
            select: { lastCheckAt: true },
            orderBy: { lastCheckAt: 'desc' },
        });
        if (!latest?.lastCheckAt) {
            // No monitors at all → nothing to assert against.
            return { name: 'worker', ok: true, detail: 'no active monitors yet' };
        }
        const ageMs = Date.now() - latest.lastCheckAt.getTime();
        return {
            name: 'worker',
            ok: ageMs < STALE_CHECK_WINDOW_MS,
            detail: ageMs < STALE_CHECK_WINDOW_MS
                ? `last check ${Math.round(ageMs / 1000)}s ago`
                : `STALE: last check ${Math.round(ageMs / 1000)}s ago (> ${STALE_CHECK_WINDOW_MS / 1000}s)`,
        };
    } catch (err) {
        return { name: 'worker', ok: false, detail: (err as Error).message };
    }
}

async function checkOutboxBacklog(): Promise<CheckResult> {
    try {
        const backlog = await prisma.notificationOutbox.count({
            where: { deliveredAt: null, failedAt: null },
        });
        return {
            name: 'outbox',
            ok: backlog < OUTBOX_DEPTH_BUDGET,
            detail: `${backlog} pending (budget: ${OUTBOX_DEPTH_BUDGET})`,
        };
    } catch (err) {
        return { name: 'outbox', ok: false, detail: (err as Error).message };
    }
}

async function checkSmtp(): Promise<CheckResult> {
    const host = process.env.SMTP_HOST;
    const portRaw = process.env.SMTP_PORT;
    if (!host || !portRaw) {
        return { name: 'smtp', ok: true, detail: 'not configured' };
    }
    const port = parseInt(portRaw, 10);
    return new Promise((resolve) => {
        const socket = new net.Socket();
        const cleanup = () => {
            try { socket.destroy(); } catch { /* noop */ }
        };
        const timer = setTimeout(() => {
            cleanup();
            resolve({ name: 'smtp', ok: false, detail: `timeout connecting to ${host}:${port}` });
        }, SMTP_CONNECT_TIMEOUT_MS);
        socket.once('connect', () => {
            clearTimeout(timer);
            cleanup();
            resolve({ name: 'smtp', ok: true, detail: `${host}:${port} reachable` });
        });
        socket.once('error', (err) => {
            clearTimeout(timer);
            cleanup();
            resolve({ name: 'smtp', ok: false, detail: err.message });
        });
        socket.connect(port, host);
    });
}

export async function GET(req: NextRequest) {
    // PR-B.4: gate behind CRON_SECRET so unauthenticated probes can't
    // enumerate operational signals (worker activity, outbox depth,
    // SMTP reachability). Same secret the worker uses for cron pings.
    const denied = requireCronSecret(req);
    if (denied) return denied;

    const checks = await Promise.all([
        checkDb(),
        checkWorkerLiveness(),
        checkOutboxBacklog(),
        checkSmtp(),
    ]);

    const allOk = checks.every((c) => c.ok);
    return NextResponse.json(
        {
            status: allOk ? 'healthy' : 'degraded',
            timestamp: new Date().toISOString(),
            checks,
        },
        { status: allOk ? 200 : 503 }
    );
}
