import {
    ITriggerFunctions,
    INodeType,
    INodeTypeDescription,
    NodeConnectionType
} from 'n8n-workflow';

import {
    ClientSession,
    MessageSecurityMode,
    SecurityPolicy,
    OPCUAClient,
    AttributeIds,
    UserIdentityInfoUserName,
    UserTokenType,
} from 'node-opcua';

import * as fs from 'fs';
import * as path from 'path';

function resolveEnvVariables(str: string): string {
    if (typeof str !== 'string') return str;
    return str.replace(/\$([a-zA-Z0-9_]+)/g, (_, v) => process.env[v] ?? '');
}

interface OpcInputOptions {
    host: string;
    port: string;
    user?: string;
    password?: string;
    cert?: string;
    key?: string;
    sec_mode: string;
    securityPolicyUri?: string;
    applicationUri?: string;
    NodeIdPath?: string;
    ns: number;
    interval: number;
}

export class _OPCInput implements INodeType {
    description: INodeTypeDescription = {
        displayName: 'OPC Input Trigger',
        name: 'opcInput',
        group: ['trigger'],
        version: 1,
        description: 'Connect to OPC UA server and periodically fetch values',
        defaults: { name: 'OPC Input' },
        inputs: [],
        outputs: [NodeConnectionType.Main],
        properties: [
            { displayName: 'Host', name: 'host', type: 'string', default: '' },
            { displayName: 'Port', name: 'port', type: 'string', default: '4840' },
            { displayName: 'Username', name: 'user', type: 'string', default: '' },
            { displayName: 'Password', name: 'password', type: 'string', typeOptions: { password: true }, default: '' },
            { displayName: 'Certificate File Path', name: 'cert', type: 'string', default: '' },
            { displayName: 'Private Key File Path', name: 'key', type: 'string', default: '' },
            {
                displayName: 'Security Mode',
                name: 'sec_mode',
                type: 'options',
                options: [
                    { name: 'NONE', value: 'NONE' },
                    { name: 'SIGN', value: 'SIGN' },
                    { name: 'SIGNANDENCRYPT', value: 'SIGNANDENCRYPT' },
                ],
                default: 'NONE',
            },
            { displayName: 'Security Policy URI', name: 'securityPolicyUri', type: 'string', default: '' },
            { displayName: 'Application URI', name: 'applicationUri', type: 'string', default: '' },
            { displayName: 'NodeId Path', name: 'NodeIdPath', type: 'string', default: '' },
            { displayName: 'Namespace Index', name: 'ns', type: 'number', default: 1 },
            { displayName: 'Interval (ms)', name: 'interval', type: 'number', default: 5000 },
        ],
    };

    async trigger(this: ITriggerFunctions) {
        const options: OpcInputOptions = {
            host: resolveEnvVariables((this.getNodeParameter('host', '') as string) || process.env.OPC_HOST || 'localhost'),
            port: resolveEnvVariables((this.getNodeParameter('port', '4840') as string) || process.env.OPC_PORT || '4840'),
            user: resolveEnvVariables((this.getNodeParameter('user', '') as string) || process.env.OPC_USER || ''),
            password: resolveEnvVariables((this.getNodeParameter('password', '') as string) || process.env.OPC_PASS || ''),
            cert: resolveEnvVariables((this.getNodeParameter('cert', '') as string) || process.env.OPC_CERT || ''),
            key: resolveEnvVariables((this.getNodeParameter('key', '') as string) || process.env.OPC_KEY || ''),
            sec_mode: resolveEnvVariables((this.getNodeParameter('sec_mode', 'NONE') as string) || process.env.OPC_SEC_MODE || 'NONE'),
            securityPolicyUri: resolveEnvVariables((this.getNodeParameter('securityPolicyUri', '') as string) || process.env.OPC_SEC_POLICY || ''),
            applicationUri: resolveEnvVariables((this.getNodeParameter('applicationUri', '') as string) || process.env.OPC_APP_URI || ''),
            NodeIdPath: resolveEnvVariables((this.getNodeParameter('NodeIdPath', '') as string) || process.env.OPC_NODE_PATH || ''),
            ns: (this.getNodeParameter('ns', 1) as number) || Number(process.env.OPC_NS) || 1,
            interval: (this.getNodeParameter('interval', 5000) as number) || Number(process.env.OPC_INTERVAL) || 5000,
        };

        const endpointUrl = `opc.tcp://${options.host}:${options.port}`;

        let securityMode: MessageSecurityMode;
        switch (options.sec_mode) {
            case 'SIGN':
                securityMode = MessageSecurityMode.Sign;
                break;
            case 'SIGNANDENCRYPT':
                securityMode = MessageSecurityMode.SignAndEncrypt;
                break;
            default:
                securityMode = MessageSecurityMode.None;
        }

        let certificateFile: string | undefined;
        let privateKeyFile: string | undefined;

        if (process.env.OPC_CERT_BASE64) {
            const certPath = path.join('/tmp', 'opc_cert.pem');
            fs.writeFileSync(certPath, Buffer.from(process.env.OPC_CERT_BASE64, 'base64'));
            certificateFile = certPath;
        } else if (options.cert && fs.existsSync(options.cert)) {
            certificateFile = options.cert;
        }

        if (process.env.OPC_KEY_BASE64) {
            const keyPath = path.join('/tmp', 'opc_key.pem');
            fs.writeFileSync(keyPath, Buffer.from(process.env.OPC_KEY_BASE64, 'base64'));
            privateKeyFile = keyPath;
        } else if (options.key && fs.existsSync(options.key)) {
            privateKeyFile = options.key;
        }

        const client = OPCUAClient.create({
            securityMode,
            securityPolicy: (options.securityPolicyUri as SecurityPolicy) || SecurityPolicy.None,
            certificateFile,
            privateKeyFile,
        });

        let connecting = false;
        let disconnecting = false;

        async function safeDisconnect() {
            if (disconnecting) return;
            disconnecting = true;
            try { await client.disconnect(); } catch { }
            disconnecting = false;
        }

        // Рекурсивный обход объекта / папки
        const browseAndRead = async (session: ClientSession, nodeId: string): Promise<any[]> => {
            const browseResult = await session.browse(nodeId);
            const results: any[] = [];
            for (const ref of browseResult.references || []) {
                if (ref.nodeClass === 2) { // Variable
                    try {
                        const dataValue = await session.read({ nodeId: ref.nodeId, attributeId: AttributeIds.Value });
                        results.push({
                            namespaceIndex: ref.nodeId.namespace,
                            browseName: ref.browseName.toString(),
                            displayName: ref.displayName.text,
                            identifier: ref.nodeId.value,
                            value: dataValue.value.value,
                        });
                    } catch (e) {
                        results.push({
                            namespaceIndex: ref.nodeId.namespace,
                            browseName: ref.browseName.toString(),
                            displayName: ref.displayName.text,
                            identifier: ref.nodeId.value,
                            value: `Error: ${(e as Error).message}`,
                        });
                    }
                } else { // Object / Folder
                    results.push({
                        namespaceIndex: ref.nodeId.namespace,
                        browseName: ref.browseName.toString(),
                        displayName: ref.displayName.text,
                        identifier: ref.nodeId.value,
                        value: await browseAndRead(session, ref.nodeId.toString()),
                    });
                }
            }
            return results;
        };

        const emitValues = async () => {
            if (connecting) return;
            connecting = true;

            const workflowName = this.getWorkflow().name;
            const nodeInfo = this.getNode();
            const fromNodeFull = nodeInfo.type;
            const fromNode = fromNodeFull.includes('.') ? fromNodeFull.split('.').pop()! : fromNodeFull;
            const idField = nodeInfo.name;

            try {
                await client.connect(endpointUrl);

                const session = await client.createSession(
                    options.user
                        ? {
                            type: UserTokenType.UserName,
                            userName: options.user,
                            password: options.password || "",
                        } as UserIdentityInfoUserName
                        : undefined
                );

                const nodeId = `ns=${options.ns};s=${options.NodeIdPath}`;
                let values: any[] = [];

                // Пробуем прочитать как переменную
                try {
                    const dataValue = await session.read({ nodeId, attributeId: AttributeIds.Value });
                    if (dataValue.value.value !== null && dataValue.value.value !== undefined) {
                        values = [dataValue.value.value];
                    } else {
                        // Если это объект / папка — рекурсивное чтение
                        values = await browseAndRead(session, nodeId);
                    }
                } catch {
                    values = await browseAndRead(session, nodeId);
                }

                const output = [
                    {
                        DATA: {
                            connection_status: true,
                            time: Date.now(),
                            values: [values],
                        },
                        METADATA: {
                            chain: workflowName,
                            from: fromNode,
                            id: idField,
                            time: Date.now(),
                        },
                    },
                ];

                this.emit([this.helpers.returnJsonArray(output)]);

                await session.close();
                await safeDisconnect();
            } catch (err) {
                const output = [
                    {
                        DATA: {
                            connection_status: false,
                            time: Date.now(),
                            values: [],
                            error: (err as Error).message,
                        },
                        METADATA: {
                            chain: workflowName,
                            from: fromNode,
                            id: idField,
                            time: Date.now(),
                        },
                    },
                ];

                this.emit([this.helpers.returnJsonArray(output)]);
                await safeDisconnect();
            } finally {
                connecting = false;
            }
        };


        const intervalHandle = setInterval(emitValues, options.interval);

        async function closeFunction() {
            clearInterval(intervalHandle);
            await safeDisconnect();
        }

        return { closeFunction };
    }
}
