import { RestateError, RetryableError, TerminalError, ensureError, logError } from "../../types/errors.js";
import { HandlerKind } from "../../types/rpc.js";
import { parseUrlComponents } from "../components.js";
import { X_RESTATE_SERVER } from "../../user_agent.js";
import { OnceStream } from "../../utils/streams.js";
import { CompletablePromise } from "../../utils/completable_promise.js";
import { LogSource, LoggerContext, RestateLogLevel } from "../../logging/logger_transport.js";
import { createLogger } from "../../logging/logger.js";
import { DEFAULT_CONSOLE_LOGGER_LOG_LEVEL, defaultLoggerTransport } from "../../logging/console_logger_transport.js";
import { LogLevel, WasmHeader, WasmIdentityVerifier, WasmVM, set_log_level } from "./vm/sdk_shared_core_wasm_bindings.js";
import { ContextImpl } from "../../context_impl.js";
import { millisOrDurationToMillis } from "@restatedev/restate-sdk-core";
import { TransformStream } from "node:stream/web";

//#region src/endpoint/handlers/generic.ts
const ENDPOINT_MANIFEST_V2 = "application/vnd.restate.endpointmanifest.v2+json";
const ENDPOINT_MANIFEST_V3 = "application/vnd.restate.endpointmanifest.v3+json";
const ENDPOINT_MANIFEST_V4 = "application/vnd.restate.endpointmanifest.v4+json";
function tryCreateContextualLogger(loggerTransport, url, headers, additionalContext) {
	try {
		const path = new URL(url, "https://example.com").pathname;
		const parsed = parseUrlComponents(path);
		if (parsed.type !== "invoke") return;
		const invocationId = invocationIdFromHeaders(headers);
		return createLogger(loggerTransport, LogSource.SYSTEM, new LoggerContext(invocationId, parsed.componentName, parsed.handlerName, void 0, void 0, additionalContext));
	} catch {
		return;
	}
}
function invocationIdFromHeaders(headers) {
	const invocationIdHeader = headers["x-restate-invocation-id"];
	return typeof invocationIdHeader === "string" ? invocationIdHeader : Array.isArray(invocationIdHeader) ? invocationIdHeader[0] ?? "unknown id" : "unknown id";
}
/**
* This is an internal API to support 'fetch' like handlers.
* It supports both request-reply mode and bidirectional streaming mode.
*
* An individual handler will have to convert the shape of the incoming request
* to a RestateRequest, and then pass it to this handler, and eventually convert back
* the response.
* Different runtimes have slightly different shapes of the incoming request, and responses.
*/
var GenericHandler = 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));
	}
	async handle(request, context) {
		try {
			return await 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 this.toErrorResponse(error instanceof RestateError ? error.code : 500, error.message);
		}
	}
	async _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, or /discover, or /invoke/SvcName/handlerName, but was: ${path}`;
			this.endpoint.rlog.trace(msg);
			return this.toErrorResponse(404, msg);
		}
		if (parsed.type === "health") return {
			body: OnceStream(new TextEncoder().encode("OK")),
			headers: {
				"content-type": "application/text",
				"x-restate-server": X_RESTATE_SERVER
			},
			statusCode: 200
		};
		const error = this.validateConnectionSignature(path, request.headers);
		if (error !== null) return error;
		if (parsed.type === "discover") return this.handleDiscovery(request.headers["accept"]);
		if (typeof request.headers["content-type"] !== "string") {
			const errorMessage = "Missing content-type header";
			this.endpoint.rlog.warn(errorMessage);
			return this.toErrorResponse(415, errorMessage);
		}
		const service = this.endpoint.components.get(parsed.componentName);
		if (!service) {
			const msg = `No service found for URL: ${JSON.stringify(parsed)}`;
			this.endpoint.rlog.error(msg);
			return this.toErrorResponse(404, msg);
		}
		const handler = service?.handlerMatching(parsed);
		if (!handler) {
			const msg = `No service found for URL: ${JSON.stringify(parsed)}`;
			this.endpoint.rlog.error(msg);
			return this.toErrorResponse(404, msg);
		}
		if (!request.body) {
			const msg = "The incoming message body was null";
			this.endpoint.rlog.error(msg);
			return this.toErrorResponse(400, msg);
		}
		return this.handleInvoke(service, handler, request.body, request.headers, request.extraArgs, request.abortSignal, 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 this.toErrorResponse(401, "Unauthorized");
		}
	}
	async handleInvoke(service, handler, body, headers, extraArgs, abortSignal, additionalContext) {
		const journalValueCodec = this.endpoint.journalValueCodec ? await this.endpoint.journalValueCodec : {
			encode: (entry) => entry,
			decode: (entry) => Promise.resolve(entry)
		};
		const loggerId = Math.floor(Math.random() * 4294967295);
		try {
			const vmHeaders = Object.entries(headers).filter(([, v]) => v !== void 0).map(([k, v]) => new WasmHeader(k, v instanceof Array ? v[0] : v));
			const coreVm = new WasmVM(vmHeaders, restateLogLevelToWasmLogLevel(DEFAULT_CONSOLE_LOGGER_LOG_LEVEL), loggerId, this.endpoint.journalValueCodec !== void 0);
			const responseHead = coreVm.get_response_head();
			const responseHeaders = responseHead.headers.reduce((headers$1, { key, value }) => ({
				[key]: value,
				...headers$1
			}), { "x-restate-server": X_RESTATE_SERVER });
			invocationLoggers.set(loggerId, createLogger(this.endpoint.loggerTransport, LogSource.JOURNAL, new LoggerContext(invocationIdFromHeaders(headers), service.name(), handler.name(), void 0, void 0, additionalContext)));
			const inputReader = body.getReader();
			abortSignal.addEventListener("abort", () => {
				invocationLoggers.delete(loggerId);
				inputReader.cancel();
			}, { once: true });
			while (!coreVm.is_ready_to_execute()) {
				const nextValue = await inputReader.read();
				if (nextValue.value !== void 0) coreVm.notify_input(nextValue.value);
				if (nextValue.done) {
					coreVm.notify_input_closed();
					break;
				}
			}
			const input = coreVm.sys_input();
			const invocationRequest = {
				id: input.invocation_id,
				headers: input.headers.reduce((headers$1, { key, value }) => {
					headers$1.set(key, value);
					return headers$1;
				}, /* @__PURE__ */ new Map()),
				attemptHeaders: Object.entries(headers).reduce((headers$1, [key, value]) => {
					if (value !== void 0) headers$1.set(key, value instanceof Array ? value[0] : value);
					return headers$1;
				}, /* @__PURE__ */ new Map()),
				body: input.input,
				extraArgs,
				attemptCompletedSignal: abortSignal
			};
			const loggerContext = new LoggerContext(input.invocation_id, handler.component().name(), handler.name(), handler.kind() === HandlerKind.SERVICE ? void 0 : input.key, invocationRequest, additionalContext);
			const ctxLogger = createLogger(this.endpoint.loggerTransport, LogSource.USER, loggerContext, () => !coreVm.is_processing());
			const vmLogger = createLogger(this.endpoint.loggerTransport, LogSource.JOURNAL, loggerContext);
			invocationLoggers.set(loggerId, vmLogger);
			if (!coreVm.is_processing()) vmLogger.info("Replaying invocation.");
			else vmLogger.info("Starting invocation.");
			const invocationEndPromise = new CompletablePromise();
			const responseTransformStream = new TransformStream();
			const outputWriter = responseTransformStream.writable.getWriter();
			const ctx = new ContextImpl(coreVm, input, ctxLogger, handler.kind(), vmLogger, invocationRequest, invocationEndPromise, inputReader, outputWriter, journalValueCodec, service.options?.serde, service.options?.asTerminalError);
			journalValueCodec.decode(input.input).catch((e) => Promise.reject(new TerminalError(`Failed to decode input using journal value codec: ${ensureError(e).message}`, { errorCode: 400 }))).then((decodedInput) => handler.invoke(ctx, decodedInput)).then((output) => {
				coreVm.sys_write_output_success(journalValueCodec.encode(output));
				coreVm.sys_end();
				vmLogger.info("Invocation completed successfully.");
			}).catch((e) => {
				const error = ensureError(e, service.options?.asTerminalError);
				logError(vmLogger, error);
				if (error instanceof TerminalError) {
					coreVm.sys_write_output_failure({
						code: error.code,
						message: error.message
					});
					coreVm.sys_end();
					return;
				}
				throw error;
			}).catch((e) => {
				const error = ensureError(e);
				if (error instanceof RetryableError) coreVm.notify_error_with_delay_override(error.message, error.stack, error.retryAfter !== void 0 ? BigInt(millisOrDurationToMillis(error.retryAfter)) : void 0);
				else coreVm.notify_error(error.message, error.stack);
			}).finally(() => {
				invocationEndPromise.resolve();
			});
			invocationEndPromise.promise.then(async () => {
				let nextOutput = coreVm.take_output();
				while (nextOutput !== null && nextOutput !== void 0) {
					await outputWriter.write(nextOutput);
					nextOutput = coreVm.take_output();
				}
				await outputWriter.close();
				inputReader.cancel().catch(() => {});
			}).finally(() => {
				invocationLoggers.delete(loggerId);
			}).catch(() => {});
			return {
				headers: responseHeaders,
				statusCode: responseHead.status_code,
				body: responseTransformStream.readable
			};
		} catch (error) {
			invocationLoggers.delete(loggerId);
			throw error;
		}
	}
	handleDiscovery(acceptVersionsString) {
		if (typeof acceptVersionsString !== "string") {
			const errorMessage = "Missing accept header";
			this.endpoint.rlog.warn(errorMessage);
			return this.toErrorResponse(415, errorMessage);
		}
		let manifestVersion;
		if (acceptVersionsString.includes(ENDPOINT_MANIFEST_V4)) manifestVersion = 4;
		else if (acceptVersionsString.includes(ENDPOINT_MANIFEST_V3)) manifestVersion = 3;
		else if (acceptVersionsString.includes(ENDPOINT_MANIFEST_V2)) manifestVersion = 2;
		else {
			const errorMessage = `Unsupported service discovery protocol version '${acceptVersionsString}'`;
			this.endpoint.rlog.warn(errorMessage);
			return this.toErrorResponse(415, errorMessage);
		}
		const discovery = {
			...this.endpoint.discoveryMetadata,
			...this.additionalDiscoveryFields,
			protocolMode: this.protocolMode
		};
		const checkUnsupportedFeature = (obj, ...fields) => {
			for (const field of fields) if (field in obj && obj[field] !== void 0) return this.toErrorResponse(500, `The code uses the new discovery feature '${String(field)}' but the runtime doesn't support it yet (discovery protocol negotiated version ${manifestVersion}). Either remove the usage of this feature, or upgrade the runtime.`);
		};
		if (manifestVersion < 3) for (const service of discovery.services) {
			const error = checkUnsupportedFeature(service, "journalRetention", "idempotencyRetention", "inactivityTimeout", "abortTimeout", "enableLazyState", "ingressPrivate");
			if (error !== void 0) return error;
			for (const handler of service.handlers) {
				const error$1 = checkUnsupportedFeature(handler, "journalRetention", "idempotencyRetention", "workflowCompletionRetention", "inactivityTimeout", "abortTimeout", "enableLazyState", "ingressPrivate");
				if (error$1 !== void 0) return error$1;
			}
		}
		if (manifestVersion < 4) {
			discovery.lambdaCompression = void 0;
			for (const service of discovery.services) {
				const error = checkUnsupportedFeature(service, "retryPolicyExponentiationFactor", "retryPolicyInitialInterval", "retryPolicyMaxAttempts", "retryPolicyMaxInterval", "retryPolicyOnMaxAttempts");
				if (error !== void 0) return error;
				for (const handler of service.handlers) {
					const error$1 = checkUnsupportedFeature(handler, "retryPolicyExponentiationFactor", "retryPolicyInitialInterval", "retryPolicyMaxAttempts", "retryPolicyMaxInterval", "retryPolicyOnMaxAttempts");
					if (error$1 !== void 0) return error$1;
				}
			}
		}
		const body = JSON.stringify(discovery);
		return {
			headers: {
				"content-type": manifestVersion === 2 ? ENDPOINT_MANIFEST_V2 : manifestVersion === 3 ? ENDPOINT_MANIFEST_V3 : ENDPOINT_MANIFEST_V4,
				"x-restate-server": X_RESTATE_SERVER
			},
			statusCode: 200,
			body: OnceStream(new TextEncoder().encode(body))
		};
	}
	toErrorResponse(code, message) {
		return {
			headers: {
				"content-type": "application/json",
				"x-restate-server": X_RESTATE_SERVER
			},
			statusCode: code,
			body: OnceStream(new TextEncoder().encode(JSON.stringify({ message })))
		};
	}
};
const invocationLoggers = /* @__PURE__ */ new Map();
const logsTextDecoder = new TextDecoder("utf-8", { fatal: false });
/**
* The shared core propagates logs to the SDK invoking this method.
* When possible it provides an invocationId, which is used to access the registered invocationLoggers, that should contain the logger per invocation id.
*/
function vm_log(level, strBytes, loggerId) {
	try {
		const logger = loggerId && invocationLoggers.get(loggerId) || void 0;
		const str = logsTextDecoder.decode(strBytes);
		if (logger !== void 0) logger.logForLevel(wasmLogLevelToRestateLogLevel(level), str);
		else defaultLoggerTransport({
			level: wasmLogLevelToRestateLogLevel(level),
			replaying: false,
			source: LogSource.JOURNAL
		}, str);
	} catch (e) {
		defaultLoggerTransport({
			level: RestateLogLevel.ERROR,
			replaying: false,
			source: LogSource.SYSTEM
		}, "Unexpected error thrown while trying to log: " + e?.toString());
	}
}
function wasmLogLevelToRestateLogLevel(level) {
	switch (level) {
		case LogLevel.TRACE: return RestateLogLevel.TRACE;
		case LogLevel.DEBUG: return RestateLogLevel.DEBUG;
		case LogLevel.INFO: return RestateLogLevel.INFO;
		case LogLevel.WARN: return RestateLogLevel.WARN;
		case LogLevel.ERROR: return RestateLogLevel.ERROR;
	}
}
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 { GenericHandler, tryCreateContextualLogger, vm_log };
//# sourceMappingURL=generic.js.map