import {RequestLeser as ProdressListener, type R2AnfrageBlaupause} from '@prodress/pdr2com';
import {Socket} from 'node:net';
import {stringifyError} from '../utils.js';
import {ProdressRequest} from './wrapper.js';

/**
 * Initializes a TCP Socket, sends a ProdressRequest and listens to this Socket for one Response
 */
export class ProdressConnection {
  public static readonly TIMEOUT = 30_000;

  private port: number;
  private host: string;
  private socket: Socket;
  private request: ProdressRequest;
  private listener: ProdressListener;
  private requestBody: Pdr2_RequestBody;
  private responseSchema: Pdr2_ResponseSchema;

  /**
   * @param header Headerdata to initialize the the TCP Socket and the ProdressRequest
   * @param body Requestdata as list of tupel
   * @param response Expected Responsedata as list of tupel
   */
  public constructor({header, body, response}: Pdr2_Request) {
    this.port = header.port ?? 4788;
    this.host = header.host ?? 'localhost';
    this.socket = new Socket();
    this.request = new ProdressRequest(header.database, header.handler, header.action);
    this.listener = new ProdressListener();
    this.requestBody = body;
    this.responseSchema = response;
  }

  /**
   * Handels the request- and response-flow
   * @returns the stringified response
   */
  public async handleRequest(): Promise<string> {
    let response: string = '';
    // Wraps a Promise around the reponse which resolves when the "finished" event is emitted and destroys the socekt
    const responseAsPromise = new Promise<string>((resolve) => {
      this.socket.once('finished', () => {
        if (!this.socket.destroyed) {
          this.socket.destroy();
        }
        resolve(response);
      });
    });

    this.socket.setTimeout(ProdressConnection.TIMEOUT);
    this.socket.connect(this.port, this.host);

    // Sends the writes and sends the request when the connecting is established
    this.socket.once('connect', () => {
      try {
        this.request.writeEntries(this.requestBody);
        this.request.sende(this.socket);
      } catch (error) {
        response = stringifyError(error as Error);
        this.finished();
      }
    });

    // @todo Rework Error Message
    this.socket.on('timeout', () => {
      const timeoutError = new Error('timeout');
      response = stringifyError(timeoutError);
      this.finished();
    });

    this.socket.on('error', (error: Error) => {
      response = stringifyError(error);
      this.finished();
    });

    // Starts writing data from the buffer to the listener-instance and emits the "dataRecieved" event
    this.socket.on('data', (data: Buffer) => {
      this.listener.verarbeiteDaten(data);
      this.socket.emit('dataRecieved');
    });

    // Since we only expect one response the "dataRecieved" event triggers only once
    // Reads the data until finished and writes the response afterwards
    this.socket.once('dataRecieved', async () => {
      try {
        const incomingResponse = await this.listener.leseRequest();
        const responseObject = this.readResponse(incomingResponse);
        response = JSON.stringify(responseObject, null, 2);
        this.finished();
      } catch (error) {
        response = stringifyError(error as Error);
        this.finished();
      }
    });

    return responseAsPromise;
  }

  /**
   * Emits a custom "finished" event
   */
  private finished(): void {
    this.socket.emit('finished');
  }

  /**
   * reads the Reponse based on the provided responseTupelList and creates a ResponseObject
   * @param responseInstance The Listener Instance of the active TCP Socket
   * @returns ResponseObject
   */
  private readResponse(responseInstance: R2AnfrageBlaupause): Pdr2_ResponseObject {
    const response: Pdr2_ResponseObject = {};
    let amountOfEntries = 1;

    if (!this.responseSchema.isStatic) {
      amountOfEntries = responseInstance.nächsteGanzzahl();
    }

    for (let i = 1; i <= amountOfEntries; i++) {
      for (const [key, type] of this.responseSchema.responseTupel) {
        switch (type) {
          case 'string':
            response[key] = responseInstance.nächsterText();
            break;
          case 'integer':
            response[key] = responseInstance.nächsteGanzzahl();
            break;
          case 'float':
            response[key] = responseInstance.nächsteDezimalzahl();
            break;
          case 'date':
            response[key] = responseInstance.nächstesDatum();
            break;
          case 'date_time':
            //@todo missing method in lib
            response[key] = responseInstance.nächstesDatum();
            break;
        }
      }
    }
    return response;
  }
}
