/*---------------------------------------------------------
 * Copyright 2022 The Go Authors. All rights reserved.
 * Licensed under the MIT License. See LICENSE in the project root for license information.
 *--------------------------------------------------------*/
import vscode = require('vscode');
import cp = require('child_process');
import { URI } from 'vscode-uri';
import { getGoConfig } from './config';
import { getWorkspaceFolderPath } from './util';

export interface IVulncheckTerminal {
	appendLine: (str: string) => void;
	show: (preserveFocus?: boolean) => void;
	exit: () => void;
}
export class VulncheckTerminal implements IVulncheckTerminal {
	private term: vscode.Terminal;
	private writeEmitter = new vscode.EventEmitter<string>();

	// Buffer messages emitted before vscode is ready.  VSC calls pty.open when it is ready.
	private ptyReady = false;
	private buf: string[] = [];

	// Constructor function to stub during test.
	static Open(): IVulncheckTerminal {
		return new VulncheckTerminal();
	}

	private constructor() {
		const pty: vscode.Pseudoterminal = {
			onDidWrite: this.writeEmitter.event,
			handleInput: () => this.exit(),
			open: () => {
				this.ptyReady = true;
				this.buf.forEach((l) => this.writeEmitter.fire(l));
				this.buf = [];
			},
			close: () => {}
		};
		this.term = vscode.window.createTerminal({ name: 'govulncheck', pty }); // TODO: iconPath
	}

	appendLine(str: string) {
		if (!str.endsWith('\n')) {
			str += '\n';
		}
		str = str.replace(/\n/g, '\n\r'); // replaceAll('\n', '\n\r').
		if (!this.ptyReady) {
			this.buf.push(str); // print when `open` is called.
		} else {
			this.writeEmitter.fire(str);
		}
	}

	show(preserveFocus?: boolean) {
		this.term.show(preserveFocus);
	}

	exit() {}
}

// VulncheckReport is the JSON data type of gopls's vulncheck result.
export interface VulncheckReport {
	Entries?: { [key: string]: unknown }; // map: osv.ID -> osv.Entry (we don't need to know the Entry shape)
	Findings?: unknown[]; // []Finding. We don't need to know the exact Fingings shape either.

	Mode?: 'govulncheck' | 'imports';

	// Legacy: Vulns populated by gopls vulncheck run.
	Vulns?: unknown;
}

export async function writeVulns(
	res: VulncheckReport,
	term: IVulncheckTerminal | undefined,
	goplsBinPath: string
): Promise<void> {
	if (term === undefined) {
		return;
	}
	term.appendLine('');
	let stdout = '';
	let stderr = '';
	const pr = new Promise<number | null>((resolve) => {
		const p = cp.spawn(goplsBinPath, ['vulncheck', '--', '-mode=convert', '-show=color'], {
			cwd: getWorkspaceFolderPath()
		});

		p.stdout.on('data', (data) => {
			stdout += data;
		});
		p.stderr.on('data', (data) => {
			stderr += data;
		});
		// 'close' fires after exit or error when the subprocess closes all stdio.
		p.on('close', (exitCode) => {
			// When vulnerabilities are found, vulncheck -mode=convert returns a non-zero exit code.
			// TODO: can we use the exitCode to set the status of terminal?
			resolve(exitCode);
		});

		// vulncheck -mode=convert expects a stream of osv.Entry and govulncheck Finding json objects.
		if (res.Entries) {
			Object.values(res.Entries).forEach((osv) => {
				const we = { osv: osv };
				p.stdin.write(`${JSON.stringify(we)}`);
			});
		}
		if (res.Findings) {
			Object.values(res.Findings).forEach((finding) => {
				const we = { finding: finding };
				p.stdin.write(`${JSON.stringify(we)}`);
			});
		}
		p.stdin.end();
	});
	try {
		await pr;
	} catch (e) {
		console.error(`writeVulns: ${e}`);
	} finally {
		// Combining stderr and stdout streams in the exact order they were received
		// is tricky since they are buffered separately.
		// Normally, govulncheck will print the text-based report to stdout first
		// and then report whether there are vulnerabilities to stderr at the end.
		// So, we just process stdout first and then stderr.
		stdout.split('\n').forEach((l) => term.appendLine(l));
		stderr.split('\n').forEach((l) => term.appendLine(l));
	}
	return;
}

export const toggleVulncheckCommandFactory = () => () => {
	const editor = vscode.window.activeTextEditor;
	const documentUri = editor?.document.uri;
	toggleVulncheckCommand(documentUri);
};

function toggleVulncheckCommand(uri?: URI) {
	const goCfgName = 'diagnostic.vulncheck';
	const cfg = getGoConfig(uri);
	const { globalValue, workspaceValue, workspaceFolderValue } = cfg.inspect(goCfgName) || {};
	if (workspaceFolderValue) {
		const newValue = workspaceFolderValue === 'Imports' ? 'Off' : 'Imports';
		cfg.update(goCfgName, newValue);
		return;
	}
	if (workspaceValue) {
		const newValue = workspaceValue === 'Imports' ? 'Off' : 'Imports';
		cfg.update(goCfgName, newValue, false);
		return;
	}
	if (globalValue) {
		const newValue = globalValue === 'Imports' ? 'Off' : 'Imports';
		cfg.update(goCfgName, newValue, true);
		return;
	}
	cfg.update(goCfgName, 'Imports');
}
