import { LogOrigin } from 'ontime-types';

import { fromBuffer, type OscPacketOutput } from 'osc-min';
import * as dgram from 'node:dgram';

import { logger } from '../classes/Logger.js';
import { dispatchFromAdapter } from '../api-integration/integration.controller.js';
import { isOntimeCloud } from '../setup/environment.js';

import { integrationPayloadFromPath } from './utils/parse.js';
import type { IAdapter } from './IAdapter.js';

class OscServer implements IAdapter {
  private udpSocket: dgram.Socket | null = null;

  init(port: number) {
    if (isOntimeCloud) {
      logger.warning(LogOrigin.Rx, 'OSC: Skip starting server in cloud environment');
    }
    this.udpSocket?.close();
    logger.info(LogOrigin.Rx, `OSC: Starting server on port ${port}`);
    this.udpSocket = dgram.createSocket('udp4');
    this.udpSocket.on('error', (error) => logger.error(LogOrigin.Rx, `OSC IN: ${error}`));
    this.udpSocket.on('message', (buf) => {
      // message should look like /ontime/{command}/{params?} {args} where
      // ontime: fixed message for app
      // command: command to be called
      // params: used to create a nested object to patch with
      // args: extra data, only used on some API entries

      let msg: OscPacketOutput;
      try {
        msg = fromBuffer(buf);
      } catch (_e) {
        logger.error(LogOrigin.Rx, 'OSC IN: Received invalid OSC message');
        return;
      }

      if (msg.oscType === 'bundle') {
        logger.error(LogOrigin.Rx, 'OSC IN: Ontime is unable to handle OSC bundles');
        return;
      }

      const { address, args: oscArgs } = msg;

      // split message
      const [, ontimeKey, command, ...params] = address.split('/');
      const args = oscArgs[0]?.value ?? undefined;

      // get first part (ontime)
      if (ontimeKey !== 'ontime') {
        logger.error(LogOrigin.Rx, `OSC IN: OSC messages to ontime must start with /ontime/, received: ${msg}`);
        return;
      }

      // get second part (command)
      if (!command) {
        logger.error(LogOrigin.Rx, 'OSC IN: No path found');
        return;
      }

      let transformedPayload: unknown = args;
      // we need to transform the params for the more complex endpoints
      if (params.length) {
        transformedPayload = integrationPayloadFromPath(params, args);
      }

      try {
        dispatchFromAdapter(command, transformedPayload, 'osc');
      } catch (error) {
        logger.error(LogOrigin.Rx, `OSC IN: ${error}`);
      }
    });
    this.udpSocket.bind(port);
  }
  shutdown() {
    logger.info(LogOrigin.Rx, 'OSC: Closing server');
    this.udpSocket?.close();
    this.udpSocket = null;
  }
}

export const oscServer = new OscServer();
