/*---------------------------------------------------------------------------------------------
 *  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 JwsToken from "../crypto/protocols/jose/jws/JwsToken";

/**
 * Type to describe a claim description.
 */
export type ClaimDescription = { header: string, body: string };

/**
 * Type to describe a claim key-value pair.
 */
export type Claim = {[key: string]: string};

/**
 * Class that represents a ClaimObject
 * @class
 */
export default class ClaimObject {

  /**
   * Pointer to the claimClass that is tied to this claimObject.
   */
  public claimClass: string;

  /**
   * Context of the specific claim object
   */
  public context: string;

  /**
   * Type of the Claim Object.
   */
  public type: string;

  /**
   * Identifier of the entity that issued ClaimObject
   */
  public issuer: string;

  /**
   * A list of claim descriptions that will define each claim.
   */
  public claimDescriptions: ClaimDescription[];

  /**
   * A list of claims that are contained in this claimObject.
   */
  public claimDetails: Claim[];

  /**
   * The Claim Details if they have been signed.
   */
  public signedClaimDetails?: string;

  /**
   * Instantiates a new ClaimObject.
   * @param claimClass Pointer to the claimClass that is tied to this claimObject.
   * @param claimDescriptions A list of claim descriptions that will define each claim.
   * @param claimDetails A list of claims that are contained in this claimObject.
   */
  constructor(claimClass: string, 
              context: string,
              type: string,
              issuer: string, 
              claimDescriptions: ClaimDescription[], 
              claimDetails: Claim[],
              signedClaimDetails?: string) {
    this.claimClass = claimClass;
    this.issuer = issuer;
    this.claimDescriptions = claimDescriptions;
    this.claimDetails = claimDetails;
    this.signedClaimDetails = signedClaimDetails;
    this.context = context;
    this.type = type;
  }

  /**
   * Sign the claim details with signer.
   * @param signer Identifier that will sign the claim details.
   */
  public async signClaimDetails(signer: Identifier, keyReference: string) {
    if (signer.id !== this.issuer) {
      throw new UserAgentError(`Signer ID: ${signer.id} does not match issuer ID: ${this.issuer}`);
    }
    this.signedClaimDetails = await signer.sign(this.claimDetails, keyReference);
    console.log(`${signer.id} signed claim details`);
  }

  /**
   * If details are signed, return signed claim details.
   * Else return unsigned claim details.
   */
  public getClaimDetails() {
    if (this.signedClaimDetails) {
      return this.signedClaimDetails;
    }
    return this.claimDetails;
  }

  /**
   * Add a claim to the claim details.
   * @param {Claim} claim Claim to be added to claimDetails.
   * @param {ClaimDescription} claimDescription Optional parameter if want to also add a corresponding claim description.
   */
  public async addClaim(claim: Claim, claimDescription?: ClaimDescription) {
    this.claimDetails.push(claim);
    if (claimDescription) {
      this.claimDescriptions.push(claimDescription);
    }
  }

  /**
   * Stringify ClaimObject.
   * @returns A Stringified ClaimObject in the correct format
   */
  public serialize(): string {

    let claimDetails = {};
    if (this.signedClaimDetails) {
      claimDetails = {
        type: 'jws',
        data: this.signedClaimDetails
      };
    } else {
      claimDetails = {
        type: 'unsigned',
        data: this.claimDetails
      }
    }

    const claimObject = {
      claimClass: this.claimClass,
      '@context': this.context,
      '@type': this.type,
      claimDescriptions: this.claimDescriptions,
      claimIssuer: this.issuer,
      claimDetails
    };

    return JSON.stringify(claimObject);
  
  }

  /**
   * Create new ClaimObject from stringified JSON.
   * @param object Stringifed JSON object
   * @returns a ClaimObject
   */
  public static async deserialize(object: any): Promise<ClaimObject> {
    
    let claimObject;
    if (typeof(object) === 'string') {
      claimObject = JSON.parse(object);
    } else {
      claimObject = object;
    }

    if (!claimObject.claimDetails) {
      throw new UserAgentError(`Claim Object does not contain parameter claimDetails`);
    }

    if (!claimObject.claimClass) {
      throw new UserAgentError(`Claim Object does not contain parameter claimClass`);
    }

    if (!claimObject.claimDescriptions) {
      throw new UserAgentError(`Claim Object does not contain parameter claimDescriptions`);
    }

    if (!claimObject.claimIssuer) {
      throw new UserAgentError(`Claim Object does not contain parameter claimIssuer`);
    }

    if (!claimObject['@context'] || !claimObject['@type']) {
      throw new UserAgentError(`Claim Object is missing context`);
    }
    
    const claimDetails = claimObject.claimDetails;
    if (claimDetails.type === 'unsigned') {
      return new ClaimObject(claimObject.claimClass,
                             claimObject['@context'],
                             claimObject['@type'],
                             claimObject.claimIssuer,
                             claimObject.claimDescriptions,
                             claimDetails.data);
    } else {
      const token : JwsToken = await JwsToken.deserialize(claimDetails.data);
      const parsedClaimDetails = JSON.parse(token.payload.toString());
      return new ClaimObject(claimObject.claimClass,
                             claimObject['@context'],
                             claimObject['@type'],
                             claimObject.claimIssuer,
                             claimObject.claimDescriptions,
                             parsedClaimDetails,
                             claimDetails.data);
    }
  }
}
