'use strict';

module.exports.create = function (deps, conf) {
  var pending = {};

  async function _checkPublicAddr(host) {
    var result = await deps.request({
      method: 'GET'
    , url: deps.OAUTH3.url.normalize(host)+'/api/org.oauth3.tunnel/checkip'
    , json: true
    });

    if (!result.body) {
      throw new Error('No response body in request for public address');
    }
    if (result.body.error) {
      // Note that the error on the body will probably have a message that overwrites the default
      throw Object.assign(new Error('error in check IP response'), result.body.error);
    }
    if (!result.body.address) {
      throw new Error("public address resonse doesn't contain address: "+JSON.stringify(result.body));
    }
    return result.body.address;
  }
  async function checkPublicAddr(provider) {
    var directives = await deps.OAUTH3.discover(provider);
    return _checkPublicAddr(directives.api);
  }

  async function checkSinglePort(host, address, port) {
    var crypto = require('crypto');
    var token   = crypto.randomBytes(8).toString('hex');
    var keyAuth = crypto.randomBytes(32).toString('hex');
    pending[token] = keyAuth;

    var reqObj = {
      method: 'POST'
    , url: deps.OAUTH3.url.normalize(host)+'/api/org.oauth3.tunnel/loopback'
    , timeout: 20*1000
    , json: {
        address: address
      , port: port
      , token: token
      , keyAuthorization: keyAuth
      , iat: Date.now()
      , timeout: 18*1000
      }
    };

    var result;
    try {
      result = await deps.request(reqObj);
    } catch (err) {
      delete pending[token];
      if (conf.debug) {
        console.log('error making loopback request for port ' + port + ' loopback', err.message);
      }
      return false;
    }

    delete pending[token];
    if (!result.body) {
      if (conf.debug) {
        console.log('No response body in loopback request for port '+port);
      }
      return false;
    }
    // If the loopback requests don't go to us then there are all kinds of ways it could
    // error, but none of them really provide much extra information so we don't do
    // anything that will break the PromiseA.all out and mask the other results.
    if (conf.debug && result.body.error) {
      console.log('error on remote side of port '+port+' loopback', result.body.error);
    }
    return !!result.body.success;
  }

  async function loopback(provider) {
    var directives = await deps.OAUTH3.discover(provider);
    var address = await _checkPublicAddr(directives.api);
    if (conf.debug) {
      console.log('checking to see if', address, 'gets back to us');
    }

    var ports = require('../servers').listeners.tcp.list();
    var values = await deps.PromiseA.all(ports.map(function (port) {
      return checkSinglePort(directives.api, address, port);
    }));

    if (conf.debug && Object.keys(pending).length) {
      console.log('remaining loopback tokens', pending);
    }

    return {
      address: address
    , ports: ports.reduce(function (obj, port, ind) {
        obj[port] = values[ind];
        return obj;
      }, {})
    };
  }

  loopback.checkPublicAddr = checkPublicAddr;
  loopback.server = require('http').createServer(function (req, res) {
    var parsed = require('url').parse(req.url);
    var token = parsed.pathname.replace('/.well-known/cloud-challenge/', '');
    if (pending[token]) {
      res.setHeader('Content-Type', 'text/plain');
      res.end(pending[token]);
    } else {
      res.statusCode = 404;
      res.end();
    }
  });

  return loopback;
};