/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import Identifier from '../Identifier';
import UserAgentError from '../UserAgentError';
import ClaimObject from '../credentials/ClaimObject';
import OidcRequest from './OidcRequest';
import UserAgentOptions from '../UserAgentOptions';
import HttpResolver from '../resolvers/HttpResolver';
import VerifyHelper from './VerifyHelper';
import CryptoFactory from '../crypto/plugin/CryptoFactory';
import CryptoOptions from '../CryptoOptions';
import JwsToken from '../crypto/protocols/jose/jws/JwsToken';
import OIDCAuthenticationResponse from '../crypto/protocols/did/responses/OIDCAuthenticationResponse';
import PublicKey from '../crypto/keys/PublicKey';
import nodeFetch from 'node-fetch';

/**
 * Optional Parameters to add to OIDCRequests.
 */
export type OptionalOIDCResponseParams = {
  /**
   * Opaque value to represent state on serverside.
   */
  state?: string;

  /**
   * ClaimObject if OIDC Request involves issuance of credential.
   */
  presentedClaimObjects?: ClaimObject[];
};

/**
 * Standard iss for SIOP Response.
 */
const iss = 'https://self-issued.me';

export default class OidcResponse {

  /**
   * Redirect URL for SIOP.
   */
  public redirectUrl: string;

  /**
   * Nonce for SIOP.
   */
  public nonce: string;

  /**
   * Sender Identifier of the request.
   */
  public sender: Identifier;

  /**
   * Optional State 
   */
  public state?: string;

  /**
   * claimObjects that were requested in the OIDC Request.
   */
  public presentedClaimObjects?: ClaimObject[];

  /**
   * Private Constructor for creating OidcResponse Object.
   * @param {Identifier} sender Identifier who is sending response.
   * @param {string} nonce Nonce from request.
   * @param {string} redirectUrl RedirectURL from request.
   * @param {OptionalOIDCResponseParams} options optional parameters for specific responses.
   */
  private constructor(sender: Identifier, nonce: string, redirectUrl: string, options?: OptionalOIDCResponseParams) {
    this.redirectUrl = redirectUrl;
    this.nonce = nonce;
    this.sender = sender;
    this.state = options!.state;
    this.presentedClaimObjects = options!.presentedClaimObjects;
  }

  /**
   * Create an OIDC Response from an OIDC Request Object.
   * @param {OidcRequest} oidcRequest OIDC Request that response is responding to.
   * @param {Identifier} sender The Sender Identifier of the OIDC Response.
   * @returns {OidcResponse}
   */
  public static async create(oidcRequest: OidcRequest, sender: Identifier) {
    if (!oidcRequest.state) {
      return new OidcResponse(sender, oidcRequest.nonce, oidcRequest.redirectUrl);
    }
    return new OidcResponse(sender, oidcRequest.nonce, oidcRequest.redirectUrl, { state: oidcRequest.state });
  }

  /**
   * sign oidcRequest and send compact JWT to redirectUrl.
   * @param {string} keyReference reference to signing key.
   * @param {ClaimObject[]} claimObjects any claims that were asked for.
   * @returns the body of the httpResponse if status is 200.
   */
  public async signAndSend(keyReference: string, claimObjects?: ClaimObject[]): Promise<any> {

    // check if sender has keystore
    if (!this.sender.options || !this.sender.options.keyStore) {
      throw new UserAgentError(`No KeyStore specified for '${this.sender.id}`);
    }

    // get public key of key referenced by keyReference in keystore
    const publicKey = (await this.sender.options.keyStore.get(keyReference, true)).getKey<PublicKey>();

    // milliseconds to seconds
    const milliseconds = 1000;
    const expirationTimeOffsetInMinutes = 5;
    const expiration = new Date(Date.now() - milliseconds * 60 * expirationTimeOffsetInMinutes); // 5 minutes from now
    const exp = Math.floor(expiration.getTime() / milliseconds);
    const iat = Math.floor(Date.now() / milliseconds); // ms to seconds

    const response = {
      iss,
      sub: await PublicKey.getThumbprint(publicKey),
      aud: this.redirectUrl,
      nonce: this.nonce,
      did: this.sender.id,
      sub_jwk: publicKey,
      iat,
      exp
    };

    const claimSources: {[key: string]: any} = {};
    const claimNames: {[key: string]: string} = {};
    let index = 1;

    // if state exists, add it to response.
    if (this.state) {
      Object.assign(response, {state: this.state});
    }

    // for presentation of claims.
    if (claimObjects) {
      const claim = claimObjects[0];
      const signedPresentationDetails = await this.sender.sign(claim.serialize(), keyReference);
      const src = `src${index}`;
      claimNames[claim.claimClass] = src;
      claimSources[src] = [{
        'JWT': signedPresentationDetails
      }]
      index++;
      Object.assign(response, { '_claim_names': claimNames });
      Object.assign(response, { '_claim_sources': claimSources });
    }

    const signedResponse = await this.sender.sign(response, keyReference);
    return this.sendResponse(signedResponse, this.redirectUrl, this.state);
  }

  /**
   * Parses and Verifies signed JWT.
   * @param {string} signedResponse signed JWT containing OIDC response.
   * @returns {OidcResponse} Object if verified.
   */
  public static async verifyAndParse(signedResponse: string, optionalCryptoFactory?: CryptoFactory): Promise<OidcResponse> {
    // get cryptoFactory or create one
    let cryptoFactory: CryptoFactory;
    if (!optionalCryptoFactory) {
      cryptoFactory = new CryptoOptions().cryptoFactory;
    } else {
      cryptoFactory = optionalCryptoFactory;
    }

    // get identifier id from did.
    const token : JwsToken = await JwsToken.deserialize(signedResponse, {cryptoFactory});
    const response = JSON.parse(token.payload.toString());
    const senderId = response.did;

    // create new identifier and verify Auth Response
    const options = new UserAgentOptions();
    options.resolver = new HttpResolver('https://beta.discover.did.microsoft.com');
    const sender = new Identifier(senderId, options);

    // if (!await VerifyHelper.verify(sender, token)) {
    console.log('not verifying for now');
      // throw new UserAgentError(`Invalid signature for token issued by: ${response.did}`);
    // }

    // check exp
    const milliseconds = 1000;
    // set a leeway of 5 minutes.
    const expirationCheckTimeOffsetInMinutes = 5;
    const expirationCheck = new Date(Date.now() - milliseconds * 60 * expirationCheckTimeOffsetInMinutes); // 5 minutes from now
    const expCheck = Math.floor(expirationCheck.getTime() / milliseconds);
    if (response.exp < expCheck) {
      throw new UserAgentError(`Token from id: ${senderId} has expired`);
    }

    const claimNames: {[key: string]: string} = response['_claim_names'];
    const claimSources: {[key: string]: {JWT: string}[]} = response['_claim_sources'];
    let claimObjects: ClaimObject[] = [];

    for (var name in claimNames) {
      // 1. get src name (ex. src1)
      const src = claimNames[name];
      const srcArray = claimSources[src];
      const claimObjectsFromSource = await OidcResponse.getClaimObjects(srcArray, sender, cryptoFactory);
      claimObjects = claimObjects.concat(claimObjectsFromSource);
    }

    if (claimObjects !== []) {
      return new OidcResponse(sender, response.nonce, response.redirectUrl, { state: response.state, presentedClaimObjects: claimObjects });
    }
    return new OidcResponse(sender, response.nonce, response.redirectUrl, { state: response.state });
  }

  /**
   * Helper method to parse for ClaimObjects in OIDC Response.
   * @param sourceArray source array that contain claims
   * @param {Identifier} sender Identifer that represents entity that sent request.
   * @param {CryptoFactory} cryptoFactory cryptoFactory used for crypto operations.
   */
  private static async getClaimObjects(sourceArray: {JWT: string}[],
                                sender: Identifier,
                                cryptoFactory: CryptoFactory): Promise<ClaimObject[]> {
    console.log(sender.id);
    const claimObjects: ClaimObject[] = [];
    // Assuming only asking for one claim for now.
    const jwtBinding = sourceArray[0];
    // 1. JWT is signed by entity that owns claim (same as entity that sent auth response).
    const signedClaim = jwtBinding.JWT;
    const claimToken : JwsToken = await JwsToken.deserialize(signedClaim, {cryptoFactory});
    const claimObject = await ClaimObject.deserialize(claimToken.payload.toString());
    // 2. Verify signature.
    // if (!await VerifyHelper.verify(sender, claimToken)) {
    console.log('not verifying for now');
      // throw new UserAgentError(`Presented Claim: ${claimObject.claimClass} not verified`);
    // }
    // 3. get issuer id from claimObject.
    const issuerId = claimObject.issuer;
    const options = new UserAgentOptions();
    options.resolver = new HttpResolver('https://beta.discover.did.microsoft.com');
    // 4. create new issuer Identifier.
    const issuer = new Identifier(issuerId, options);
    // 5. Verify that issuer signed the claimDetails.
    const claimDetailsToken : JwsToken = await JwsToken.deserialize(signedClaim, {cryptoFactory});
    // if (!await VerifyHelper.verify(issuer, claimDetailsToken)) {
      // console.log('not verifying for now');
      // throw new UserAgentError(`Presented Claim: ${claimObject.claimClass} not verified`);
    // }
    // 6. Add claimObject to array of presentedClaims.
    claimObjects.push(claimObject);
    return claimObjects;
  }

  /**
   * Send response to redirect url and 
   * 
   * @param {string} response OIDC Response 
   * @param {string} url url to send the response to.
   * @param {string} state optional state parameter
   */
  private async sendResponse(response: string, url: string, state?: string) {
    let responseBody = `id_token=${response}`;
    if (state) {
      responseBody = `${responseBody}&state=${state}`;
    }
    const oidcResponse = await nodeFetch(url, {
      method: 'POST',
      body: responseBody,
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Content-Length': responseBody.length.toString()
      },
    });
    if (oidcResponse.status !== 200) {
      throw new Error(`OpenID Connect response failed to send: ${oidcResponse.text()}`);
    }
    return oidcResponse.json();
  }
}
