import type { Handle } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';
import { handleErrorWithSentry } from '@sentry/sveltekit';
import * as Sentry from '@sentry/sveltekit';
import { randomBytes, createHmac } from 'node:crypto';
import { env } from '$env/dynamic/private';
import { env as pubEnv } from '$env/dynamic/public';
import { log } from '$lib/server/logger';
import { createUserSession, touchUserSession, isSessionRevoked, getUserById } from '$lib/server/pocketbase';
import { drain as drainPresignCache } from '$lib/server/presignCache';
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
import { BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';
import { resourceFromAttributes } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
import { paraglideMiddleware } from '$lib/paraglide/server';

// ─── OpenTelemetry server-side tracing + logs ─────────────────────────────────
// No-op when OTEL_EXPORTER_OTLP_ENDPOINT is unset (e.g. local dev).
const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
if (otlpEndpoint) {
	const sdk = new NodeSDK({
		resource: resourceFromAttributes({
			[ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME ?? 'ui',
			[ATTR_SERVICE_VERSION]: pubEnv.PUBLIC_BUILD_VERSION ?? 'dev'
		}),
		traceExporter: new OTLPTraceExporter({ url: `${otlpEndpoint}/v1/traces` }),
		logRecordProcessors: [
			new BatchLogRecordProcessor(
				new OTLPLogExporter({ url: `${otlpEndpoint}/v1/logs` })
			)
		]
	});
	sdk.start();
	process.once('SIGTERM', () => sdk.shutdown().catch(() => {}));
	process.once('SIGINT', () => sdk.shutdown().catch(() => {}));
}

// ─── Sentry / GlitchTip server-side error tracking ────────────────────────────
// No-op when PUBLIC_GLITCHTIP_DSN is unset (e.g. local dev).
if (pubEnv.PUBLIC_GLITCHTIP_DSN) {
	Sentry.init({
		dsn: pubEnv.PUBLIC_GLITCHTIP_DSN,
		tracesSampleRate: 0.1,
		// Must match the release name used when uploading source maps in CI
		// (BUILD_VERSION injected by Dockerfile as PUBLIC_BUILD_VERSION).
		release: pubEnv.PUBLIC_BUILD_VERSION || undefined
	});
}

export const handleError = handleErrorWithSentry();

// ─── Graceful shutdown ────────────────────────────────────────────────────────
//
// When Docker/Kubernetes sends SIGTERM (or the user sends SIGINT), we:
//  1. Set shuttingDown = true so new requests immediately receive 503.
//  2. Flush/drain in-process caches (presign URL cache).
//  3. Allow Node.js to exit naturally once in-flight requests finish.
//
// adapter-node does not provide a built-in hook for this, so we wire it here
// in hooks.server.ts which runs in the server Node.js process.

let shuttingDown = false;

function shutdown(signal: string) {
	if (shuttingDown) return;
	shuttingDown = true;
	log.info('shutdown', `received ${signal}, draining in-flight requests`);
	drainPresignCache();
	// Don't call process.exit() — let Node exit naturally once the event loop
	// is empty (adapter-node closes the HTTP server on its own).
}

process.once('SIGTERM', () => shutdown('SIGTERM'));
process.once('SIGINT', () => shutdown('SIGINT'));

const SESSION_COOKIE = 'libnovel_session';
const AUTH_COOKIE = 'libnovel_auth';
const ONE_YEAR = 60 * 60 * 24 * 365;

const AUTH_SECRET = env.AUTH_SECRET ?? 'dev_secret_change_in_production';

// ─── Token helpers ────────────────────────────────────────────────────────────

/**
 * Sign a payload string with HMAC-SHA256 using AUTH_SECRET.
 * Returns "<payload>.<signature>".
 */
export function signToken(payload: string): string {
	const sig = createHmac('sha256', AUTH_SECRET).update(payload).digest('hex');
	return `${payload}.${sig}`;
}

/**
 * Verify a signed token. Returns the payload string on success, null on failure.
 */
export function verifyToken(token: string): string | null {
	const lastDot = token.lastIndexOf('.');
	if (lastDot < 0) return null;
	const payload = token.slice(0, lastDot);
	const expected = createHmac('sha256', AUTH_SECRET).update(payload).digest('hex');
	const actual = token.slice(lastDot + 1);
	// constant-time comparison
	if (expected.length !== actual.length) return null;
	let diff = 0;
	for (let i = 0; i < expected.length; i++) {
		diff |= expected.charCodeAt(i) ^ actual.charCodeAt(i);
	}
	return diff === 0 ? payload : null;
}

/**
 * Create a signed auth token for a user.
 * Payload format: "<userId>:<username>:<role>:<authSessionId>"
 * authSessionId uniquely identifies this login session (for revocation).
 */
export function createAuthToken(userId: string, username: string, role: string, authSessionId: string): string {
	return signToken(`${userId}:${username}:${role}:${authSessionId}`);
}

/**
 * Parse a verified auth token into user data. Returns null if invalid.
 * Supports both old format (3 segments) and new format (4 segments).
 */
export function parseAuthToken(token: string): { id: string; username: string; role: string; authSessionId: string } | null {
	const payload = verifyToken(token);
	if (!payload) return null;
	const parts = payload.split(':');
	// New format: userId:username:role:authSessionId (4 parts)
	// Old format: userId:username:role (3 parts — legacy tokens before session tracking)
	if (parts.length < 3) return null;
	const id = parts[0];
	const username = parts[1];
	const role = parts[2];
	const authSessionId = parts[3] ?? ''; // empty string for legacy tokens
	if (!id || !username) return null;
	return { id, username, role, authSessionId };
}

// ─── Hook ─────────────────────────────────────────────────────────────────────

function getTextDirection(locale: string): string {
	// All supported locales (en, ru, id, pt, fr) are LTR
	return 'ltr';
}

const paraglideHandle: Handle = ({ event, resolve }) =>
	paraglideMiddleware(event.request, ({ request: localizedRequest, locale }) => {
		event.request = localizedRequest;
		return resolve(event, {
			transformPageChunk: ({ html }) =>
				html.replace('%lang%', locale).replace('%dir%', getTextDirection(locale))
		});
	});

const appHandle: Handle = async ({ event, resolve }) => {
	// During graceful shutdown, reject new requests immediately so the load
	// balancer / Docker health-check can drain existing connections.
	if (shuttingDown) {
		return new Response('Service shutting down', { status: 503 });
	}

	// Anonymous session cookie (for reading progress)
	let sessionId = event.cookies.get(SESSION_COOKIE) ?? '';
	if (!sessionId) {
		sessionId = randomBytes(16).toString('hex');
		event.cookies.set(SESSION_COOKIE, sessionId, {
			path: '/',
			httpOnly: true,
			sameSite: 'lax',
			maxAge: ONE_YEAR
		});
	}
	event.locals.sessionId = sessionId;

	// Auth cookie → resolve logged-in user
	const authToken = event.cookies.get(AUTH_COOKIE);
	if (authToken) {
		const user = parseAuthToken(authToken);
		if (!user) {
			log.warn('auth', 'auth cookie present but failed to parse (malformed or tampered)');
			event.locals.user = null;
		} else {
			// Validate session against DB (only for new-format tokens with authSessionId)
			let sessionValid = true;
			if (user.authSessionId) {
				try {
					const revoked = await isSessionRevoked(user.authSessionId);
					if (revoked) {
						log.info('auth', 'auth cookie references revoked session', {
							userId: user.id,
							authSessionId: user.authSessionId
						});
						sessionValid = false;
						// Clear the invalid cookie
						event.cookies.delete(AUTH_COOKIE, { path: '/' });
					} else {
						// Best-effort: update last_seen in the background
						touchUserSession(user.authSessionId).catch(() => {});
					}
				} catch (err) {
					// DB error — fail open to avoid locking everyone out
					log.warn('auth', 'session check failed (fail open)', { err: String(err) });
				}
			}
			event.locals.user = sessionValid ? user : null;
		}
	} else {
		event.locals.user = null;
	}

	// ── isPro: read fresh from DB so role changes take effect without re-login ──
	if (event.locals.user) {
		try {
			const dbUser = await getUserById(event.locals.user.id);
			event.locals.isPro = dbUser?.role === 'pro' || dbUser?.role === 'admin';
		} catch {
			event.locals.isPro = false;
		}
	} else {
		event.locals.isPro = false;
	}

	return resolve(event);
};

export const handle = sequence(paraglideHandle, appHandle);

