import * as grpc from "@grpc/grpc-js";
import {Buffer} from "buffer";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import {PassThrough, Readable} from "stream";
import {Access} from "../../access";
import {IronPdfServiceClient} from "../../generated_proto/ironpdfengineproto/IronPdfService";
import {BytesResultStreamP__Output} from "../../generated_proto/ironpdfengineproto/BytesResultStreamP";
import {BooleanResultP__Output} from "../../generated_proto/ironpdfengineproto/BooleanResultP";
import {
	QPdfIsLinearizedRequestStreamP
} from "../../generated_proto/ironpdfengineproto/QPdfIsLinearizedRequestStreamP";
import {
	QPdfLinearizeInMemoryRequestStreamP
} from "../../generated_proto/ironpdfengineproto/QPdfLinearizeInMemoryRequestStreamP";
import {
	QPdfSaveAsLinearizedFromBytesRequestStreamP
} from "../../generated_proto/ironpdfengineproto/QPdfSaveAsLinearizedFromBytesRequestStreamP";
import {LinearizationMode} from "../../../public/render";
import {chunkBuffer, handleRemoteException} from "../util";
 
/**
 * Check if the given PDF bytes represent a linearized ("Fast Web View") PDF.
 */
export async function isLinearizedFromBytes(
	pdfBytes: Buffer,
	password = ""
): Promise<boolean> {
	const client: IronPdfServiceClient = await Access.ensureConnection();
 
	return new Promise(
		(resolve: (_: boolean) => void, reject: (errorMsg: string) => void) => {
			const stream: grpc.ClientWritableStream<QPdfIsLinearizedRequestStreamP> =
				client.QPdf_Linearization_IsLinearized(
					(err: grpc.ServiceError | null, value: BooleanResultP__Output | undefined) => {
						if (err) {
							reject(`${err.name}/n${err.message}`);
						} else if (value) {
							if (value.exception) {
								handleRemoteException(value.exception, reject);
								return;
							}
							resolve(value.result ?? false);
						} else {
							reject("No response from IronPdfEngine for isLinearized");
						}
					}
				);
 
			stream.write({info: {password: password ?? ""}});
			chunkBuffer(pdfBytes).forEach((chunk) => {
				stream.write({pdfBytesChunk: chunk});
			});
			stream.end();
		}
	);
}
 
/**
 * Linearize a PDF held by the engine (by document id) and return the linearized bytes
 * via the {@code QPdf_Linearization_LinearizeInMemoryFromId} unary-request/server-streaming RPC.
 */
export async function linearizeInMemoryFromId(
	id: string,
	password = ""
): Promise<Buffer> {
	const client: IronPdfServiceClient = await Access.ensureConnection();
 
	return new Promise(
		(resolve: (_: Buffer) => void, reject: (errorMsg: string) => void) => {
			const stream: grpc.ClientReadableStream<BytesResultStreamP__Output> =
				client.QPdf_Linearization_LinearizeInMemoryFromId({
					document: {documentId: id},
					password: password ?? "",
				});
 
			const buffers: Buffer[] = [];
			stream.on("data", (data: BytesResultStreamP__Output) => {
				if (data.exception) {
					handleRemoteException(data.exception, reject);
				} else if (data.resultChunk) {
					buffers.push(data.resultChunk);
				}
			});
 
			stream.on("error", (err: Error) => {
				reject(`${err.name}/n${err.message}`);
			});
 
			stream.on("end", () => {
				resolve(Buffer.concat(buffers));
			});
		}
	);
}
 
/**
 * Linearize a PDF held by the engine (by document id) and stream the linearized bytes
 * as a {@link Readable}. Useful for piping to HTTP responses or file streams without
 * buffering the entire PDF in memory.
 */
export async function linearizeInMemoryFromIdStream(
	id: string,
	password = ""
): Promise<Readable> {
	const client: IronPdfServiceClient = await Access.ensureConnection();
 
	const passThrough = new PassThrough();
 
	const stream: grpc.ClientReadableStream<BytesResultStreamP__Output> =
		client.QPdf_Linearization_LinearizeInMemoryFromId({
			document: {documentId: id},
			password: password ?? "",
		});
 
	stream.on("data", (data: BytesResultStreamP__Output) => {
		if (data.exception) {
			passThrough.destroy(
				new Error(`${data.exception.message}/n${data.exception.remoteStackTrace}`)
			);
		} else if (data.resultChunk) {
			passThrough.write(data.resultChunk);
		}
	});
 
	stream.on("error", (err: Error) => {
		passThrough.destroy(err);
	});
 
	stream.on("end", () => {
		passThrough.end();
	});
 
	return passThrough;
}
 
/**
 * Linearize a PDF provided as raw bytes and return the linearized bytes via the
 * bidirectional streaming {@code QPdf_Linearization_LinearizeInMemory} RPC.
 */
export async function linearizeInMemoryFromBytes(
	pdfBytes: Buffer,
	password = ""
): Promise<Buffer> {
	const client: IronPdfServiceClient = await Access.ensureConnection();
 
	return new Promise(
		(resolve: (_: Buffer) => void, reject: (errorMsg: string) => void) => {
			const stream: grpc.ClientDuplexStream<
				QPdfLinearizeInMemoryRequestStreamP,
				BytesResultStreamP__Output
			> = client.QPdf_Linearization_LinearizeInMemory();
 
			const buffers: Buffer[] = [];
			stream.on("data", (data: BytesResultStreamP__Output) => {
				if (data.exception) {
					handleRemoteException(data.exception, reject);
				} else if (data.resultChunk) {
					buffers.push(data.resultChunk);
				}
			});
 
			stream.on("error", (err: Error) => {
				reject(`${err.name}/n${err.message}`);
			});
 
			stream.on("end", () => {
				resolve(Buffer.concat(buffers));
			});
 
			stream.write({info: {password: password ?? ""}});
			chunkBuffer(pdfBytes).forEach((chunk) => {
				stream.write({pdfBytesChunk: chunk});
			});
			stream.end();
		}
	);
}
 
/**
 * Core linearization logic shared across all linearization entry points. Implements the
 * {@link LinearizationMode} strategy pattern. Mirrors {@code PdfDocument.LinearizePdfCore}
 * on the C# side.
 */
export async function linearizeCoreFromBytes(
	pdfBytes: Buffer,
	password = "",
	mode: LinearizationMode = LinearizationMode.Automatic
): Promise<Buffer> {
	if (!pdfBytes || pdfBytes.length === 0) {
		throw new Error("The PDF bytes cannot be null or empty.");
	}
 
	if (mode === LinearizationMode.InMemory) {
		return linearizeInMemoryFromBytes(pdfBytes, password);
	}
 
	if (mode === LinearizationMode.FileBased) {
		// Explicit FileBased — let any disk error bubble up.
		return linearizeViaTempFile(pdfBytes, password);
	}
 
	// Automatic mode
	if (canWriteToTemp()) {
		try {
			return await linearizeViaTempFile(pdfBytes, password);
		} catch (e) {
			console.warn(
				`Automatic Linearization: Disk attempt failed (${(e as Error).message}). ` +
					"Falling back to Memory linearization."
			);
			return linearizeInMemoryFromBytes(pdfBytes, password);
		}
	}
	console.warn("Automatic Linearization: No write permission detected. Using Memory linearization.");
	return linearizeInMemoryFromBytes(pdfBytes, password);
}
 
/**
 * Variant of {@link linearizeCoreFromBytes} that starts from an open document on the engine.
 * For {@link LinearizationMode.InMemory} we use the cheap document-id RPC; for the disk-based
 * paths we have to fetch the bytes once and delegate to {@link linearizeCoreFromBytes}.
 */
export async function linearizeCoreFromId(
	id: string,
	getBytes: () => Promise<Buffer>,
	password = "",
	mode: LinearizationMode = LinearizationMode.Automatic
): Promise<Buffer> {
	if (mode === LinearizationMode.InMemory) {
		return linearizeInMemoryFromId(id, password);
	}
	const pdfBytes = await getBytes();
	return linearizeCoreFromBytes(pdfBytes, password, mode);
}
 
/**
 * Linearize via the engine's file-based RPC and persist the result through a client-side
 * temporary file. The client-side disk write is the difference between this and
 * {@link linearizeInMemoryFromBytes} — when the client filesystem is read-only,
 * {@code FileBased} mode fails here so {@link LinearizationMode.Automatic} can fall back.
 *
 * Mirrors C# {@code PdfDocument.LinearizeViaTempFile}.
 */
async function linearizeViaTempFile(pdfBytes: Buffer, password: string): Promise<Buffer> {
	const linearized = await saveAsLinearizedFromBytes(pdfBytes, "", password);
	const tempPath = path.join(
		os.tmpdir(),
		`ironpdf-linearize-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}.pdf`
	);
	try {
		fs.writeFileSync(tempPath, linearized);
		return fs.readFileSync(tempPath);
	} finally {
		try {
			if (fs.existsSync(tempPath)) {
				fs.unlinkSync(tempPath);
			}
		} catch {
			// best-effort cleanup
		}
	}
}
 
/**
 * Probe whether the current process can create files in the system temp directory.
 */
function canWriteToTemp(): boolean {
	const probePath = path.join(
		os.tmpdir(),
		`ironpdf-probe-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}.tmp`
	);
	try {
		fs.writeFileSync(probePath, "");
		return true;
	} catch {
		return false;
	} finally {
		try {
			if (fs.existsSync(probePath)) {
				fs.unlinkSync(probePath);
			}
		} catch {
			// best-effort cleanup
		}
	}
}
 
/**
 * Linearize a PDF provided as raw bytes and save the result to disk via the file-based
 * streaming {@code QPdf_Linearization_SaveAsLinearizedFromBytes} RPC.
 *
 * Mirrors the in-memory behavior used by {@link compressInMemory} in {@code compress.ts}:
 * the engine streams the linearized bytes back, and we concatenate and persist them
 * locally at {@code outputPath}.
 */
export async function saveAsLinearizedFromBytes(
	pdfBytes: Buffer,
	outputPath: string,
	password = ""
): Promise<Buffer> {
	const client: IronPdfServiceClient = await Access.ensureConnection();
 
	return new Promise(
		(resolve: (_: Buffer) => void, reject: (errorMsg: string) => void) => {
			const stream: grpc.ClientDuplexStream<
				QPdfSaveAsLinearizedFromBytesRequestStreamP,
				BytesResultStreamP__Output
			> = client.QPdf_Linearization_SaveAsLinearizedFromBytes();
 
			const buffers: Buffer[] = [];
			stream.on("data", (data: BytesResultStreamP__Output) => {
				if (data.exception) {
					handleRemoteException(data.exception, reject);
				} else if (data.resultChunk) {
					buffers.push(data.resultChunk);
				}
			});
 
			stream.on("error", (err: Error) => {
				reject(`${err.name}/n${err.message}`);
			});
 
			stream.on("end", () => {
				resolve(Buffer.concat(buffers));
			});
 
			stream.write({
				info: {outputPath: outputPath, password: password ?? ""},
			});
			chunkBuffer(pdfBytes).forEach((chunk) => {
				stream.write({pdfBytesChunk: chunk});
			});
			stream.end();
		}
	);
}