const { URL } = require('node:url');
const QS = require('node:querystring');
const net = require('node:net');
const tls = require('node:tls');
const Connection = require('./connection').Connection;
const fmt = require('node:util').format;
const credentials = require('./credentials');

function copyInto(obj, target) {
  const keys = Object.keys(obj);
  let i = keys.length;
  while (i--) {
    const k = keys[i];
    target[k] = obj[k];
  }
  return target;
}

// Adapted from util._extend, which is too fringe to use.
function clone(obj) {
  return copyInto(obj, {});
}

const CLIENT_PROPERTIES = {
  product: 'amqplib',
  version: require('../package.json').version,
  platform: fmt('Node.JS %s', process.version),
  information: 'https://amqp-node.github.io/amqplib/',
  capabilities: {
    publisher_confirms: true,
    exchange_exchange_bindings: true,
    'basic.nack': true,
    consumer_cancel_notify: true,
    'connection.blocked': true,
    authentication_failure_close: true,
  },
};

// Construct the main frames used in the opening handshake
function openFrames(vhost, query, credentials, extraClientProperties) {
  if (!vhost) vhost = '/';
  else vhost = QS.unescape(vhost);

  query = query || {};

  function intOrDefault(val, def) {
    return val === undefined ? def : parseInt(val, 10);
  }

  const clientProperties = Object.create(CLIENT_PROPERTIES);

  return {
    // start-ok
    clientProperties: copyInto(extraClientProperties, clientProperties),
    mechanism: credentials.mechanism,
    response: credentials.response(),
    locale: query.locale || 'en_US',

    // tune-ok
    channelMax: intOrDefault(query.channelMax, 0),
    frameMax: intOrDefault(query.frameMax, 131072),
    heartbeat: query.heartbeat != null ? intOrDefault(query.heartbeat, 0) : null,

    // open
    virtualHost: vhost,
    capabilities: '',
    insist: 0,
  };
}

// Decide on credentials based on what we're supplied.
function credentialsFromUrl(parts) {
  let user = 'guest';
  let passwd = 'guest';
  if (parts.username !== '' || parts.password !== '') {
    user = parts.username ? unescape(parts.username) : '';
    passwd = parts.password ? unescape(parts.password) : '';
  }
  return credentials.plain(user, passwd);
}

function connect(url, socketOptions, openCallback) {
  // tls.connect uses `util._extend()` on the options given it, which
  // copies only properties mentioned in `Object.keys()`, when
  // processing the options. So I have to make copies too, rather
  // than using `Object.create()`.
  const sockopts = clone(socketOptions || {});
  url = url || 'amqp://localhost';

  const noDelay = !!sockopts.noDelay;
  const timeout = sockopts.timeout;
  const keepAlive = !!sockopts.keepAlive;
  // 0 is default for node
  const keepAliveDelay = sockopts.keepAliveDelay || 0;

  const extraClientProperties = sockopts.clientProperties || {};

  let protocol;
  let fields;
  if (typeof url === 'object') {
    protocol = `${url.protocol || 'amqp'}:`;
    sockopts.host = url.hostname;
    if (!sockopts.servername && !net.isIP(url.hostname)) sockopts.servername = url.hostname;
    sockopts.port = url.port || (protocol === 'amqp:' ? 5672 : 5671);

    let user;
    let pass;
    // Only default if both are missing, to have the same behaviour as
    // the stringly URL.
    if (url.username === undefined && url.password === undefined) {
      user = 'guest';
      pass = 'guest';
    } else {
      user = url.username || '';
      pass = url.password || '';
    }

    const config = {
      locale: url.locale,
      channelMax: url.channelMax,
      frameMax: url.frameMax,
      heartbeat: url.heartbeat,
    };

    fields = openFrames(url.vhost, config, sockopts.credentials || credentials.plain(user, pass), extraClientProperties);
  } else {
    let parts;
    try {
      parts = new URL(url);
    } catch (_e) {
      parts = {};
    }
    const host = (parts.hostname || '').replace(/^\[|\]$/g, '');
    protocol = parts.protocol || '';
    sockopts.host = host;
    if (!sockopts.servername && !net.isIP(host)) sockopts.servername = host;
    sockopts.port = parseInt(parts.port, 10) || (protocol === 'amqp:' ? 5672 : 5671);
    const vhost = parts.pathname ? parts.pathname.substr(1) : null;
    const query = QS.parse(parts.search ? parts.search.slice(1) : '');
    // QS.parse returns arrays for duplicate keys; match url-parse's first-value-wins behavior.
    for (const key in query) {
      if (Array.isArray(query[key])) query[key] = query[key][0];
    }
    fields = openFrames(vhost, query, sockopts.credentials || credentialsFromUrl(parts), extraClientProperties);
  }

  let sockok = false;
  let sock;

  function onConnect() {
    sockok = true;
    sock.setNoDelay(noDelay);
    if (keepAlive) sock.setKeepAlive(keepAlive, keepAliveDelay);

    const c = new Connection(sock);
    c.open(fields, (err, _ok) => {
      // disable timeout once the connection is open, we don't want
      // it fouling things
      if (timeout) sock.setTimeout(0);
      if (err === null) {
        openCallback(null, c);
      } else {
        // The connection isn't closed by the server on e.g. wrong password
        sock.end();
        sock.destroy();
        openCallback(err);
      }
    });
  }

  if (protocol === 'amqp:') {
    sock = net.connect(sockopts, onConnect);
  } else if (protocol === 'amqps:') {
    sock = tls.connect(sockopts, onConnect);
  } else {
    throw new Error(`Expected amqp: or amqps: as the protocol; got ${protocol}`);
  }

  if (timeout) {
    sock.setTimeout(timeout, () => {
      sock.end();
      sock.destroy();
      openCallback(new Error('connect ETIMEDOUT'));
    });
  }

  sock.once('error', (err) => {
    if (!sockok) openCallback(err);
  });
}

module.exports.connect = connect;
module.exports.credentialsFromUrl = credentialsFromUrl;
