import { Injectable } from "@angular/core";
import { TwitterApiProvider } from "../twitter-api/twitter-api";
import { P2pStorageIpfsProvider } from "../p2p-storage-ipfs/p2p-storage-ipfs";
import { Storage } from "@ionic/storage";
import NodeRSA from "node-rsa";
declare var TextDecoder: any;
declare var TextEncoder: any;
@Injectable()
export class CryptoProvider {
  ownUserId: string;
  IV_LENGTH = 12;
  HYBRID_OSN_AES_KEY = "Z1vxAULQnZdoWhJOvv+hWEvVpyUHzNjD/ichEE2c8i4=";

  constructor(
    private twitter: TwitterApiProvider,
    private ipfs: P2pStorageIpfsProvider,
    private storage: Storage
  ) {
    this.init();
  }

  private async init() {
    this.ownUserId = await this.storage.get("userId");
  }

  public async publishPublicKey(key: string) {
    let publicKeyHistory = await this.getKeyHistory(this.ownUserId);

    // Todo: avoid publishing the same public key twice - check if new key equals newest key in history

    // Add new key to history
    const newKey = {
      key: key,
      validFrom: Date.now()
    };

    if (publicKeyHistory) {
      publicKeyHistory["keys"].push(newKey);
    } else {
      publicKeyHistory = {
        keys: [newKey]
      };
    }

    // Ecnrypt key history
    const encryptedPublicKeyHistory = await this.aesEncrypt(
      JSON.stringify(publicKeyHistory)
    );

    // Publish updated key history...
    const res = await this.ipfs.storePublicKey(encryptedPublicKeyHistory);

    // tweet ipfs link
    const tweetResponse = await this.twitter.tweet(
      "ipfs://" + res["Hash"] + " #hybridOSN"
    );

    // ... and update description in user profile with tweet id
    this.twitter.updateProfileDescription(
      "tweet://" + tweetResponse["data"]["id_str"] + " #hybridOSN"
    );
  }

  private async getKeyHistory(userId: string) {
    // Get user description
    const userData = await this.twitter.fetchUser(userId);
    const profileDescription = userData["description"];

    // Get tweet with link to key history
    const tweetId = this.extractTweetId(profileDescription);
    if (tweetId.length === 0) {
      return null;
    }
    const tweetWithKeyHistoryLink = await this.twitter.fetchTweet(tweetId);

    // Extract link to public key
    const link = this.extractLinkFromDescription(
      tweetWithKeyHistoryLink["data"]["full_text"]
    );

    // Fetch public key history
    if (link.length) {
      const encryptedKeyHistory = await this.ipfs.fetchJson(link);
      // Decrypt key history
      const keyHistory = await this.aesDecrypt(encryptedKeyHistory.toString());
      return JSON.parse(keyHistory);
    } else {
      return null;
    }
  }

  private extractTweetId(text: string): string {
    for (let word of text.split(" ")) {
      if (this.isTweetId(word)) {
        return word.substr(8);
      }
    }
    return "";
  }

  private extractLinkFromDescription(text: string): string {
    for (let word of text.split(" ")) {
      if (this.isIpfsLink(word)) {
        return word.substr(7);
      }
    }
    return "";
  }

  private isIpfsLink(word: string): boolean {
    return /ipfs:\/\/Qm[a-zA-Z0-9]+/.test(word);
  }

  private isTweetId(word: string): boolean {
    return /tweet:\/\/[0-9]+/.test(word);
  }

  public async generateRsaKeys() {
    return await crypto.subtle.generateKey(
      {
        name: "RSA-OAEP",
        modulusLength: 1024,
        publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
        hash: { name: "SHA-256" }
      },
      true,
      ["encrypt", "decrypt"]
    );
  }

  public async extractPrivateKey(keys): Promise<string> {
    return btoa(
      String.fromCharCode.apply(
        null,
        new Uint8Array(await crypto.subtle.exportKey("pkcs8", keys.privateKey))
      )
    );
  }

  public async extractPublicKey(keys): Promise<string> {
    return btoa(
      String.fromCharCode.apply(
        null,
        new Uint8Array(await crypto.subtle.exportKey("spki", keys.publicKey))
      )
    );
  }

  public async isPublicKeyPublished(): Promise<boolean> {
    const publicKey = await this.storage.get("publicKey");
    const keyHistory = await this.getKeyHistory(this.ownUserId);

    if (keyHistory && publicKey.length) {
      const newestPublicKey = keyHistory["keys"].reverse()[0]["key"];
      return newestPublicKey === publicKey;
    } else {
      return false;
    }
  }

  public async isPrivateKeySet(): Promise<boolean> {
    const privateKey = await this.storage.get("privateKey");
    return privateKey;
  }

  public encrypt(plainText: string, privateKey: string) {
    const key = new NodeRSA(
      `-----BEGIN PRIVATE KEY-----${privateKey}-----END PRIVATE KEY-----`
    );
    return key.encryptPrivate(plainText, "base64");
  }

  public decrypt(encryptedMessage: string, publicKey: string): string {
    const key = new NodeRSA(
      `-----BEGIN PUBLIC KEY-----${publicKey}-----END PUBLIC KEY-----`
    );
    return key.decryptPublic(encryptedMessage).toString();
  }

  public async fetchPublicKeyHistoryForUser(userId: string): Promise<object[]> {
    const keyHistory = await this.getKeyHistory(userId);
    return keyHistory["keys"].reverse();
  }

  private async aesEncrypt(plainText: string) {
    const utf8BytesOfPlainText = new TextEncoder().encode(plainText);

    const iv = crypto.getRandomValues(new Uint8Array(this.IV_LENGTH));
    const keyBytes = this.base64ToBytes(this.HYBRID_OSN_AES_KEY);

    const algorithm = { name: "AES-GCM", iv, length: 256 };

    return crypto.subtle
      .importKey("raw", keyBytes, algorithm, false, ["encrypt"])
      .then((cryptoKey: CryptoKey) => {
        return crypto.subtle.encrypt(
          algorithm,
          cryptoKey,
          utf8BytesOfPlainText
        );
      })
      .then((encData: ArrayBuffer) => {
        // prepend the iv to the bytes, so that decryption-process can use it, see markdown file for "AES (symm.) encryption" and "AES Initialization vector (iv)"
        return this.mergeBytes(iv, this.bufferToBytes(encData));
      })
      .then(this.bytesToBase64);
  }

  private aesDecrypt(encryptedMessage: string) {
    try {
      const ivWithEncryptedBytes = this.base64ToBytes(encryptedMessage);

      const { encryptedBytes, ivBytes } = this.splitIvAndEncrypted(
        ivWithEncryptedBytes
      );

      const keyBytes = this.base64ToBytes(this.HYBRID_OSN_AES_KEY);

      const algorithm = { name: "AES-GCM", iv: ivBytes, length: 256 };

      return crypto.subtle
        .importKey("raw", keyBytes, algorithm, false, ["decrypt"])
        .then((cryptoKey: CryptoKey) => {
          return crypto.subtle.decrypt(algorithm, cryptoKey, encryptedBytes);
        })
        .then((decryptedData: ArrayBuffer) => {
          return new TextDecoder().decode(decryptedData);
        });
    } catch (e) {
      return Promise.reject(e);
    }
  }

  private bufferToBytes(arrayBuffer: ArrayBuffer) {
    return new Uint8Array(arrayBuffer);
  }

  private bytesToBase64(bytes: Uint8Array): string {
    let binary = "";

    const len = bytes.length;
    for (let i = 0; i < len; i++) {
      binary += String.fromCharCode(bytes[i]);
    }

    return btoa(binary);
  }

  private base64ToBytes(base64: string): Uint8Array {
    const binaryString = atob(base64);

    const len = binaryString.length;
    const bytes = new Uint8Array(len);
    for (let i = 0; i < len; i++) {
      bytes[i] = binaryString.charCodeAt(i);
    }

    return bytes;
  }

  private mergeBytes(a: Uint8Array, b: Uint8Array) {
    const aLen = a.length;
    const bLen = b.length;

    const res = new Uint8Array(aLen + bLen);

    for (let i = 0; i < aLen; i++) {
      res[i] = a[i];
    }

    for (let i = 0; i < bLen; i++) {
      res[i + aLen] = b[i];
    }

    return res;
  }

  private splitIvAndEncrypted(ivWithEncryptedBytes: Uint8Array) {
    const dataLen = ivWithEncryptedBytes.length;

    const ivBytes = new Uint8Array(this.IV_LENGTH);
    const encryptedBytes = new Uint8Array(dataLen - this.IV_LENGTH);

    for (let i = 0; i < this.IV_LENGTH; i++) {
      ivBytes[i] = ivWithEncryptedBytes[i];
    }

    for (let i = this.IV_LENGTH; i < dataLen; i++) {
      encryptedBytes[i - this.IV_LENGTH] = ivWithEncryptedBytes[i];
    }

    return { ivBytes, encryptedBytes };
  }
}
