/**
 * Pure utility helpers for HttpSyncServer.
 *
 * JSON canonicalization, response builders, request validation
 * (route, content-type, body size, body parse), and auth initialization.
 * All functions are stateless and side-effect-free.
 *
 * @module domain/services/sync/HttpSyncServerHelpers
 */
import z from 'zod';
import SyncAuthService from "./SyncAuthService.js";
import SyncError from "../../errors/SyncError.js";
import { validateSyncRequest } from "./SyncPayloadSchema.js";
import SyncSecret from "./SyncSecret.js";
// ── Constants ────────────────────────────────────────────────────────────────
export const DEFAULT_MAX_REQUEST_BYTES = 4 * 1024 * 1024;
export const MAX_REQUEST_BYTES_CEILING = 128 * 1024 * 1024; // 134217728
export const INTERNAL_SYNC_ERROR_CODE = 'E_SYNC_INTERNAL';
// ── Zod schemas ──────────────────────────────────────────────────────────────
export const authSchema = z.object({
    mode: z.enum(['enforce', 'log-only']).default('enforce'),
    keys: z.record(z.custom((v) => v instanceof SyncSecret, 'auth.keys values must be SyncSecret')).refine((obj) => Object.keys(obj).length > 0, 'auth.keys must not be empty'),
    crypto: z.custom((v) => v === undefined || (typeof v === 'object' && v !== null)).optional(),
    logger: z.custom((v) => v === undefined || (typeof v === 'object' && v !== null)).optional(),
    wallClockMs: z.custom((v) => v === undefined || typeof v === 'function').optional(),
    rateLimit: z.object({
        capacity: z.number().int().positive(),
        refillTokensPerSecond: z.number().positive(),
        clock: z.custom((v) => typeof v === 'function', 'auth.rateLimit.clock must be a function'),
    }).strict().optional(),
}).strict();
export const optionsSchema = z.object({
    httpPort: z.custom((v) => v !== null && v !== undefined && typeof v === 'object', 'httpPort must be a non-null object'),
    logger: z.custom((v) => v === undefined || (typeof v === 'object' && v !== null)).optional(),
    graph: z.custom((v) => v !== null && v !== undefined && typeof v === 'object', 'graph must be a non-null object'),
    maxRequestBytes: z.number().int().positive().max(MAX_REQUEST_BYTES_CEILING).default(DEFAULT_MAX_REQUEST_BYTES),
    path: z.string().startsWith('/').default('/sync'),
    host: z.string().min(1).default('127.0.0.1'),
    auth: authSchema.optional(),
    unsafeAllowUnauthenticatedLocalhost: z.boolean().default(false),
    allowedWriters: z.array(z.string()).optional(),
}).strict().superRefine(validateAuthDefaults);
function addConfigIssue(ctx, path, message) {
    ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message,
        path,
    });
}
function validateAuthDefaults(data, ctx) {
    validateAllowedWritersAuthDependency(data, ctx);
    const localHost = isLocalSyncHost(data.host);
    if (data.auth === undefined) {
        validateUnauthenticatedDefaults(data, ctx, localHost);
        return;
    }
    validateAuthenticatedDefaults(data, ctx, localHost);
}
function validateAllowedWritersAuthDependency(data, ctx) {
    if (data.allowedWriters && data.allowedWriters.length > 0 && data.auth === undefined) {
        addConfigIssue(ctx, ['allowedWriters'], 'allowedWriters requires auth.keys to be configured');
    }
}
function validateAuthenticatedDefaults(data, ctx, localHost) {
    if (data.auth === undefined || localHost) {
        return;
    }
    validateNonLocalAuthMode(data.auth, ctx);
    validateNonLocalRateLimit(data.auth, ctx);
}
function validateNonLocalAuthMode(auth, ctx) {
    if (auth.mode !== 'enforce') {
        addConfigIssue(ctx, ['auth', 'mode'], 'non-local sync hosts require auth.mode "enforce"');
    }
}
function validateNonLocalRateLimit(auth, ctx) {
    if (auth.mode === 'enforce' && auth.rateLimit === undefined) {
        addConfigIssue(ctx, ['auth', 'rateLimit'], 'non-local sync hosts require auth.rateLimit');
    }
}
function validateUnauthenticatedDefaults(data, ctx, localHost) {
    if (!localHost) {
        addConfigIssue(ctx, ['auth'], 'sync auth is required for non-local sync hosts');
        return;
    }
    if (!data.unsafeAllowUnauthenticatedLocalhost) {
        addConfigIssue(ctx, ['unsafeAllowUnauthenticatedLocalhost'], 'unauthenticated localhost sync requires unsafeAllowUnauthenticatedLocalhost: true');
    }
}
export function isLocalSyncHost(host) {
    return host === 'localhost' ||
        host === '127.0.0.1' ||
        host === '::1' ||
        host === '[::1]';
}
// ── JSON canonicalization ────────────────────────────────────────────────────
function sortObjectKeys(obj) {
    const sorted = {}; // nosemgrep: ts-no-record-string-unknown-outside-adapters -- 0025B; nosemgrep: ts-no-unknown-outside-adapters -- 0025B
    for (const key of Object.keys(obj).sort()) {
        sorted[key] = canonicalizeJson(obj[key]);
    }
    return sorted;
}
function canonicalizeJson(value) {
    if (Array.isArray(value)) {
        return value.map(canonicalizeJson);
    }
    if (value !== null && value !== undefined && typeof value === 'object') {
        return sortObjectKeys(value); // nosemgrep: ts-no-record-string-unknown-outside-adapters -- 0025B; nosemgrep: ts-no-unknown-outside-adapters -- 0025B
    }
    return value;
}
export function canonicalStringify(value) {
    return JSON.stringify(canonicalizeJson(value)); // nosemgrep: ts-no-json-stringify-in-core -- 0025B
}
// ── Response builders ────────────────────────────────────────────────────────
export function errorResponse(status, message) {
    return {
        status,
        headers: { 'content-type': 'application/json' },
        body: canonicalStringify({ error: message }),
    };
}
export function internalSyncErrorResponse() {
    return {
        status: 500,
        headers: { 'content-type': 'application/json' },
        body: canonicalStringify({
            code: INTERNAL_SYNC_ERROR_CODE,
            error: 'Sync failed',
        }),
    };
}
export function jsonResponse(data) {
    return {
        status: 200,
        headers: { 'content-type': 'application/json' },
        body: canonicalStringify(data),
    };
}
// ── Request validators ───────────────────────────────────────────────────────
export function checkContentType(headers) {
    const contentType = String(headers['content-type'] ?? '').toLowerCase();
    if (contentType.length > 0 && !contentType.startsWith('application/json')) {
        return errorResponse(400, 'Expected application/json');
    }
    return null;
}
export function safeParseUrl(url, headers, defaultHost) {
    const rawUrl = url.length > 0 ? url : '/';
    const hostHeader = String(headers.host ?? '');
    const host = hostHeader.length > 0 ? hostHeader : defaultHost;
    try {
        return new URL(rawUrl, `http://${host}`);
    }
    catch {
        return null;
    }
}
export function validateRoute(request, expectedPath, defaultHost) {
    const requestUrl = safeParseUrl(request.url, request.headers, defaultHost);
    if (requestUrl === null) {
        return errorResponse(400, 'Invalid URL');
    }
    if (requestUrl.pathname !== expectedPath) {
        return errorResponse(404, 'Not Found');
    }
    if (request.method !== 'POST') {
        return errorResponse(405, 'Method Not Allowed');
    }
    return null;
}
export function checkBodySize(body, maxBytes) {
    if (body && body.length > maxBytes) {
        return errorResponse(413, 'Request too large');
    }
    return null;
}
export function parseBody(body) {
    const bodyStr = body ? new TextDecoder().decode(body) : '';
    let parsed; // nosemgrep: ts-no-unknown-outside-adapters -- 0025B
    try {
        parsed = bodyStr.length > 0 ? JSON.parse(bodyStr) : null; // nosemgrep: ts-no-json-parse-in-core -- 0025B
    }
    catch {
        return { error: errorResponse(400, 'Invalid JSON'), parsed: null };
    }
    const validation = validateSyncRequest(parsed);
    if (!validation.ok) {
        return { error: errorResponse(400, `Invalid sync request: ${validation.error}`), parsed: null };
    }
    return { error: null, parsed: validation.value };
}
function buildAuthConfig(auth, allowedWriters) {
    return {
        keys: auth.keys,
        mode: auth.mode,
        ...cryptoField(auth),
        ...loggerField(auth),
        ...wallClockField(auth),
        ...rateLimitField(auth),
        ...allowedWritersField(allowedWriters),
    };
}
function cryptoField(auth) {
    if (auth.crypto === undefined) {
        return {};
    }
    return { crypto: auth.crypto };
}
function loggerField(auth) {
    if (auth.logger === undefined) {
        return {};
    }
    return { logger: auth.logger };
}
function wallClockField(auth) {
    if (auth.wallClockMs === undefined) {
        return {};
    }
    return { wallClockMs: auth.wallClockMs };
}
function rateLimitField(auth) {
    if (auth.rateLimit === undefined) {
        return {};
    }
    return { rateLimit: auth.rateLimit };
}
function allowedWritersField(allowedWriters) {
    if (allowedWriters === undefined) {
        return {};
    }
    return { allowedWriters };
}
export function initAuth(auth, allowedWriters) {
    if (auth) {
        return {
            auth: new SyncAuthService(buildAuthConfig(auth, allowedWriters)),
            authMode: auth.mode,
        };
    }
    return { auth: null, authMode: null };
}
export function waitForListen(server, port, host) {
    return new Promise((resolve, reject) => {
        server.listen(port, host, (err) => {
            if (err) {
                reject(err);
            }
            else {
                resolve();
            }
        });
    });
}
export function buildListenResult(opts) {
    const { server, port, host, path } = opts;
    const address = server.address();
    const actualPort = typeof address === 'object' && address !== null ? address.port : port;
    const url = `http://${host}:${actualPort}${path}`;
    const close = () => new Promise((resolve, reject) => {
        server.close((err) => {
            if (err) {
                reject(err);
            }
            else {
                resolve();
            }
        });
    });
    return { url, close };
}
// ── Frontier extraction ──────────────────────────────────────────────────────
export function extractFrontierWriters(parsed) {
    const { frontier } = parsed;
    if (frontier === null || frontier === undefined || typeof frontier !== 'object') {
        return [];
    }
    return Object.keys(frontier);
}
export function parseOptions(options) {
    try {
        return optionsSchema.parse(options);
    }
    catch (err) {
        if (err instanceof z.ZodError) {
            const messages = err.issues.map((i) => i.message).join('; ');
            throw new SyncError(`HttpSyncServer config: ${messages}`, { code: 'E_SYNC_PROTOCOL' });
        }
        throw err;
    }
}
