import * as functions from "firebase-functions";
import { initializeApp } from "firebase-admin/app";
import { getStorage } from "firebase-admin/storage";
import passkit from "passkit-generator";
import type { Barcode, TransitType } from "passkit-generator";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";

// Please note this is experimental in NodeJS as
// it is marked as Stage 3 in TC39
// Should probably not be used in production
import startData from "./startData.json" assert { type: "json" };

const PKPass = passkit.PKPass;

interface RequestWithBody extends functions.Request {
	body: {
		passModel: string;
		serialNumber: string;
		logoText: string;
		textColor: string;
		backgroundColor: string;
		labelColor: string;
		relevantDate?: string;
		expiryDate?: string;
		relevantLocationLat?: number;
		relevantLocationLong?: number;
		header?: { value: string; label: string }[];
		primary?: { value: string; label: string }[];
		secondary?: { value: string; label: string }[];
		auxiliary?: { value: string; label: string }[];
		codeAlt?: string;
		qrText?: string;
		transitType?: TransitType;
		codeType?: Barcode["format"];
		thumbnailFile?: string;
		logoFile?: string;
	};
}

/**
 * Declaring our .env contents
 * @see https://firebase.google.com/docs/functions/config-env?gen=2nd#deploying_multiple_sets_of_environment_variables
 */

declare global {
	namespace NodeJS {
		interface ProcessEnv {
			WWDR: string;
			SIGNER_CERT: string;
			SIGNER_KEY: string;
			SIGNER_KEY_PASSPHRASE: string;

			// reserved, but we use it to discriminate emulator from deploy
			FUNCTIONS_EMULATOR: "true" | "false" | undefined;
		}
	}
}

/**
 * @see https://firebase.google.com/docs/storage/admin/start#node.js
 */

initializeApp({
	storageBucket: startData.FIREBASE_BUCKET_ADDR,
});

const storageRef = getStorage().bucket();

export const pass = functions.https.onRequest(
	async (request: RequestWithBody, response) => {
		let modelBasePath: string;

		if (process.env.FUNCTIONS_EMULATOR === "true") {
			modelBasePath = "../../models/";
		} else {
			/**
			 * Models are cloned on deploy through
			 * the commands in `firebase.json` and
			 * are uploaded along with our program.
			 *
			 * When deployed, root folder is the `functions` folder
			 */
			modelBasePath = "./models/";
		}

		try {
			if (request.headers["content-type"] !== "application/json") {
				response.status(400);
				response.send({
					error: `Payload with content-type ${request.headers["content-type"]} is not supported. Use "application/json"`,
				});
				return;
			}

			if (!request.body.passModel) {
				response.status(400);
				response.send({
					error: "Unspecified 'passModel' parameter: which model should be used?",
				});

				return;
			}

			if (request.body.passModel.endsWith(".pass")) {
				request.body.passModel = request.body.passModel.replace(
					".pass",
					"",
				);
			}

			const newPass = await PKPass.from(
				{
					/**
					 * Get relevant pass model from model folder (see passkit-generator/examples/models/)
					 * Path seems to get read like the function is in "firebase/" folder and not in "firebase/functions/"
					 */
					model: `${modelBasePath}${request.body.passModel}.pass`,
					certificates: {
						// Assigning certificates from certs folder (you will need to provide these yourself)
						wwdr: process.env.WWDR,
						signerCert: process.env.SIGNER_CERT,
						signerKey: process.env.SIGNER_KEY,
						signerKeyPassphrase: process.env.SIGNER_KEY_PASSPHRASE,
					},
				},
				{
					serialNumber: request.body.serialNumber,
					description: "DESCRIPTION",
					logoText: request.body.logoText,
					foregroundColor: request.body.textColor,
					backgroundColor: request.body.backgroundColor,
					labelColor: request.body.labelColor,
				},
			);

			if (newPass.type == "boardingPass") {
				if (!request.body.transitType) {
					response.status(400);
					response.send({
						error: "transitType is required",
					});

					return;
				}

				newPass.transitType = request.body.transitType;
			}

			if (typeof request.body.relevantDate === "string") {
				newPass.setRelevantDate(new Date(request.body.relevantDate));
			}

			if (typeof request.body.expiryDate === "string") {
				newPass.setExpirationDate(new Date(request.body.expiryDate));
			}

			if (
				request.body.relevantLocationLat &&
				request.body.relevantLocationLong
			) {
				newPass.setLocations({
					latitude: request.body.relevantLocationLat,
					longitude: request.body.relevantLocationLong,
				});
			}

			if (Array.isArray(request.body.header)) {
				for (let i = 0; i < request.body.header.length; i++) {
					const field = request.body.header[i];

					if (!(field?.label && field.value)) {
						continue;
					}

					newPass.headerFields.push({
						key: `header${i}`,
						label: field.label,
						value: field.value,
					});
				}
			}

			if (Array.isArray(request.body.primary)) {
				for (let i = 0; i < request.body.primary.length; i++) {
					const field = request.body.primary[i];

					if (!(field?.label && field.value)) {
						continue;
					}

					newPass.primaryFields.push({
						key: `primary${i}`,
						label: field.label,
						value:
							newPass.type == "boardingPass"
								? field.value.toUpperCase()
								: field.value,
					});
				}
			}

			if (Array.isArray(request.body.secondary)) {
				for (let i = 0; i < request.body.secondary.length; i++) {
					const field = request.body.secondary[i];

					if (!(field?.label && field.value)) {
						continue;
					}

					const isElementInLastTwoPositions =
						i === request.body.secondary.length - 2 ||
						i === request.body.secondary.length - 1;

					newPass.secondaryFields.push({
						key: `secondary${i}`,
						label: field.label,
						value: field.value,
						textAlignment: isElementInLastTwoPositions
							? "PKTextAlignmentRight"
							: "PKTextAlignmentLeft",
					});
				}
			}

			if (Array.isArray(request.body.auxiliary)) {
				for (let i = 0; i < request.body.auxiliary.length; i++) {
					const field = request.body.auxiliary[i];

					if (!(field?.label && field.value)) {
						continue;
					}

					const isElementInLastTwoPositions =
						i === request.body.auxiliary.length - 2 ||
						i === request.body.auxiliary.length - 1;

					newPass.auxiliaryFields.push({
						key: `auxiliary${i}`,
						label: field.label,
						value: field.value,
						textAlignment: isElementInLastTwoPositions
							? "PKTextAlignmentRight"
							: "PKTextAlignmentLeft",
					});
				}
			}

			if (request.body.qrText && request.body.codeType) {
				newPass.setBarcodes({
					message: request.body.qrText,
					format: request.body.codeType,
					messageEncoding: "iso-8859-1",
					altText: request.body.codeAlt?.trim() ?? "",
				});
			}

			const { thumbnailFile, logoFile } = request.body;

			// Downloading thumbnail and logo files from Firebase Storage and adding to pass
			if (newPass.type == "generic" || newPass.type == "eventTicket") {
				if (thumbnailFile) {
					const tempPath1 = path.join(os.tmpdir(), thumbnailFile);

					try {
						await storageRef
							.file(`thumbnails/${thumbnailFile}`)
							.download({ destination: tempPath1 });

						const buffer = fs.readFileSync(tempPath1);

						newPass.addBuffer("thumbnail.png", buffer);
						newPass.addBuffer("thumbnail@2x.png", buffer);
					} catch (error) {
						console.error(error);
					}
				}
			}

			if (logoFile) {
				const tempPath2 = path.join(os.tmpdir(), logoFile);

				try {
					await storageRef
						.file(`logos/${logoFile}`)
						.download({ destination: tempPath2 });

					const buffer = fs.readFileSync(tempPath2);

					newPass.addBuffer("logo.png", buffer);
					newPass.addBuffer("logo@2x.png", buffer);
				} catch (error) {
					console.error(error);
				}
			}

			const bufferData = newPass.getAsBuffer();

			response.set("Content-Type", newPass.mimeType);
			response.status(200).send(bufferData);
		} catch (error) {
			console.log("Error Uploading pass " + error);

			const err = Object.assign(
				{},
				...Object.entries(Object.getOwnPropertyDescriptors(error)).map(
					([key, descriptor]) => {
						return { [key]: descriptor.value };
					},
				),
			);

			response.status(500);
			response.send({
				error: err,
			});
		}
	},
);
