import _ from 'lodash-es';

const portPattern = /^([1-9]|[1-5]?[0-9]{2,4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/m;

function parsePort(port) {
  if (portPattern.test(port)) {
    return parseInt(port, 10);
  } else {
    return 0;
  }
}

function parsePortRange(portRange) {
  if (typeof portRange !== 'string') {
    portRange = portRange.toString();
  }

  // Split the range and convert to integers
  const stringPorts = _.split(portRange, '-', 2);
  const intPorts = _.map(stringPorts, parsePort);

  // If it's not a range, we still make sure that we return two ports (start & end)
  if (intPorts.length == 1) {
    intPorts.push(intPorts[0]);
  }

  return intPorts;
}

function isValidPortRange(portRange) {
  if (typeof portRange === 'string') {
    portRange = parsePortRange();
  }

  return Array.isArray(portRange) && portRange.length === 2 && portRange[0] > 0 && portRange[1] >= portRange[0];
}

function createPortRange(portRangeText, port) {
  if (typeof portRangeText !== 'string') {
    portRangeText = portRangeText.toString();
  }

  let hostIp = null;
  const colonIndex = portRangeText.indexOf(':');
  if (colonIndex >= 0) {
    hostIp = portRangeText.substr(0, colonIndex);
    portRangeText = portRangeText.substr(colonIndex + 1);
  }

  port = typeof port === 'number' ? port : parsePort(port);
  const portRange = parsePortRange(portRangeText);
  const startPort = Math.min(portRange[0], port);
  const endPort = Math.max(portRange[1], port);

  if (hostIp) {
    return hostIp + ':' + startPort + '-' + endPort;
  } else {
    return startPort + '-' + endPort;
  }
}

angular.module('portainer.docker').factory('ContainerHelper', [
  function ContainerHelperFactory() {
    'use strict';
    var helper = {};

    helper.configFromContainer = function (container) {
      var config = container.Config;
      // HostConfig
      config.HostConfig = container.HostConfig;
      // Name
      config.name = container.Name.replace(/^\//g, '');
      // Network
      var mode = config.HostConfig.NetworkMode;
      config.NetworkingConfig = {
        EndpointsConfig: {},
      };
      config.NetworkingConfig.EndpointsConfig = container.NetworkSettings.Networks;

      if (config.ExposedPorts === undefined) {
        config.ExposedPorts = {};
      }

      if (mode.indexOf('container:') !== -1) {
        delete config.Hostname;
        delete config.ExposedPorts;
      }
      // Set volumes
      var binds = [];
      var volumes = {};
      for (var v in container.Mounts) {
        if ({}.hasOwnProperty.call(container.Mounts, v)) {
          var mount = container.Mounts[v];
          var name = mount.Name || mount.Source;
          var containerPath = mount.Destination;
          if (name && containerPath) {
            var bind = name + ':' + containerPath;
            volumes[containerPath] = {};
            if (mount.RW === false) {
              bind += ':ro';
            }
            binds.push(bind);
          }
        }
      }
      config.HostConfig.Mounts = null;
      config.HostConfig.Binds = binds;
      config.Volumes = volumes;
      return config;
    };

    helper.preparePortBindings = function (portBindings) {
      const bindings = {};
      _.forEach(portBindings, (portBinding) => {
        if (!portBinding.containerPort) {
          return;
        }

        let hostPort = portBinding.hostPort;
        const containerPortRange = parsePortRange(portBinding.containerPort);
        if (!isValidPortRange(containerPortRange)) {
          throw new Error('Invalid port specification: ' + portBinding.containerPort);
        }

        const startPort = containerPortRange[0];
        const endPort = containerPortRange[1];
        let hostIp = undefined;
        let startHostPort = 0;
        let endHostPort = 0;
        if (hostPort) {
          if (hostPort.indexOf('[') > -1) {
            const hostAndPort = _.split(hostPort, ']:');

            if (hostAndPort.length < 2) {
              throw new Error('Invalid port specification: ' + portBinding.containerPort);
            }

            hostIp = hostAndPort[0].replace('[', '');
            hostPort = hostAndPort[1];
          } else {
            if (hostPort.indexOf(':') > -1) {
              const hostAndPort = _.split(hostPort, ':');
              hostIp = hostAndPort[0];
              hostPort = hostAndPort[1];
            }
          }

          const hostPortRange = parsePortRange(hostPort);
          if (!isValidPortRange(hostPortRange)) {
            throw new Error('Invalid port specification: ' + hostPort);
          }

          startHostPort = hostPortRange[0];
          endHostPort = hostPortRange[1];
          if (endPort !== startPort && endPort - startPort !== endHostPort - startHostPort) {
            throw new Error('Invalid port specification: ' + hostPort);
          }
        }

        for (let i = 0; i <= endPort - startPort; i++) {
          const containerPort = (startPort + i).toString();
          if (startHostPort > 0) {
            hostPort = (startHostPort + i).toString();
          }
          if (startPort === endPort && startHostPort !== endHostPort) {
            hostPort += '-' + endHostPort.toString();
          }

          const bindKey = containerPort + '/' + portBinding.protocol;
          if (bindings[bindKey]) {
            bindings[bindKey].push({ HostIp: hostIp, HostPort: hostPort });
          } else {
            bindings[bindKey] = [{ HostIp: hostIp, HostPort: hostPort }];
          }
        }
      });
      return bindings;
    };

    helper.sortAndCombinePorts = function (portBindings) {
      const bindings = [];
      const portBindingKeys = _.keys(portBindings);

      // Group the port bindings by protocol
      const portBindingKeysByProtocol = _.groupBy(portBindingKeys, (portKey) => {
        return _.split(portKey, '/')[1];
      });

      _.forEach(portBindingKeysByProtocol, (portBindingKeys, protocol) => {
        // Group the port bindings by host IP
        const portBindingKeysByHostIp = {};
        for (const portKey of portBindingKeys) {
          for (const portBinding of portBindings[portKey]) {
            portBindingKeysByHostIp[portBinding.HostIp] = portBindingKeysByHostIp[portBinding.HostIp] || [];
            portBindingKeysByHostIp[portBinding.HostIp].push(portKey);
          }
        }

        _.forEach(portBindingKeysByHostIp, (portBindingKeys, ip) => {
          // Sort by host port
          const sortedPortBindingKeys = _.orderBy(portBindingKeys, (portKey) => {
            return parseInt(_.split(portKey, '/')[0], 10);
          });

          let previousHostPort = -1;
          let previousContainerPort = -1;
          _.forEach(sortedPortBindingKeys, (portKey) => {
            const portKeySplit = _.split(portKey, '/');
            const containerPort = parseInt(portKeySplit[0], 10);
            const portBinding = portBindings[portKey][0];
            portBindings[portKey].shift();
            const hostPort = parsePort(portBinding.HostPort);

            // We only combine single ports, and skip the host port ranges on one container port
            if (hostPort > 0) {
              // If we detect consecutive ports, we create a range of them
              if (bindings.length > 0 && previousHostPort === hostPort - 1 && previousContainerPort === containerPort - 1) {
                bindings[bindings.length - 1].hostPort = createPortRange(bindings[bindings.length - 1].hostPort, hostPort);
                bindings[bindings.length - 1].containerPort = createPortRange(bindings[bindings.length - 1].containerPort, containerPort);
                previousHostPort = hostPort;
                previousContainerPort = containerPort;
                return;
              }

              previousHostPort = hostPort;
              previousContainerPort = containerPort;
            } else {
              previousHostPort = -1;
              previousContainerPort = -1;
            }

            let bindingHostPort = portBinding.HostPort.toString();
            if (ip) {
              bindingHostPort = `${ip}:${bindingHostPort}`;
            }

            const binding = {
              hostPort: bindingHostPort,
              containerPort: containerPort,
              protocol: protocol,
            };
            bindings.push(binding);
          });
        });
      });

      return bindings;
    };

    helper.getContainerNames = function (containers) {
      return _.map(_.flatten(_.map(containers, 'Names')), (name) => _.trimStart(name, '/'));
    };

    return helper;
  },
]);
