import net from 'node:net';

import {RPCError} from '../types/RPCError';
import type {MultiMethodCalls} from './rTorrentMethodCallUtil';
import deserializer from './XMLRPCDeserializer';
import type {XMLRPCValue} from './XMLRPCSerializer';
import serializer from './XMLRPCSerializer';

const NULL_CHAR = String.fromCharCode(0);

const bufferStream = (stream: net.Socket): Promise<string> => {
  const chunks: Buffer[] = [];
  return new Promise<string>((resolve, reject) => {
    stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
    stream.on('error', reject);
    stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
  });
};

export const methodCallXML = (options: net.NetConnectOpts, methodName: string, params: XMLRPCValue[]) =>
  // TODO: better typings

  new Promise<any>((resolve, reject) => {
    const stream = net.connect(options);
    const xml = serializer.serializeSync(methodName, params);
    const xmlLength = Buffer.byteLength(xml, 'utf8');

    stream.on('error', reject);
    stream.setEncoding('utf8');

    const headerItems = [
      `CONTENT_LENGTH${NULL_CHAR}${xmlLength}${NULL_CHAR}`,
      `CONTENT_TYPE${NULL_CHAR}text/xml${NULL_CHAR}`,
      `SCGI${NULL_CHAR}1${NULL_CHAR}`,
    ];

    const headerLength = headerItems.reduce((accumulator, headerItem) => accumulator + headerItem.length, 0);

    stream.end(`${headerLength}:${headerItems.join('')},${xml}`);

    bufferStream(stream)
      .then((data) => deserializer.deserialize(data))
      .then(resolve, reject);
  });

export const methodCallJSON = (options: net.NetConnectOpts, methodName: string, params: unknown[]) =>
  // TODO: better typings

  new Promise<any>((resolve, reject) => {
    const stream = net.connect(options);
    const request =
      methodName == 'system.multicall'
        ? (params[0] as MultiMethodCalls).map((call) => ({
            jsonrpc: '2.0',
            id: null,
            method: call.methodName,
            params: call.params,
          }))
        : {
            jsonrpc: '2.0',
            id: null,
            method: methodName,
            params,
          };

    const json = JSON.stringify(request);
    const jsonLength = Buffer.byteLength(json, 'utf8');

    stream.on('error', reject);
    stream.setEncoding('utf8');

    const headerItems = [
      `CONTENT_LENGTH${NULL_CHAR}${jsonLength}${NULL_CHAR}`,
      `CONTENT_TYPE${NULL_CHAR}application/json${NULL_CHAR}`,
      `SCGI${NULL_CHAR}1${NULL_CHAR}`,
    ];

    const headerLength = headerItems.reduce((accumulator, headerItem) => accumulator + headerItem.length, 0);

    stream.end(`${headerLength}:${headerItems.join('')},${json}`);

    bufferStream(stream)
      .then((data: string) => {
        const jsonResponse = JSON.parse(data.slice(data.lastIndexOf('\n')));
        if (Array.isArray(jsonResponse)) {
          return jsonResponse.map((response) => {
            if (response.result == null) {
              const {code, message} = response.error || {};
              throw RPCError(message, code);
            }
            return response.result;
          });
        } else {
          if (jsonResponse.result == null) {
            const {code, message} = jsonResponse.error || {};
            throw RPCError(message, code);
          }
          return jsonResponse.result;
        }
      })
      .then(resolve, reject);
  });
