import * as crypto from "node:crypto";
import { URL } from "node:url";

type Request = {
	url: string;
	method: string;
	headers: Record<string, string>;
};

type PrivateKey = {
	privateKeyPem: string;
	keyId: string;
};

export function createSignedPost(args: {
	key: PrivateKey;
	url: string;
	body: string;
	additionalHeaders: Record<string, string>;
}) {
	const u = new URL(args.url);
	const digestHeader = `SHA-256=${crypto
		.createHash("sha256")
		.update(args.body)
		.digest("base64")}`;

	const request: Request = {
		url: u.href,
		method: "POST",
		headers: objectAssignWithLcKey(
			{
				Date: new Date().toUTCString(),
				Host: u.hostname,
				"Content-Type": "application/activity+json",
				Digest: digestHeader,
			},
			args.additionalHeaders,
		),
	};

	const result = signToRequest(request, args.key, [
		"(request-target)",
		"date",
		"host",
		"digest",
	]);

	return {
		request,
		signingString: result.signingString,
		signature: result.signature,
		signatureHeader: result.signatureHeader,
	};
}

export function createSignedGet(args: {
	key: PrivateKey;
	url: string;
	additionalHeaders: Record<string, string>;
}) {
	const u = new URL(args.url);

	const request: Request = {
		url: u.href,
		method: "GET",
		headers: objectAssignWithLcKey(
			{
				Accept: "application/activity+json, application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
				Date: new Date().toUTCString(),
				Host: new URL(args.url).hostname,
			},
			args.additionalHeaders,
		),
	};

	const result = signToRequest(request, args.key, [
		"(request-target)",
		"date",
		"host",
		"accept",
	]);

	return {
		request,
		signingString: result.signingString,
		signature: result.signature,
		signatureHeader: result.signatureHeader,
	};
}

function signToRequest(
	request: Request,
	key: PrivateKey,
	includeHeaders: string[],
) {
	const signingString = genSigningString(request, includeHeaders);
	const signature = crypto
		.sign("sha256", Buffer.from(signingString), key.privateKeyPem)
		.toString("base64");
	const signatureHeader = `keyId="${
		key.keyId
	}",algorithm="rsa-sha256",headers="${includeHeaders.join(
		" ",
	)}",signature="${signature}"`;

	request.headers = objectAssignWithLcKey(request.headers, {
		Signature: signatureHeader,
	});

	return {
		request,
		signingString,
		signature,
		signatureHeader,
	};
}

function genSigningString(request: Request, includeHeaders: string[]) {
	request.headers = lcObjectKey(request.headers);

	const results: string[] = [];

	for (const key of includeHeaders.map((x) => x.toLowerCase())) {
		if (key === "(request-target)") {
			results.push(
				`(request-target): ${request.method.toLowerCase()} ${
					new URL(request.url).pathname
				}`,
			);
		} else {
			results.push(`${key}: ${request.headers[key]}`);
		}
	}

	return results.join("\n");
}

function lcObjectKey(src: Record<string, string>) {
	const dst: Record<string, string> = {};
	for (const key of Object.keys(src).filter(
		(x) => x !== "__proto__" && typeof src[x] === "string",
	))
		dst[key.toLowerCase()] = src[key];
	return dst;
}

function objectAssignWithLcKey(
	a: Record<string, string>,
	b: Record<string, string>,
) {
	return Object.assign(lcObjectKey(a), lcObjectKey(b));
}
