import {
	IExecuteFunctions,
	INodeExecutionData,
	INodeType,
	INodeTypeDescription,
	NodeApiError,
	NodeConnectionType,
} from 'n8n-workflow';
import Docker from 'dockerode';

export class Pytenable implements INodeType {
	description: INodeTypeDescription = {
		displayName: 'Pytenable',
		name: 'pytenable',
		icon: 'file:pytenable.svg',
		group: ['transform'],
		version: 1,
		description: 'Ejecuta código Python con Pytenable en un entorno Docker seguro',
		defaults: {
			name: 'Pytenable',
		},
		inputs: ['main' as unknown as NodeConnectionType],
		outputs: ['main' as unknown as NodeConnectionType],
		credentials: [
			{
				name: 'tenableApi',
				required: true,
			},
		],
		properties: [
			{
				displayName: 'Modo de Operación',
				name: 'mode',
				type: 'options',
				noDataExpression: true,
				options: [
					{
						name: 'Ejecutar una vez para todos los items',
						value: 'runOnceForAllItems',
						description: 'Ejecuta este código una sola vez para todos los datos de entrada.',
					},
					{
						name: 'Ejecutar una vez por cada item',
						value: 'runOnceForEachItem',
						description: 'Ejecuta este código una vez por cada item de entrada.',
					},
				],
				default: 'runOnceForAllItems',
			},
			{
				displayName: 'Código Python',
				name: 'pythonCode',
				type: 'string',
				typeOptions: {
					editor: 'codeNodeEditor',
					editorLanguage: 'python',
				},
				default: '# El resultado debe ser una lista de diccionarios\n# return [{"resultado": "ok"}]',
				description: 'El código Python a ejecutar. Usa las variables _items, _item y _credentials.',
				noDataExpression: true,
			},
		],
	};

	async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
		const node = this.getNode();
		const docker = new Docker({ socketPath: '/var/run/docker.sock' });
		const imageName = 'pytenable-runner:latest';
		const userCode = this.getNodeParameter('pythonCode', 0, '') as string;
		const credentials = await this.getCredentials('tenableApi');

		const runInContainer = async (inputData: object): Promise<INodeExecutionData[]> => {
			const pythonWrapper = `
import sys
import json
import traceback
import asyncio

async def main():
    try:
        input_data_str = sys.stdin.read()
        input_data = json.loads(input_data_str)

        global _items, _item, _credentials
        _item = input_data.get('item', {})
        _items = input_data.get('items', [])
        _credentials = input_data.get('credentials', {})

        user_code = """
${userCode}
"""

        exec_scope = {}
        exec(f"async def user_main():\\n" + "\\n".join(f"    {line}" for line in user_code.split("\\n")), globals(), exec_scope)

        result = await exec_scope['user_main']()
        print(json.dumps(result))

    except Exception as e:
        print(f"Error en el script de Python: {e}", file=sys.stderr)
        traceback.print_exc(file=sys.stderr)
        sys.exit(1)

if __name__ == "__main__":
    asyncio.run(main())
`;
			let outputData = '';
			let errorData = '';

			const container = await docker.createContainer({
				Image: imageName,
				Cmd: ['python', '-u', '-c', pythonWrapper],
				Tty: false,
				AttachStdin: true,
				AttachStdout: true,
				AttachStderr: true,
				OpenStdin: true,
				StdinOnce: true,
				HostConfig: {
					AutoRemove: true,
				},
			});

			const stream = await container.attach({ stream: true, stdin: true, stdout: true, stderr: true });

			return new Promise<INodeExecutionData[]>((resolve, reject) => {
				// Docker multiplexa stdout y stderr en un solo stream.
                // El primer byte del chunk indica el canal (1 = stdout, 2 = stderr).
                // Los siguientes 3 bytes son padding.
                // Los 4 bytes finales son el tamaño del payload.
                // Nos saltamos los 8 bytes de la cabecera para leer el mensaje.
				stream.on('data', (chunk: Buffer) => {
					if (chunk[0] === 1) {
						outputData += chunk.slice(8).toString('utf-8');
					} else if (chunk[0] === 2) {
						errorData += chunk.slice(8).toString('utf-8');
					}
				});

				stream.on('end', () => {
					if (errorData) {
						return reject(new NodeApiError(node, { message: `Error en el contenedor de Python: ${errorData}` }));
					}
					try {
						const result = JSON.parse(outputData);
						const returnItems = this.helpers.constructExecutionMetaData(
							this.helpers.returnJsonArray(result),
							{ itemData: [] },
						);
						return resolve(returnItems);
					} catch (error) {
						const parseErrorMsg = `Error al parsear la salida de Python. Salida recibida: "${outputData}". Error: ${(error as Error).message}`;
						return reject(new NodeApiError(node, { message: parseErrorMsg }));
					}
				});

				stream.on('error', (err: Error) => {
					return reject(new NodeApiError(node, { message: `Error de stream con Docker: ${err.message}` }));
				});

				container.start((err: Error) => {
					if (err) return reject(new NodeApiError(node, { message: `Error al iniciar el contenedor: ${err.message}` }));
					stream.write(JSON.stringify(inputData));
					stream.end();
				});
			});
		};

		const mode = this.getNodeParameter('mode', 0, 'runOnceForAllItems');
		const inputItems = this.getInputData();

		if (mode === 'runOnceForAllItems') {
			const result = await runInContainer({ items: inputItems.map(item => item.json), credentials });
			return [result];
		} else { // runOnceForEachItem
			const returnData: INodeExecutionData[] = [];
			for (let i = 0; i < inputItems.length; i++) {
				const resultItems = await runInContainer({ item: inputItems[i].json, credentials });
				for (const item of resultItems) {
					item.pairedItem = { item: i };
					returnData.push(item);
				}
			}
			return [returnData];
		}
	}
}
