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

import { OPCUAClient, AttributeIds, DataType } from 'node-opcua';
import mqtt from 'mqtt';

function getValueByPath(obj: any, path: string): any {
  if (!path) return obj;
  return path.split('.').reduce((acc, key) => acc?.[key], obj);
}

export class _OPCServer implements INodeType {
  description: INodeTypeDescription = {
    displayName: 'OPCServer',
    name: 'opcServer',
    group: ['transform'],
    version: 1,
    description: 'Записывает данные в OPC UA и, при необходимости, публикует в MQTT',
    defaults: {
      name: 'OPCServer',
    },
    inputs: [NodeConnectionType.Main],
    outputs: [NodeConnectionType.Main],
    properties: [
      {
        displayName: 'Requests or Devices',
        name: 'sectionDevices',
        type: 'string',
        default: 'Устройства',
      },
      {
        displayName: 'Путь до данных внутри DATA',
        name: 'useKeyFromData',
        type: 'string',
        default: '',
      },
      {
        displayName: 'Имя сущности (например: Весы, Табло и т.д.)',
        name: 'entityName',
        type: 'string',
        default: 'Состояние',
      },
      {
        displayName: 'Тип сущности',
        name: 'entityType',
        type: 'string',
        default: 'Весы',
      },
      {
        displayName: 'Локация',
        name: 'location',
        type: 'string',
        default: '9',
      },
      {
        displayName: 'Отправлять в MQTT',
        name: 'sendToMqtt',
        type: 'boolean',
        default: false,
      },
      {
        displayName: 'Сетевой узел',
        name: 'networkNode',
        type: 'string',
        default: 'dev_re_promuc_local',
      },
    ],
  };

  async execute(this: IExecuteFunctions) {
    const items = this.getInputData();
    const sectionDevices = this.getNodeParameter('sectionDevices', 0) as string;
    const useKeyFromData = this.getNodeParameter('useKeyFromData', 0) as string;
    const entityName = this.getNodeParameter('entityName', 0) as string;
    const entityType = this.getNodeParameter('entityType', 0) as string;
    const location = this.getNodeParameter('location', 0) as string;
    const networkNode = this.getNodeParameter('networkNode', 0) as string;
    const sendToMqtt = this.getNodeParameter('sendToMqtt', 0) as boolean;

    const opcHost = process.env.OPChost;
    const opcPort = process.env.OPCport;

    if (!opcHost || !opcPort) {
      throw new Error('OPChost или OPCport не заданы в переменных окружения');
    }

    const opcEndpoint = `opc.tcp://${opcHost}:${opcPort}`;
    const client = OPCUAClient.create({ endpointMustExist: false });

    await client.connect(opcEndpoint);
    const session = await client.createSession();

    for (let i = 0; i < items.length; i++) {
      const item = items[i];
      const rawData = item.json.DATA as Record<string, any>;
      const metadata = item.json.METADATA as { nodeId?: string };

      if (!rawData) continue;



      const dataToWrite = getValueByPath(rawData, useKeyFromData);
      if (typeof dataToWrite !== 'object' || dataToWrite === null) {
        throw new Error(`Не удалось получить данные по пути '${useKeyFromData}'`);
      }

      let basePath: string;
      if (typeof rawData.route === 'string') {
        basePath = rawData.route;
      } else {
        basePath = `${networkNode}.${location}.${sectionDevices}.${entityType}.${entityName}`;
      }

      const opcNamespacePath = `${basePath}`;
      Object.entries(dataToWrite).forEach(([key, value]) => {
        console.log(`key=${key}, value=`, value, typeof value);
      });

      const nodesToWrite = Object.entries(dataToWrite).map(([key, value]) => {
        let dataType: DataType = DataType.String;
        if (typeof value === 'boolean') dataType = DataType.Boolean;
        else if (typeof value === 'number') dataType = DataType.Double;

        return {
          nodeId: `ns=1;s=${opcNamespacePath}.${key}`,
          attributeId: AttributeIds.Value,
          value: {
            value: {
              dataType,
              value,
            },
          },
        };
      });

      await session.write(nodesToWrite);
      const writeResults = await session.write(nodesToWrite);

      writeResults.forEach((statusCode, index) => {
        const nodeId = nodesToWrite[index].nodeId;
        console.log(`Write to ${nodeId} => status: ${statusCode.name || statusCode.toString()}`);
      });


      // ===== MQTT (если включено) =====
      if (sendToMqtt) {
        const mqttHost = process.env.mqtthost;
        const mqttPort = process.env.mqttport;
        const mqttUser = process.env.mqttuser;
        const mqttPass = process.env.mqttpassword;

        if (!mqttHost || !mqttPort || !mqttUser || !mqttPass) {
          throw new Error('Отсутствуют переменные окружения для MQTT');
        }

        const payload = JSON.stringify(dataToWrite);
        const mqttUrl = `mqtt://${mqttHost}:${mqttPort}`;
        const mqttClient = mqtt.connect(mqttUrl, {
          username: mqttUser,
          password: mqttPass,
        });

        await new Promise<void>((resolve, reject) => {
          mqttClient.on('connect', () => {
            mqttClient.publish(basePath, payload, { qos: 1, retain: false }, (err) => {
              mqttClient.end();
              if (err) return reject(err);
              resolve();
            });
          });

          mqttClient.on('error', (err) => reject(err));
        });
      }


      if (typeof item.json.DATA === 'object' && item.json.DATA !== null) {
        (item.json.DATA as Record<string, any>).route = opcNamespacePath;
      }

    }

    await session.close();
    await client.disconnect();

    return this.prepareOutputData(items);
  }
}
