import * as path from "path";
import * as grpc from "@grpc/grpc-js";
import * as protoLoader from "@grpc/proto-loader";
import { IronPdfServiceClient } from "./generated_proto/ironpdfengineproto/IronPdfService";
import * as cp from "child_process";
import * as Os from "os";
import * as fs from "fs";
import { glob } from "glob";
import * as https from "https";
import * as unzipper from "unzipper";
import { spawn } from "child_process";
import * as net from "net";
import * as util from "util";
import {ProtoGrpcType} from "./generated_proto/iron_pdf_service";
import {IronPdfGlobalConfig} from "../public/ironpdfglobalconfig";
import {handshakeWithRetry} from "./grpc_layer/handshake";
import {setIsDebug, setLicenseKey} from "./grpc_layer/system";

export class Access {
	private static _instance: Access;

	private constructor() {
		if (Access._instance) {
			throw new Error(
				"Error: Instantiation failed: Use Access.getInstance() instead of new."
			);
		}
		Access._instance = this;
	}

	public static get Instance() {
		return this._instance || (this._instance = new this());
	}

	public static forceShutdown() {
		if(this.ironPdfEngineProcess)
			this.ironPdfEngineProcess?.kill();
	}

	public static usedDocumentIds = new Set<string>();
	private static PROTO_FILE =
		"IronPdfEngine.ProtoFiles/iron_pdf_service.proto";
	private static packageDef = protoLoader.loadSync(
		path.resolve(__dirname, this.PROTO_FILE)
	);
	private static grpcObj = grpc.loadPackageDefinition(
		this.packageDef
	) as unknown as ProtoGrpcType;
	private static client: IronPdfServiceClient;
	private static targetDir: string = path.join(
		__dirname,
		`../../ironpdf-engine-bin-${IronPdfGlobalConfig.ironPdfEngineVersion}`
	);

	public static ironPdfEngineAddress = `127.0.0.1:33350`;

	private static ironPdfEngineProcess: cp.ChildProcess;

	private static downloadFromCDN(): Promise<void> {
		return new Promise<void>((resolve, reject) => {
			let redirectCount = 0;
			const zipFilePath = "./ironPdfEngineDownload.zip";
			const downloadZip = (url: string) => {
				https
					.get(url, (response) => {
						if (
							response.statusCode === 302 ||
							response.statusCode === 301
						) {
							if (redirectCount >= 5) {
								reject("Too many redirects");
								return;
							}
							redirectCount++;
							const redirectUrl: string | undefined =
								response.headers.location;
							if (!redirectUrl) {
								reject(
									`Invalid redirect URL code: ${response.statusCode} : ${redirectUrl}`
								);
							}
							downloadZip(redirectUrl!);
							return;
						}
						if (response.statusCode !== 200) {
							reject(
								`Invalid status code: ${response.statusCode}`
							);
							return;
						}
						const totalLength: number = parseInt(
							response.headers["content-length"]!,
							10
						);
						let downloadedLength = 0;
						const zipFile = fs.createWriteStream(zipFilePath);

						let lastLoggedPercent = 0;

						response.on("data", (data) => {
							downloadedLength += data.length;
							const percent =
								Math.floor(
									((downloadedLength / totalLength) * 100) /
										10
								) * 10;

							if (percent > lastLoggedPercent && percent < 100) {
								console.debug(
									`Download IronPdfEngine progress: ${percent}%`
								);
								lastLoggedPercent = percent;
							}
							zipFile.write(data);
						});

						response.on("end", () => {
							console?.log(`Download IronPdfEngine complete`);
							zipFile.end();
						});

						zipFile.on("finish", () => {
							console?.log(
								`Extract IronPdfEngine Zip to ${this.targetDir}`
							);
							const readStream = fs.createReadStream(zipFilePath);
							readStream.on("open", () => {
								readStream
									.pipe(
										unzipper.Extract({
											path: this.targetDir,
										})
									)
									.on("close", () => {
										try {
											fs.unlinkSync(zipFilePath);
										} catch (e) {}
										resolve();
									})
									.on("error", (error) => {
										reject(
											`Error extracting ZIP file: ${error}`
										);
									});
							});
							readStream.on("error", (error) => {
								reject(`Error reading ZIP file: ${error}`);
							});
						});

						response.on("error", (error) => {
							reject(`Error downloading ZIP file: ${error}`);
						});
					})
					.on("error", (error) => {
						reject(`Error downloading ZIP file: ${error}`);
					});
			};

			const zipUrl = `https://ironpdfengine.azurewebsites.net/api/IronPdfEngineDownload?version=${
				IronPdfGlobalConfig.ironPdfEngineVersion
			}&platform=${getPlatformName()}&architect=${getOsArch()}`;

			console.debug("Download IronPdfEngine");
			downloadZip(zipUrl);
		});
	}

	private static async tryDeleteUnusedEngineBin(
		baseDir: string,
		excludeFolder: string
	): Promise<void> {
		try {
			//const folders = await glob(path.join(baseDir, 'ironpdf-engine-bin-*'));
			const wc = path
				.join(baseDir, "ironpdfenginebin*")
				.replace(/\\/g, "/");
			const folders = await glob.glob(wc, { absolute: true });
			// exclude the folder you don't want to delete
			const foldersToDelete = folders.filter(
				(folder) => path.basename(folder) !== excludeFolder
			);

			// delete all other folders
			await Promise.all(
				foldersToDelete.map((folder) =>
					fs.promises.rmdir(folder, { recursive: true })
				)
			);
		} catch (err) {}
	}

	private static async getAvailableIronPdfEngineFile() {
		let dir;
		try {
			const ironPdfEnginePackageName = `@ironsoftware/ironpdf-engine-${getOsName()}-${getOsArch()}`;
			// eslint-disable-next-line @typescript-eslint/no-var-requires
			const ironPdfEnginePackage = require(ironPdfEnginePackageName);
			dir = `${ironPdfEnginePackage.dir}${path.sep}ironpdf-engine-bin-${ironPdfEnginePackage.version}`;
			console.debug(
				`FOUND ${ironPdfEnginePackageName}:${ironPdfEnginePackage.version} at:${dir}`
			);
		} catch (e) {
			//NOT FOUND ironpdf-engine-windows-x64, ignore

			//if files exists Locally
			const isLocalFilesExists = fs.existsSync(
				`${this.targetDir}${path.sep}${ironPdfEngineExecutable()}`
			);
			if (!isLocalFilesExists) {
				await this.downloadFromCDN();
			}
			dir = this.targetDir;
		}
		await tryChangePermissions(dir, "777");
		return `${dir}${path.sep}${ironPdfEngineExecutable()}`;
	}

	private static async startServer() {
		const config = IronPdfGlobalConfig.getConfig();
		if (config.debugMode) console.debug("Start IronPdfEngine");

		const ironPdfEngineBinPath = await this.getAvailableIronPdfEngineFile();

		if (config.debugMode)
			console.debug(`IronPdfEngine bin: ${ironPdfEngineBinPath}`);

		let host = "localhost";
		let port = "33350";
		if (config.ironPdfEngineAddress) {
			const splitter = config.ironPdfEngineAddress.lastIndexOf(":");
			host = config.ironPdfEngineAddress.substring(0, splitter);
			port = config.ironPdfEngineAddress.substring(splitter + 1);
			this.ironPdfEngineAddress = config.ironPdfEngineAddress;
		}

		const args = [
			`host=${host}`,
			`port=${port}`,
			`docker_build=false`,
			`keep_alive=true`,
			`linux_and_docker_auto_config=false`,
			`skip_initialization=false`,
			`single_process=${config.singleProcess ?? getOsName() == "macos"}`,
			`chrome_browser_limit=${config.chromeBrowserLimit??"30"}`,
			`chrome_gpu_mode=${config.chromeGpuMode??0}`,
			`linux_and_docker_auto_config=${config.autoInstallDependency??"true"}`,
			`programming_language=nodejs`
		];

		if (config.debugMode) {
			args.push(`enable_debug=true`)
			args.push(`log_path=./IronPdfEngine.log`)
		} else {
			args.push(`enable_debug=false`)
		}

		if(config.chromeBrowserCachePath){
			args.push(`chrome_cache_path=${config.chromeBrowserCachePath}`)
		}

		if (config.debugMode){
			console.debug("args:"+JSON.stringify(args))
		}

		const ironPdfEngineProcess: cp.ChildProcess = spawn(
			`${ironPdfEngineBinPath}`,
			args,
			{
				detached: false,
				stdio: IronPdfGlobalConfig.getConfig().debugMode
					? ["ignore"]
					: "ignore",
			}
		)
			.on("error", (err) =>
				console?.debug(`spawn IRON_PDF_ENGINE error: ${err}`)
			)
			.on("message", (err) =>
				console?.debug(`spawn IRON_PDF_ENGINE message: ${err}`)
			);

		this.ironPdfEngineProcess = ironPdfEngineProcess;

		if (IronPdfGlobalConfig.getConfig().debugMode) {
			this.ironPdfEngineProcess.stdout?.on("data", (data) => {
				console?.debug(`[IRON_PDF_ENGINE] ${data}`);
			});
		}

		process.on("exit", function () {
			ironPdfEngineProcess.kill();
		});

		process.on("beforeExit", function () {
			ironPdfEngineProcess.kill();
		});

		process.on("disconnect", function () {
			ironPdfEngineProcess.kill();
		});

		process.on("SIGINT", () => {
			ironPdfEngineProcess.kill();
		});

		process.on("SIGTERM", () => {
			ironPdfEngineProcess.kill();
		});

		this.ironPdfEngineProcess.unref();

		Access.tryDeleteUnusedEngineBin(
			path.join(__dirname, `../..`),
			`ironpdf-engine-bin-${IronPdfGlobalConfig.ironPdfEngineVersion}`
		).then();

		if (IronPdfGlobalConfig.getConfig().debugMode)
			console.debug("wait for IronPdfEngine to start up ");

		await this.waitUntilPortIsOpen(+port);
		await new Promise((resolve) => setTimeout(resolve, 10000));
	}

	private static async checkPort(port: number) {
		return new Promise((resolve, reject) => {
			const server = net
				.createServer()
				.once("error", (err) => {
					if (err.name !== "EADDRINUSE") reject(err);
				})
				.once("listening", () => {
					server.close();
					resolve(null);
				})
				.listen(port);
		});
	}

	private static async waitUntilPortIsOpen(port: number) {
		while (true) {
			try {
				await this.checkPort(port);
				break;
			} catch (err) {
				await new Promise((resolve) => setTimeout(resolve, 1000));
			}
		}
	}

	public static async ensureConnection(): Promise<IronPdfServiceClient> {
		if (!this.client) {
			if (!IronPdfGlobalConfig.getConfig().ironPdfEngineDockerAddress) {
				//local mode (non-docker)
				await this.startServer();
			}else{
				this.ironPdfEngineAddress = IronPdfGlobalConfig.getConfig().ironPdfEngineDockerAddress!
			}

			for (let i = 0; i < 5; i++) {
				try {
					this.client = new this.grpcObj.ironpdfengineproto.IronPdfService(
						this.ironPdfEngineAddress,
						grpc.credentials.createInsecure()
				);
				break;
				} catch (e) {
					if(IronPdfGlobalConfig.getConfig().debugMode)
						console.error(`Attempt ${i+1} to connect to IronPdfEngine Retrying...`);
					await new Promise(r => setTimeout(r, 2000)); // wait for 2 seconds before next try
				}
			}

			const response = await handshakeWithRetry(this.client, 20).catch(
				async (reason) => {
					throw new Error(
						`Cannot connect to IronPdfEngine: ${reason}`
					);
				}
			);
			if (response) {
				if (response.exception) {
					throw new Error(
						`${response.exception.exceptionType} ${response.exception.message} \n ${response.exception.remoteStackTrace} \n ${response.exception.rootException}`
					);
				}
				if (response.requiredVersion) {
					console.warn(
						`[IronPdf] mismatch version, required: ${IronPdfGlobalConfig.ironPdfEngineVersion} found: ${response.requiredVersion}`
					);
				}

				//apply configuration after handshake
				await setIsDebug(
					this.client,
					IronPdfGlobalConfig.getConfig().debugMode ?? false
				);
				const licenseKey = IronPdfGlobalConfig.getConfig().licenseKey;
				if (licenseKey) {
					await setLicenseKey(this.client, licenseKey);
				}
				if (IronPdfGlobalConfig.getConfig().debugMode)
					console.debug("Connected to IronPdfEngine");
			}
		}
		return this.client;
	}
}

export function getOsName() {
	switch (process.platform) {
		case "win32":
			return `windows`;
		case "darwin":
			return `macos`;
		case "linux":
			return `linux`;
		default:
			throw new Error(`OS: ${process.platform} are not supported`);
	}
}

export function getPlatformName() {
	switch (process.platform) {
		case "win32":
			return `Windows`;
		case "darwin":
			return `MacOS`;
		case "linux":
			return `Linux`;
		default:
			throw new Error(`Platform: ${process.platform} are not supported`);
	}
}

export function ironPdfEngineExecutable() {
	switch (process.platform) {
		case "win32":
			return `IronPdfEngineConsole.exe`;
		case "darwin":
			return `IronPdfEngineConsole`;
		case "linux":
			return `IronPdfEngineConsole`;
		default:
			throw new Error(`OS: ${process.platform} are not supported`);
	}
}

export function getOsArch() {
	switch (Os.arch()) {
		case "ppc64":
		case "x64":
		case "s390x":
			return `x64`;
		case "arm":
		case "arm64":
			return `arm64`;
		default:
			return "x86";
	}
}

async function tryChangePermissions(directoryPath: string, fileMode: string) {
	try {
		const syncReaddir = util.promisify(fs.readdir);
		const syncChmod = util.promisify(fs.chmod);

		const files = await syncReaddir(directoryPath);

		// Listing all files using forEach
		for (const file of files) {
			const filePath = path.join(directoryPath, file);
			if (IronPdfGlobalConfig.getConfig().debugMode)
				console.debug(`chmod ${filePath}`);
			await syncChmod(filePath, fileMode);
		}

	} catch (e) {
		console.debug(`tryChangePermissions of: ${directoryPath} Error: ${e}`);
	}
}
