/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

require('es6-promise').polyfill();
import base64url from 'base64url';
import 'isomorphic-fetch';
import Identifier from '../Identifier';
import IdentifierDocument from '../IdentifierDocument';
import UserAgentError from '../UserAgentError';
import UserAgentOptions from '../UserAgentOptions';
import IRegistrar from './IRegistrar';
import Multihash from './Multihash';
import CryptoFactory from '../crypto/plugin/CryptoFactory';
import RegistrarCryptoOptions from './RegistrarCryptoOptions';
import IPayloadProtectionOptions from '../crypto/protocols/IPayloadProtectionOptions';
import PayloadSigningStrategy from '../crypto/strategies/PayloadSigningStrategy';
import JoseProtocol from '../crypto/protocols/jose/JoseProtocol';
const cloneDeep = require('lodash/fp/cloneDeep');
declare var fetch: any;

/**
 * Registrar implementation for the Sidetree (ION) network
 */
export default class SidetreeRegistrar implements IRegistrar {
  private timeoutInMilliseconds: number;
  private options: UserAgentOptions;
  private cryptoFactory: CryptoFactory;

  /**
   * Crypto options for the Registrar
   */
  public registrarCryptoOptions: RegistrarCryptoOptions;

  /**
   * Constructs a new instance of the Sidetree registrar
   * @param url to the registration endpoint at the registrar
   * @param options to configure the registrar.
   */
  constructor (public url: string, options: UserAgentOptions) {
    // Set options. Stringify to avoid circular exception during serialization of this object.
    if (!(options && options.keyStore)) {
      throw new UserAgentError('options and options.keyStore need to be defined');
    }

    this.options = options;
    this.cryptoFactory = options.cryptoFactory;
    this.registrarCryptoOptions = options.registrar ? options.registrar.registrarCryptoOptions : new RegistrarCryptoOptions(this.cryptoFactory, new JoseProtocol());

    // Format the url
    this.url = `${url.replace(/\/?$/, '/')}`;
    this.timeoutInMilliseconds =
      1000 *
      (!options || !options.timeoutInSeconds
        ? 30
        : options.timeoutInSeconds);
  }

  /**
   * Prepare the document for registration
   * @param document Document to format
   */
  public prepareDocForRegistration (document: IdentifierDocument): IdentifierDocument {
    delete document.id;
    return document;
  }

  /**
   * Registers the identifier document on the ledger
   * returning the identifier generated by the registrar.
   * @param identifierDocument to register.
   * @param keyReference Reference to the identifier for the signing key.
   */
  public async register (
    identifierDocument: IdentifierDocument,
    keyReference: string
  ): Promise<Identifier> {

    return new Promise(async (resolve, reject) => {
      const timer = setTimeout(
        () =>
          reject(new UserAgentError('Fetch timed out.')),
        this.timeoutInMilliseconds
      );

      // prepare document for registration
      identifierDocument = this.prepareDocForRegistration(identifierDocument);
      let bodyString = JSON.stringify(identifierDocument);
      console.debug(bodyString);

      // registration with signed message for bodyString
      const encodedBodyString = await this.sign(keyReference, bodyString);

      const fetchOptions = {
        method: 'POST',
        body: encodedBodyString,
        headers: {
          'Content-Type': 'application/json',
          'Content-Length': encodedBodyString.length.toString()
        }
      };

      // Now call the actual fetch with the updated options
      const response = await fetch(this.url, fetchOptions);

      // Got a response so clear the timer
      clearTimeout(timer);

      if (!response.ok) {
        let msg: string;
        if (response.body) {
          msg = response.body.read().toString();;
        } else {
          msg = `Status ${response.status}`;
        }
        const error = new UserAgentError(
          `Failed to register the identifier document: ${msg}`
        );
        reject(error);
        return;
      }

      const responseJson = IdentifierDocument.fromJSON(await response.json());
      const identifier = new Identifier(responseJson, <UserAgentOptions> this.options);
      resolve(identifier);
    });
  }

  /**
   * Sign the registration payload
   * @param keyReference Reference to signature key
   * @param bodyString original payload to sign
   */
  public async sign(keyReference: string, bodyString: string): Promise<string> {
    const strategy: PayloadSigningStrategy = this.registrarCryptoOptions.protectionStrategy.payloadSigningStrategy;
    const options: IPayloadProtectionOptions = strategy.protectionOptions;
    const protocol = options.payloadProtection;
    const signature = await protocol.sign(keyReference, Buffer.from(bodyString), strategy.serializationFormat, options);
    return protocol.serialize(signature, strategy.serializationFormat, options);
  }

  /**
   * Uses the specified input to create a basic Sidetree
   * compliant identifier document and then hashes the document
   * in accordance with the Sidetree protocol specification
   * to generate and return the identifier.
   *
   * @param identifierDocument for which to generate the identifier.
   */
  public async generateIdentifier (identifierDocument: IdentifierDocument
  ): Promise<Identifier> {

    if (!Array.isArray(identifierDocument.publicKeys) || identifierDocument.publicKeys.length === 0) {
      throw new UserAgentError('At least one public key must be specified in the identifier document.');
    }

    // The genesis document is used for generating the hash,
    // but we need to ensure that the id property of the document
    // if specified is removed beforehand.
    const genesisDocument = cloneDeep(identifierDocument);
    genesisDocument.id = undefined;

    // Hash the document JSON
    const documentBuffer = Buffer.from(JSON.stringify(genesisDocument));
    const hashedDocument = Multihash.hash(documentBuffer, 18);
    const encodedDocument = base64url.encode(hashedDocument);

    // Now update the identifier property in
    // the genesis document
    genesisDocument.id = `did:ion:test:${encodedDocument}`;
    return new Identifier(genesisDocument);
  }
}
