import * as envoyProtocol from "@rivetkit/engine-envoy-protocol";
import type { Context as HonoContext, Next } from "hono";
import type { ActorSpecifier } from "@/actor/errors";
import {
	HEADER_ACTOR_GENERATION,
	HEADER_ACTOR_ID,
	HEADER_ACTOR_KEY,
} from "@/common/actor-router-consts";
import type * as protocol from "@/common/client-protocol";
import {
	CURRENT_VERSION as CLIENT_PROTOCOL_CURRENT_VERSION,
	HTTP_RESPONSE_ERROR_VERSIONED,
} from "@/common/client-protocol-versioned";
import {
	type HttpResponseError as HttpResponseErrorJson,
	HttpResponseErrorSchema,
} from "@/common/client-protocol-zod";
import type { Encoding, JsonCompatValue } from "@/common/encoding";
import {
	getRequestEncoding,
	getRequestExposeInternalError,
} from "@/common/router-request";
import { buildActorNames, type RegistryConfig } from "@/registry/config";
import { encodeCborCompat, serializeWithEncoding } from "@/serde";
import { bufferToArrayBuffer, VERSION } from "@/utils";
import { getLogHeaders } from "@/utils/env-vars";
import { getLogger, type Logger } from "./log";
import { deconstructError } from "./utils";

export function logger() {
	return getLogger("router");
}

export function loggerMiddleware(logger: Logger) {
	return async (c: HonoContext, next: Next) => {
		const method = c.req.method;
		const path = c.req.path;
		const startTime = Date.now();

		await next();

		const duration = Date.now() - startTime;
		logger.debug({
			msg: "http request",
			method,
			path,
			status: c.res.status,
			dt: `${duration}ms`,
			reqSize: c.req.header("content-length"),
			resSize: c.res.headers.get("content-length"),
			userAgent: c.req.header("user-agent"),
			...(getLogHeaders()
				? { allHeaders: JSON.stringify(c.req.header()) }
				: {}),
		});
	};
}

export function handleRouteNotFound(c: HonoContext) {
	return c.text("Not Found (RivetKit)", 404);
}

export function handleRouteError(error: unknown, c: HonoContext) {
	const exposeInternalError = getRequestExposeInternalError(c.req.raw);

	const { statusCode, group, code, message, metadata, actor } =
		deconstructError(error, exposeInternalError);

	if (actor) {
		logger().warn({
			msg: "actor http error response",
			actorId: actor.actorId,
			generation: actor.generation,
			actorKey: actor.key,
			group,
			code,
		});
	}

	let encoding: Encoding;
	try {
		encoding = getRequestEncoding(c.req);
	} catch (_) {
		encoding = "json";
	}

	const errorData = { group, code, message, metadata, actor };
	const output = serializeWithEncoding(
		encoding,
		errorData,
		HTTP_RESPONSE_ERROR_VERSIONED,
		CLIENT_PROTOCOL_CURRENT_VERSION,
		HttpResponseErrorSchema,
		// JSON: metadata is the raw value (will be serialized by jsonStringifyCompat)
		(value): HttpResponseErrorJson => ({
			group: value.group,
			code: value.code,
			message: value.message,
			metadata: value.metadata,
			actor: value.actor,
		}),
		// BARE/CBOR: metadata needs to be CBOR-encoded to ArrayBuffer
		(value): protocol.HttpResponseError => ({
			group: value.group,
			code: value.code,
			message: value.message,
			metadata: value.metadata
				? bufferToArrayBuffer(
						encodeCborCompat(value.metadata as JsonCompatValue),
					)
				: null,
			actor: value.actor
				? {
						actorId: value.actor.actorId,
						generation: BigInt(value.actor.generation),
						key: value.actor.key ?? null,
					}
				: null,
		}),
	);

	const headers = actor ? actorResponseHeaders(actor) : undefined;

	// TODO: Remove any
	return c.body(output as any, { status: statusCode, headers });
}

function actorResponseHeaders(actor: ActorSpecifier): Record<string, string> {
	return {
		[HEADER_ACTOR_ID]: actor.actorId,
		[HEADER_ACTOR_GENERATION]: String(actor.generation),
		...(actor.key ? { [HEADER_ACTOR_KEY]: actor.key } : {}),
	};
}

export type MetadataEnvoyKind =
	| { serverless: Record<never, never> }
	| { normal: Record<never, never> };

/**
 * Metadata response interface for the /metadata endpoint
 */
export interface MetadataResponse {
	runtime: string;
	version: string;
	envoy?: {
		kind: MetadataEnvoyKind;
		version?: number;
	};
	envoyProtocolVersion: number;
	actorNames: ReturnType<typeof buildActorNames>;
	/**
	 * Endpoint that the client should connect to to access this envoy.
	 *
	 * If defined, will override the endpoint the user has configured on startup.
	 *
	 * This is helpful if attempting to connect to a serverless envoy, so the serverless runner can define where the main endpoint lives.
	 *
	 * This is also helpful for setting up clean redirects as needed.
	 **/
	clientEndpoint?: string;
	/**
	 * Namespace that the client should use when connecting.
	 **/
	clientNamespace?: string;
	/**
	 * Token that the client should use when connecting.
	 **/
	clientToken?: string;
}

export function handleMetadataRequest(
	c: HonoContext,
	config: RegistryConfig,
	envoyKind: MetadataEnvoyKind,
	clientEndpoint: string | undefined,
	clientNamespace: string | undefined,
	clientToken: string | undefined,
) {
	const response: MetadataResponse = {
		runtime: "rivetkit",
		version: VERSION,
		envoy: {
			kind: envoyKind,
			version: config.envoy.version,
		},
		envoyProtocolVersion: envoyProtocol.VERSION,
		actorNames: buildActorNames(config),
		clientEndpoint,
		clientNamespace,
		clientToken,
	};

	return c.json(response);
}

export function handleHealthRequest(c: HonoContext) {
	return c.json({
		status: "ok",
		runtime: "rivetkit",
		version: VERSION,
	});
}
