/*!
 * jws/verify.js - Verifies from a JWS
 *
 * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file.
 */
"use strict";

var clone = require("lodash/clone"),
    merge = require("../util/merge"),
    base64url = require("../util/base64url"),
    AlgConfig = require("../util/algconfig"),
    JWK = require("../jwk");

var DEFAULT_OPTIONS = {
  algorithms: "*",
  allowEmbeddedKey: false
};

/**
 * @class JWS.Verifier
 * @classdesc Parser of signed content.
 *
 * @description
 * **NOTE:** this class cannot be instantiated directly. Instead call {@link
 * JWS.createVerify}.
 */
var JWSVerifier = function(ks, globalOpts) {
  var assumedKey,
      keystore;

  if (JWK.isKey(ks)) {
    assumedKey = ks;
    keystore = assumedKey.keystore;
  } else if (JWK.isKeyStore(ks)) {
    keystore = ks;
  } else {
    keystore = JWK.createKeyStore();
  }

  globalOpts = merge(DEFAULT_OPTIONS, globalOpts);

  Object.defineProperty(this, "defaultKey", {
    value: assumedKey || undefined,
    enumerable: true
  });
  Object.defineProperty(this, "keystore", {
    value: keystore,
    enumerable: true
  });

  Object.defineProperty(this, "verify", {
    value: function(input, opts) {
      opts = merge({}, globalOpts, opts || {});
      var extraHandlers = opts.handlers || {};
      var handlerKeys = Object.keys(extraHandlers);
      var algSpec = new AlgConfig(opts.algorithms);

      if ("string" === typeof input) {
        input = input.split(".");
        input = {
          payload: input[1],
          signatures: [
            {
              protected: input[0],
              signature: input[2]
            }
          ]
        };
      } else if (!input || "object" !== typeof input) {
        throw new Error("invalid input");
      }

      // fixup "flattened JSON" to look like "general JSON"
      if (input.signature) {
        input.signatures = [
          {
            protected: input.protected || undefined,
            header: input.header || undefined,
            signature: input.signature
          }
        ];
      }

      // ensure signatories exists
      var sigList = input.signatures || [{}];

      // combine fields and decode signature per signatory
      sigList = sigList.map(function(s) {
        var header = clone(s.header || {});
        var protect = s.protected ?
                      JSON.parse(base64url.decode(s.protected, "utf8")) :
                      {};
        header = merge(header, protect);
        var signature = base64url.decode(s.signature);

        // process allowed algorithims
        if (!algSpec.match(header.alg)) {
          return Promise.reject(new Error("Algorithm not allowed: " + header.alg));
        }

        // process "crit" first
        var crit = protect.crit;
        if (crit) {
          if (!Array.isArray(crit)) {
            return Promise.reject(new Error("Invalid 'crit' header"));
          }
          for (var idx = 0; crit.length > idx; idx++) {
            if (-1 === handlerKeys.indexOf(crit[idx])) {
              return Promise.reject(new Error(
                  "Critical extension is not supported: " + crit[idx]
              ));
            }
          }
        }
        protect = Object.keys(protect);

        return Promise.resolve({
          protected: protect,
          aad: s.protected || "",
          header: header,
          signature: signature
        });
      });

      var promise = Promise.all(sigList);
      promise = promise.then(function(sigList) {
        return new Promise(function(resolve, reject) {
          var processSig = function() {
            var sig = sigList.shift();
            if (!sig) {
              reject(new Error("no key found"));
              return;
            }

            sig = merge({}, sig, {
              payload: input.payload
            });
            var p = Promise.resolve(sig);
            // find the key
            p = p.then(function(sig) {
              var algKey;
              // TODO: resolve jku, x5c, x5u
              if (opts.allowEmbeddedKey && sig.header.jwk) {
                algKey = JWK.asKey(sig.header.jwk);
              } else if (opts.allowEmbeddedKey && sig.header.x5c) {
                algKey = sig.header.x5c[0];
                algKey = Buffer.from(algKey, "base64");
                // TODO: callback to validate chain
                algKey = JWK.asKey(algKey, "pkix");
              } else {
                algKey = Promise.resolve(assumedKey || keystore.get({
                  use: "sig",
                  alg: sig.header.alg,
                  kid: sig.header.kid
                }));
              }
              return algKey.then(function(k) {
                if (!k) {
                  return Promise.reject(new Error("key does not match"));
                }
                sig.key = k;
                return sig;
              });
            });

            // process any prepare-verify handlers
            p = p.then(function(sig) {
              var processing = [];
              handlerKeys.forEach(function(h) {
                h = extraHandlers[h];
                var p;
                if ("function" === typeof h) {
                  p = h(sig);
                } else if ("object" === typeof h && "function" === typeof h.prepare) {
                  p = h.prepare(sig);
                }
                if (p) {
                  processing.push(Promise.resolve(p));
                }
              });
              return Promise.all(processing).then(function() {
                // don't actually care about individual handler results
                // assume {sig} is updated
                return sig;
              });
            });

            // prepare verify inputs
            p = p.then(function(sig) {
              var aad = sig.aad || "",
                  payload = sig.payload || "";
              var content = Buffer.alloc(1 + aad.length + payload.length),
                  pos = 0;
              content.write(aad, pos, "ascii");
              pos += aad.length;
              content.write(".", pos, "ascii");
              pos++;

              if (Buffer.isBuffer(payload)) {
                payload.copy(content, pos);
              } else {
                content.write(payload, pos, "binary");
              }
              sig.content = content;
              return sig;
            });

            p = p.then(function(sig) {
              return sig.key.verify(sig.header.alg,
                                    sig.content,
                                    sig.signature);
            });

            p = p.then(function(result) {
              var payload = sig.payload;
              payload = base64url.decode(payload);
              return {
                protected: sig.protected,
                header: sig.header,
                payload: payload,
                signature: result.mac,
                key: sig.key
              };
            });

            // process any post-verify handlers
            p = p.then(function(jws) {
              var processing = [];
              handlerKeys.forEach(function(h) {
                h = extraHandlers[h];
                var p;
                if ("object" === typeof h && "function" === typeof h.complete) {
                  p = h.complete(jws);
                }
                if (p) {
                  processing.push(Promise.resolve(p));
                }
              });
              return Promise.all(processing).then(function() {
                // don't actually care about individual handler results
                // assume {jws} is updated
                return jws;
              });
            });
            p.then(resolve, processSig);
          };
          processSig();
        });
      });
      return promise;
    }
  });
};

/**
 * @description
 * Creates a new JWS.Verifier with the given Key or KeyStore.
 *
 * @param {JWK.Key|JWK.KeyStore} ks The Key or KeyStore to use for verification.
 * @returns {JWS.Verifier} The new Verifier.
 */
function createVerify(ks, opts) {
  var vfy = new JWSVerifier(ks, opts);

  return vfy;
}

module.exports = {
  verifier: JWSVerifier,
  createVerify: createVerify
};
