'use strict';

var PromiseA = require('bluebird');

function generateRescope(req, Models, decoded, fullPpid, ppid) {
  return function (/*sub*/) {
    // TODO: this function is supposed to convert PPIDs of different parties to some account
    // ID that allows application to keep track of permisions and what-not.
    console.log('[rescope] Attempting ', fullPpid);
    return Models.IssuerOauth3OrgGrants.find({ azpSub: fullPpid }).then(function (results) {
      if (results[0]) {
        console.log('[rescope] lukcy duck: got it on the 1st try');
        return PromiseA.resolve(results);
      }

      // XXX BUG XXX
      // should be able to distinguish between own ids and 3rd party via @whatever.com
      return Models.IssuerOauth3OrgGrants.find({ azpSub: ppid });
    }).then(function (results) {
      var result = results[0];

      if (!result || !result.sub || !decoded.iss) {
        // XXX BUG XXX TODO swap this external ppid for an internal (and ask user to link with existing profile)
        //req.oauth3.accountIdx = fullPpid;
        throw new Error("internal / external ID swapping not yet implemented. TODO: "
          + "No profile found with that credential. Would you like to create a new profile or link to an existing profile?");
      }

      // XXX BUG XXX need to pass own url in to use as issuer for own tokens
      req.oauth3.accountIdx = result.sub + '@' + decoded.iss;

      console.log('[rescope] result:');
      console.log(results);
      console.log(req.oauth3.accountIdx);

      return PromiseA.resolve(req.oauth3.accountIdx);
    });
  };
}

function extractAccessToken(req) {
  var token = null;
  var parts;
  var scheme;
  var credentials;

  if (req.headers && req.headers.authorization) {
    // Works for all of Authorization: Bearer {{ token }}, Token {{ token }}, JWT {{ token }}
    parts = req.headers.authorization.split(' ');

    if (parts.length !== 2) {
      return PromiseA.reject(new Error("malformed Authorization header"));
    }

    scheme = parts[0];
    credentials = parts[1];

    if (-1 !== ['token', 'bearer'].indexOf(scheme.toLowerCase())) {
      token = credentials;
    }
  }

  if (req.body && req.body.access_token) {
    if (token) { PromiseA.reject(new Error("token exists in header and body")); }
    token = req.body.access_token;
  }

  // TODO disallow query with req.method === 'GET'
  // NOTE: the case of DDNS on routers requires a GET and access_token
  // (cookies should be used for protected static assets)
  if (req.query && req.query.access_token) {
    if (token) { PromiseA.reject(new Error("token already exists in either header or body and also in query")); }
    token = req.query.access_token;
  }

  /*
  err = new Error(challenge());
  err.code = 'E_BEARER_REALM';

  if (!token) { return PromiseA.reject(err); }
  */

  return PromiseA.resolve(token);
}

function verifyToken(token) {
  var jwt = require('jsonwebtoken');
  var decoded;

  if (!token) {
    return PromiseA.reject({
      message: 'no token provided'
    , code: 'E_NO_TOKEN'
    , url: 'https://oauth3.org/docs/errors#E_NO_TOKEN'
    });
  }

  try {
    decoded = jwt.decode(token, {complete: true});
  } catch (e) {}
  if (!decoded) {
    return PromiseA.reject({
      message: 'provided token not a JSON Web Token'
    , code: 'E_NOT_JWT'
    , url: 'https://oauth3.org/docs/errors#E_NOT_JWT'
    });
  }

  var sub = decoded.payload.sub || decoded.payload.ppid || decoded.payload.appScopedId;
  if (!sub) {
    return PromiseA.reject({
      message: 'token missing sub'
    , code: 'E_MISSING_SUB'
    , url: 'https://oauth3.org/docs/errors#E_MISSING_SUB'
    });
  }
  var kid = decoded.header.kid || decoded.payload.kid;
  if (!kid) {
    return PromiseA.reject({
      message: 'token missing kid'
    , code: 'E_MISSING_KID'
    , url: 'https://oauth3.org/docs/errors#E_MISSING_KID'
    });
  }
  if (!decoded.payload.iss) {
    return PromiseA.reject({
      message: 'token missing iss'
    , code: 'E_MISSING_ISS'
    , url: 'https://oauth3.org/docs/errors#E_MISSING_ISS'
    });
  }

  var OAUTH3 = require('oauth3.js');
  OAUTH3._hooks = require('oauth3.js/oauth3.node.storage.js');
  return OAUTH3.discover(decoded.payload.iss).then(function (directives) {
    var args = (directives || {}).retrieve_jwk;
    if (typeof args === 'string') {
      args = { url: args, method: 'GET' };
    }
    if (typeof (args || {}).url !== 'string') {
      return PromiseA.reject({
        message: 'token issuer does not support retrieving JWKs'
      , code: 'E_INVALID_ISS'
      , url: 'https://oauth3.org/docs/errors#E_INVALID_ISS'
      });
    }

    var params = {
      sub: sub
    , kid: kid
    };
    var url = args.url;
    var body;
    Object.keys(params).forEach(function (key) {
      if (url.indexOf(':'+key) !== -1) {
        url = url.replace(':'+key, params[key]);
        delete params[key];
      }
    });
    if (Object.keys(params).length > 0) {
      if ('GET' === (args.method || 'GET').toUpperCase()) {
        url += '?' + OAUTH3.query.stringify(params);
      } else {
        body = params;
      }
    }

    return OAUTH3.request({
      url: OAUTH3.url.resolve(directives.api, url)
    , method: args.method
    , data: body
    }).catch(function (err) {
      return PromiseA.reject({
        message: 'failed to retrieve public key from token issuer'
      , code: 'E_NO_PUB_KEY'
      , url: 'https://oauth3.org/docs/errors#E_NO_PUB_KEY'
      , subErr: err.toString()
      });
    });
  }, function (err) {
    return PromiseA.reject({
      message: 'token issuer is not a valid OAuth3 provider'
    , code: 'E_INVALID_ISS'
    , url: 'https://oauth3.org/docs/errors#E_INVALID_ISS'
    , subErr: err.toString()
    });
  }).then(function (res) {
    if (res.data.error) {
      return PromiseA.reject(res.data.error);
    }
    var opts = {};
    if (Array.isArray(res.data.alg)) {
      opts.algorithms = res.data.alg;
    } else if (typeof res.data.alg === 'string') {
      opts.algorithms = [res.data.alg];
    }

    try {
      return jwt.verify(token, require('jwk-to-pem')(res.data), opts);
    } catch (err) {
      return PromiseA.reject({
        message: 'token verification failed'
      , code: 'E_INVALID_TOKEN'
      , url: 'https://oauth3.org/docs/errors#E_INVALID_TOKEN'
      , subErr: err.toString()
      });
    }
  });
}

function deepFreeze(obj) {
  Object.keys(obj).forEach(function (key) {
    if (obj[key] && typeof obj[key] === 'object') {
      deepFreeze(obj[key]);
    }
  });
  Object.freeze(obj);
}

function cookieOauth3(Models, req, res, next) {
  req.oauth3 = {};

  var token = req.cookies.jwt;

  req.oauth3.encodedToken = token;
  req.oauth3.verifyAsync = function (jwt) {
    return verifyToken(jwt || token);
  };

  return verifyToken(token).then(function  (decoded) {
    req.oauth3.token = decoded;
    if (!decoded) {
      return null;
    }

    var ppid = decoded.sub || decoded.ppid || decoded.appScopedId;
    req.oauth3.ppid = ppid;
    req.oauth3.accountIdx = ppid+'@'+decoded.iss;

    var hash = require('crypto').createHash('sha256').update(req.oauth3.accountIdx).digest('base64');
    hash = hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+/g, '');
    req.oauth3.accountHash = hash;

    req.oauth3.rescope = generateRescope(req, Models, decoded, fullPpid, ppid);
  }).then(function () {
    deepFreeze(req.oauth3);
    //Object.defineProperty(req, 'oauth3', {configurable: false, writable: false});
    next();
  }, function (err) {
    if ('E_NO_TOKEN' === err.code) {
      next();
      return;
    }
    console.error('[walnut] cookie lib/oauth3 error:');
    console.error(err);
    res.send(err);
  });
}

function attachOauth3(Models, req, res, next) {
  req.oauth3 = {};

  extractAccessToken(req).then(function (token) {
    req.oauth3.encodedToken = token;
    req.oauth3.verifyAsync = function (jwt) {
      return verifyToken(jwt || token);
    };

    if (!token) {
      return null;
    }
    return verifyToken(token);
  }).then(function  (decoded) {
    req.oauth3.token = decoded;
    if (!decoded) {
      return null;
    }

    var ppid = decoded.sub || decoded.ppid || decoded.appScopedId;
    var fullPpid = ppid+'@'+decoded.iss;
    req.oauth3.ppid = ppid;

    // TODO we can anonymize the relationship between our user as the other service's user
    // in our own database by hashing the remote service's ppid and using that as the lookup
    var hash = require('crypto').createHash('sha256').update(fullPpid).digest('base64');
    hash = hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+/g, '');
    req.oauth3.accountHash = hash;

    req.oauth3.rescope = generateRescope(req, Models, decoded, fullPpid, ppid);

    console.log('############### assigned req.oauth3:');
    console.log(req.oauth3);
  }).then(function () {
    //deepFreeze(req.oauth3);
    //Object.defineProperty(req, 'oauth3', {configurable: false, writable: false});
    next();
  }, function (err) {
    console.error('[walnut] JWT lib/oauth3 error:');
    console.error(err);
    res.send(err);
  });
}

module.exports.attachOauth3 = attachOauth3;
module.exports.cookieOauth3 = cookieOauth3;
module.exports.verifyToken = verifyToken;
