import { RestateError, RetryableError, TerminalError, ensureError, logError } from "../../types/errors.js";
import { CompletablePromise } from "../../utils/completable_promise.js";
import { HandlerKind } from "../../types/rpc.js";
import { parseUrlComponents } from "../components.js";
import { LogSource, LoggerContext, RestateLogLevel } from "../../logging/logger_transport.js";
import { DEFAULT_CONSOLE_LOGGER_LOG_LEVEL } from "../../logging/console_logger_transport.js";
import { createLogger } from "../../logging/logger.js";
import { X_RESTATE_SERVER } from "../../user_agent.js";
import { destroyLogger, registerLogger } from "./core_logging.js";
import { LogLevel, WasmHeader, WasmIdentityVerifier, WasmVM, set_log_level } from "./vm/sdk_shared_core_wasm_bindings.js";
import { CommandError, ContextImpl } from "../../context_impl.js";
import { restoreError, sanitizeError } from "../../error_sanitization.js";
import { errorResponse, invocationIdFromHeaders, simpleResponse, tryCreateContextualLogger } from "./utils.js";
import { handleDiscovery } from "./discovery.js";
import { handlePreview } from "./preview.js";
import { millisOrDurationToMillis } from "@restatedev/restate-sdk-core";

//#region src/endpoint/handlers/generic.ts
const HOOK_CONTEXT_IS_PROCESSING_SYMBOL = Symbol.for("@restatedev/restate-sdk/hooks.isProcessing");
function createRestateHandler(endpoint, protocolMode, additionalDiscoveryFields) {
	return new RestateHandlerImpl(endpoint, protocolMode, additionalDiscoveryFields);
}
/**
* This is the RestateHandler implementation
*/
var RestateHandlerImpl = class {
	identityVerifier;
	constructor(endpoint, protocolMode, additionalDiscoveryFields) {
		this.endpoint = endpoint;
		this.protocolMode = protocolMode;
		this.additionalDiscoveryFields = additionalDiscoveryFields;
		if (this.endpoint.keySet === void 0 || this.endpoint.keySet.length === 0) this.endpoint.rlog.warn(`Accepting requests without validating request signatures; handler access must be restricted`);
		else {
			this.endpoint.rlog.info(`Validating requests using signing keys [${this.endpoint.keySet}]`);
			this.identityVerifier = new WasmIdentityVerifier(this.endpoint.keySet);
		}
		set_log_level(restateLogLevelToWasmLogLevel(DEFAULT_CONSOLE_LOGGER_LOG_LEVEL));
	}
	handle(request, context) {
		try {
			return this._handle(request, context);
		} catch (e) {
			const error = ensureError(e);
			(tryCreateContextualLogger(this.endpoint.loggerTransport, request.url, request.headers) ?? this.endpoint.rlog).error("Error while handling request: " + (error.stack ?? error.message));
			return errorResponse(error instanceof RestateError ? error.code : 500, error.message);
		}
	}
	_handle(request, context) {
		const path = new URL(request.url, "https://example.com").pathname;
		const parsed = parseUrlComponents(path);
		if (parsed.type === "unknown") {
			const msg = "Invalid path. Allowed are /health, /discover, /invoke/SvcName/handlerName, /serdes/SvcName/decode/<serdeName>, or /serdes/SvcName/encode/<serdeName>, but was: " + path;
			this.endpoint.rlog.trace(msg);
			return errorResponse(404, msg);
		}
		if (parsed.type === "health") return simpleResponse(200, {
			"content-type": "application/text",
			"x-restate-server": X_RESTATE_SERVER
		}, new TextEncoder().encode("OK"));
		const error = this.validateConnectionSignature(path, request.headers);
		if (error !== null) return error;
		if (parsed.type === "discover") return handleDiscovery(this.endpoint, this.protocolMode, this.additionalDiscoveryFields, request.headers["accept"]);
		if (parsed.type === "preview") return handlePreview(this.endpoint, parsed);
		return this.handleInvoke(parsed, request.headers, request.extraArgs, context ?? {});
	}
	validateConnectionSignature(path, headers) {
		if (!this.identityVerifier) return null;
		const vmHeaders = Object.entries(headers).filter(([, v]) => v !== void 0).map(([k, v]) => new WasmHeader(k, v instanceof Array ? v[0] : v));
		try {
			this.identityVerifier.verify_identity(path, vmHeaders);
			return null;
		} catch (e) {
			this.endpoint.rlog.error(`Rejecting request as its JWT did not validate: ${e}`);
			return errorResponse(401, "Unauthorized");
		}
	}
	handleInvoke(invokePathComponent, headers, extraArgs, additionalContext) {
		if (typeof headers["content-type"] !== "string") {
			const errorMessage = "Missing content-type header";
			this.endpoint.rlog.warn(errorMessage);
			return errorResponse(415, errorMessage);
		}
		const service = this.endpoint.components.get(invokePathComponent.componentName);
		if (!service) {
			const msg = `No service found for URL: ${JSON.stringify(invokePathComponent)}`;
			this.endpoint.rlog.error(msg);
			return errorResponse(404, msg);
		}
		const handler = service.handlerMatching(invokePathComponent);
		if (!handler) {
			const msg = `No handler found for URL: ${JSON.stringify(invokePathComponent)}`;
			this.endpoint.rlog.error(msg);
			return errorResponse(404, msg);
		}
		return new RestateInvokeResponse(service, handler, headers, extraArgs, additionalContext, this.endpoint.journalValueCodec, this.endpoint.loggerTransport);
	}
};
var RestateInvokeResponse = class {
	headers;
	statusCode;
	loggerId;
	vmLogger;
	coreVm;
	constructor(service, handler, attemptHeaders, extraArgs, additionalContext, journalValueCodecInit, loggerTransport) {
		this.service = service;
		this.handler = handler;
		this.attemptHeaders = attemptHeaders;
		this.extraArgs = extraArgs;
		this.additionalContext = additionalContext;
		this.journalValueCodecInit = journalValueCodecInit;
		this.loggerTransport = loggerTransport;
		this.loggerId = Math.floor(Math.random() * 4294967295);
		const isJournalCodecDefined = this.journalValueCodecInit !== void 0;
		const vmHeaders = Object.entries(this.attemptHeaders).filter(([, v]) => v !== void 0).map(([k, v]) => new WasmHeader(k, v instanceof Array ? v[0] : v));
		this.coreVm = new WasmVM(vmHeaders, restateLogLevelToWasmLogLevel(DEFAULT_CONSOLE_LOGGER_LOG_LEVEL), this.loggerId, isJournalCodecDefined, handler.executionOptions.explicitCancellation ?? false);
		const responseHead = this.coreVm.get_response_head();
		this.statusCode = responseHead.status_code;
		this.headers = responseHead.headers.reduce((headers, { key, value }) => ({
			[key]: value,
			...headers
		}), { "x-restate-server": X_RESTATE_SERVER });
		this.vmLogger = createLogger(this.loggerTransport, LogSource.JOURNAL, new LoggerContext(invocationIdFromHeaders(this.attemptHeaders), this.service.name(), this.handler.name(), void 0, void 0, this.additionalContext));
	}
	async process({ inputReader, outputWriter, writeHead, abortSignal }) {
		writeHead(this.statusCode, this.headers);
		abortSignal.addEventListener("abort", () => {
			destroyLogger(this.loggerId);
			setImmediate(() => {
				const msg = "Connection closed";
				this.coreVm.notify_error(msg, msg);
			});
		}, { once: true });
		registerLogger(this.loggerId, this.vmLogger);
		const journalValueCodec = this.journalValueCodecInit ? await this.journalValueCodecInit : {
			encode: (entry) => entry,
			decode: (entry) => Promise.resolve(entry)
		};
		const invocationEndPromise = new CompletablePromise();
		let ctx;
		try {
			await bufferJournalReplayInCoreVm(this.coreVm, inputReader);
			const input = this.coreVm.sys_input();
			const invocationRequest = {
				target: {
					service: this.service.name(),
					handler: this.handler.name(),
					key: input.key || void 0,
					toString() {
						return this.key !== void 0 ? `${this.service}/${this.key}/${this.handler}` : `${this.service}/${this.handler}`;
					}
				},
				id: input.invocation_id,
				headers: input.headers.reduce((headers, { key, value }) => {
					headers.set(key, value);
					return headers;
				}, /* @__PURE__ */ new Map()),
				attemptHeaders: Object.entries(this.attemptHeaders).reduce((headers, [key, value]) => {
					if (value !== void 0) headers.set(key, value instanceof Array ? value[0] : value);
					return headers;
				}, /* @__PURE__ */ new Map()),
				body: input.input,
				extraArgs: this.extraArgs,
				attemptCompletedSignal: abortSignal
			};
			const loggerContext = new LoggerContext(input.invocation_id, this.handler.component().name(), this.handler.name(), this.handler.kind() === HandlerKind.SERVICE ? void 0 : input.key, invocationRequest, this.additionalContext);
			const ctxLogger = createLogger(this.loggerTransport, LogSource.USER, loggerContext, () => !this.coreVm.is_processing());
			this.vmLogger = createLogger(this.loggerTransport, LogSource.JOURNAL, loggerContext);
			registerLogger(this.loggerId, this.vmLogger);
			if (!this.coreVm.is_processing()) this.vmLogger.info("Replaying invocation.");
			else this.vmLogger.info("Starting invocation.");
			ctx = new ContextImpl(this.coreVm, input, ctxLogger, this.handler.kind(), this.vmLogger, invocationRequest, invocationEndPromise, inputReader, outputWriter, journalValueCodec, this.handler.executionOptions);
		} catch (e) {
			const error = ensureError(e);
			this.coreVm.notify_error(error.message, error.message);
			await flushAndClose(this.coreVm, this.vmLogger, inputReader, outputWriter);
			return;
		}
		try {
			await startUserHandler(ctx, this.service, this.handler, journalValueCodec);
		} catch (e) {
			notifyError(e, ctx, this.handler.executionOptions.asTerminalError);
		} finally {
			await flushAndClose(this.coreVm, this.vmLogger, inputReader, outputWriter);
		}
	}
};
async function bufferJournalReplayInCoreVm(coreVm, inputReader) {
	while (!coreVm.is_ready_to_execute()) {
		const nextValue = await inputReader.next();
		if (nextValue.done) {
			coreVm.notify_input_closed();
			break;
		}
		if (nextValue.value !== void 0) coreVm.notify_input(nextValue.value);
	}
}
async function startUserHandler(ctx, service, handler, journalValueCodec) {
	const hooks = [];
	for (const provider of handler.executionOptions.hooks ?? []) {
		const hookContext = { request: ctx.request() };
		Object.defineProperty(hookContext, HOOK_CONTEXT_IS_PROCESSING_SYMBOL, {
			value: () => ctx.isProcessing(),
			enumerable: false
		});
		hooks.push(provider(hookContext));
	}
	const handlerInterceptor = composeInterceptors(hooks.map((h) => h.interceptor?.handler).filter(isDefined));
	ctx.setRunInterceptor(composeInterceptors(hooks.map((h) => h.interceptor?.run).filter(isDefined)));
	let encodedOutput;
	await raceWithAttemptEnd(ctx, handlerInterceptor)(async () => {
		const decodedInput = await journalValueCodec.decode(ctx.request().body).catch((e) => Promise.reject(new TerminalError(`Failed to decode input using journal value codec: ${ensureError(e).message}`, { errorCode: 400 })));
		const output = await handler.invoke(ctx, decodedInput);
		encodedOutput = journalValueCodec.encode(output);
	});
	ctx.coreVm.sys_write_output_success(encodedOutput);
	ctx.coreVm.sys_end();
	ctx.vmLogger.info("Invocation completed successfully.");
}
/**
* Classifies the error and notifies the VM. Called from the process() catch
* block — the single place that decides how to report errors to the VM.
*/
function notifyError(e, ctx, asTerminalError) {
	if (e instanceof CommandError) {
		const cause = ensureError(e.cause);
		logError(ctx.vmLogger, cause);
		if (e.hasCommandIndex) ctx.coreVm.notify_error_for_specific_command(cause.message, cause.stack, e.commandType, e.commandIndex, null);
		else ctx.coreVm.notify_error_for_next_command(cause.message, cause.stack, e.commandType);
		return;
	}
	const error = ensureError(e, asTerminalError);
	logError(ctx.vmLogger, error);
	try {
		if (error instanceof TerminalError) {
			ctx.coreVm.sys_write_output_failure({
				code: error.code,
				message: error.message,
				metadata: Object.entries(error.metadata ?? {}).map(([key, value]) => ({
					key,
					value
				}))
			});
			ctx.coreVm.sys_end();
		} else if (error instanceof RetryableError) ctx.coreVm.notify_error_with_delay_override(error.message, error.stack, error.retryAfter !== void 0 ? BigInt(millisOrDurationToMillis(error.retryAfter)) : void 0);
		else ctx.coreVm.notify_error(error.message, error.stack);
	} catch (vmError) {
		const inner = ensureError(vmError);
		ctx.coreVm.notify_error(inner.message, inner.stack);
	}
}
async function flushAndClose(coreVm, vmLogger, inputReader, outputWriter) {
	let inputClosed = false;
	try {
		let nextOutput = coreVm.take_output();
		while (nextOutput !== null && nextOutput !== void 0) {
			await outputWriter.write(nextOutput);
			nextOutput = coreVm.take_output();
		}
		while (!inputClosed) try {
			inputClosed = (await inputReader.next()).done ?? false;
		} catch (e) {
			inputClosed = true;
		}
		await outputWriter.close();
	} catch (e) {
		const error = ensureError(e);
		const abortErrorOnWrite = isAbortErrorOnWrite(error);
		if (inputClosed && abortErrorOnWrite) return;
		if (abortErrorOnWrite) vmLogger.error("Got abort error from connection: " + error.message + "\nThis might indicate that:\n* The restate-server aborted the connection after hitting the 'abort-timeout'\n* The connection with the restate-server was lost\n\nPlease check the invocation in the Restate UI for more details.");
		else vmLogger.error("Error while handling request: " + (error.stack ?? error.message));
	}
}
function isAbortErrorOnWrite(error) {
	return error.name === "AbortError" || error.message === "Invalid state: WritableStream is closed" || error.code === "ERR_HTTP2_INVALID_STREAM";
}
function composeInterceptors(interceptors) {
	return interceptors.reduceRight((innerInterceptor, interceptor) => (...args) => {
		const context = args.slice(0, -1);
		const callback = args.at(-1);
		return interceptor(...context, () => innerInterceptor(...context, callback));
	}, (...args) => args.at(-1)());
}
/**
* Wraps an interceptor so that both `next()` and the interceptor body race
* against `invocationEndPromise`. When the attempt ends (suspension, retryable
* error, etc.), the promise rejects and the interceptor chain unwinds through
* catch/finally blocks — preventing interceptors from hanging on a `next()`
* that will never settle.
*
* SDK-internal metadata (CommandError, retryAfter) is stripped before the
* interceptor sees the error and restored after the chain exits.
*/
function raceWithAttemptEnd(ctx, interceptor) {
	return (...args) => {
		let originalError;
		const signal = ctx.invocationEndPromise.promise.catch((e) => {
			originalError = e;
			throw sanitizeError(e);
		});
		const originalNext = args.at(-1);
		const racingNext = () => Promise.race([originalNext(), signal]);
		const newArgs = [...args.slice(0, -1), racingNext];
		return Promise.race([interceptor(...newArgs), signal]).catch((e) => {
			if (originalError !== void 0) throw restoreError(e, originalError);
			throw e;
		});
	};
}
function isDefined(value) {
	return value != null;
}
function restateLogLevelToWasmLogLevel(level) {
	switch (level) {
		case RestateLogLevel.TRACE: return LogLevel.TRACE;
		case RestateLogLevel.DEBUG: return LogLevel.DEBUG;
		case RestateLogLevel.INFO: return LogLevel.INFO;
		case RestateLogLevel.WARN: return LogLevel.WARN;
		case RestateLogLevel.ERROR: return LogLevel.ERROR;
	}
}

//#endregion
export { createRestateHandler };
//# sourceMappingURL=generic.js.map