#!/usr/bin/env node
require("dotenv").config()
require("sucrase/register")

const path = require("node:path")
const Module = require("node:module")
const { Buffer } = require("node:buffer")
const { webcrypto: crypto } = require("node:crypto")
const { InfisicalClient } = require("@infisical/sdk")
const moduleAlias = require("module-alias")
const { onExit } = require("signal-exit")
const opentelemetry = require("@opentelemetry/sdk-node")
const {
	getNodeAutoInstrumentations,
} = require("@opentelemetry/auto-instrumentations-node")
const { OTLPTraceExporter } = require("@opentelemetry/exporter-trace-otlp-http")
const { OTLPLogExporter } = require("@opentelemetry/exporter-logs-otlp-http")
const { Resource } = require("@opentelemetry/resources")
const {
	SemanticResourceAttributes,
} = require("@opentelemetry/semantic-conventions")

// Override file execution arg
process.argv.splice(1, 1)
process.argv[1] = path.resolve(process.argv[1])

// Expose boot function to global
global.Boot = Boot
global.isProduction = process.env.NODE_ENV === "production"

global["__root"] = path.resolve(process.cwd())
global["__src"] = path.resolve(
	globalThis["__root"],
	path.dirname(process.argv[1]),
)

global["aliases"] = {
	root: global["__root"],
	src: global["__src"],

	// expose shared resources
	"@db_models": path.resolve(__dirname, "db_models"),
	"@shared-utils": path.resolve(__dirname, "utils"),
	"@shared-classes": path.resolve(__dirname, "classes"),
	"@shared-lib": path.resolve(__dirname, "lib"),
	"@shared-middlewares": path.resolve(__dirname, "middlewares"),

	// expose internal resources
	"@lib": path.resolve(global["__src"], "lib"),
	"@middlewares": path.resolve(global["__src"], "middlewares"),
	"@controllers": path.resolve(global["__src"], "controllers"),
	"@config": path.resolve(global["__src"], "config"),
	"@shared": path.resolve(global["__src"], "shared"),
	"@classes": path.resolve(global["__src"], "classes"),
	"@models": path.resolve(global["__src"], "models"),
	"@services": path.resolve(global["__src"], "services"),
	"@utils": path.resolve(global["__src"], "utils"),
}

function registerBaseAliases(fromPath, customAliases = {}) {
	if (typeof fromPath === "undefined") {
		if (module.parent.filename.includes("dist")) {
			fromPath = path.resolve(process.cwd(), "dist")
		} else {
			fromPath = path.resolve(process.cwd(), "src")
		}
	}

	moduleAlias.addAliases({
		...customAliases,
		"@controllers": path.resolve(fromPath, "controllers"),
		"@middlewares": path.resolve(fromPath, "middlewares"),
		"@models": path.resolve(fromPath, "models"),
		"@classes": path.resolve(fromPath, "classes"),
		"@lib": path.resolve(fromPath, "lib"),
		"@utils": path.resolve(fromPath, "utils"),
	})
}

function registerPatches() {
	global.b64Decode = (data) => {
		return Buffer.from(data, "base64").toString("utf-8")
	}
	global.b64Encode = (data) => {
		return Buffer.from(data, "utf-8").toString("base64")
	}

	global.nanoid = (t = 21) =>
		crypto
			.getRandomValues(new Uint8Array(t))
			.reduce(
				(t, e) =>
					(t +=
						(e &= 63) < 36
							? e.toString(36)
							: e < 62
								? (e - 26).toString(36).toUpperCase()
								: e > 62
									? "-"
									: "_"),
				"",
			)

	Array.prototype.updateFromObjectKeys = function (obj) {
		this.forEach((value, index) => {
			if (obj[value] !== undefined) {
				this[index] = obj[value]
			}
		})

		return this
	}

	global.ToBoolean = (value) => {
		if (typeof value === "boolean") {
			return value
		}

		if (typeof value === "string") {
			return value.toLowerCase() === "true"
		}

		return false
	}
}

function registerAliases() {
	registerBaseAliases(global["__src"], global["aliases"])
}

async function injectEnvFromInfisical() {
	const envMode = (global.FORCE_ENV ?? global.isProduction) ? "prod" : "dev"

	console.log(
		`[BOOT] 🔑 Injecting env variables from INFISICAL in [${envMode}] mode...`,
	)

	const client = new InfisicalClient({
		auth: {
			universalAuth: {
				clientId: process.env.INFISICAL_CLIENT_ID,
				clientSecret: process.env.INFISICAL_CLIENT_SECRET,
			},
		},
	})

	const secrets = await client.listSecrets({
		environment: envMode,
		path: process.env.INFISICAL_PATH ?? "/",
		projectId: process.env.INFISICAL_PROJECT_ID ?? null,
		includeImports: false,
	})

	//inject to process.env
	secrets.forEach((secret) => {
		if (!process.env[secret.secretKey]) {
			process.env[secret.secretKey] = secret.secretValue
		}
	})
}

async function Boot(main) {
	if (!main) {
		throw new Error("main class is not defined")
	}

	const service_id = process.env.lb_service.id

	console.log(
		`[BOOT] Booting (${service_id}) in [${global.isProduction ? "production" : "development"}] mode...`,
	)

	const traceExporter = new OTLPTraceExporter({
		url:
			process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ??
			"http://fr02.ragestudio.net:4318/v1/traces",
	})
	const logExporter = new OTLPLogExporter({
		url:
			process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT ??
			"http://fr02.ragestudio.net:4318/v1/logs",
	})

	const sdk = new opentelemetry.NodeSDK({
		traceExporter,
		logExporter,
		instrumentations: [getNodeAutoInstrumentations()],
		resource: new Resource({
			[SemanticResourceAttributes.SERVICE_NAME]: service_id ?? "node_app",
		}),
	})

	sdk.start()

	if (
		process.env.INFISICAL_CLIENT_ID &&
		process.env.INFISICAL_CLIENT_SECRET
	) {
		console.log(
			`[BOOT] INFISICAL Credentials found, injecting env variables from INFISICAL...`,
		)
		await injectEnvFromInfisical()
	}

	const instance = new main()

	onExit(
		(code, signal) => {
			console.log(`[BOOT] Cleaning up...`)

			sdk.shutdown()
				.then(() => console.log("Tracing terminated"))
				.catch((error) =>
					console.log("Error terminating tracing", error),
				)

			if (typeof instance.onClose === "function") {
				instance.onClose()
			}

			instance.engine.close()
		},
		{
			alwaysLast: true,
		},
	)

	await instance.initialize()

	if (process.env.lb_service && process.send) {
		process.send({
			status: "ready",
		})
	}

	return instance
}

try {
	// Apply patches
	registerPatches()

	// Apply aliases
	registerAliases()

	// execute main
	Module.runMain()
} catch (error) {
	console.error("[BOOT] ❌ Boot error: ", error)
}
