import Nanobus from 'nanobus';
import { arrayToB64, b64ToArray, bytes } from './utils';

export default class FileReceiver extends Nanobus {
  constructor(url, file) {
    super('FileReceiver');
    this.secretKeyPromise = window.crypto.subtle.importKey(
      'raw',
      b64ToArray(file.key),
      'HKDF',
      false,
      ['deriveKey']
    );
    this.encryptKeyPromise = this.secretKeyPromise.then(sk => {
      const encoder = new TextEncoder();
      return window.crypto.subtle.deriveKey(
        {
          name: 'HKDF',
          salt: new Uint8Array(),
          info: encoder.encode('encryption'),
          hash: 'SHA-256'
        },
        sk,
        {
          name: 'AES-GCM',
          length: 128
        },
        false,
        ['decrypt']
      );
    });
    if (file.pwd) {
      const encoder = new TextEncoder();
      this.authKeyPromise = window.crypto.subtle
        .importKey(
          'raw',
          encoder.encode(file.password),
          { name: 'PBKDF2' },
          false,
          ['deriveKey']
        )
        .then(pwdKey =>
          window.crypto.subtle.deriveKey(
            {
              name: 'PBKDF2',
              salt: encoder.encode(file.url),
              iterations: 100,
              hash: 'SHA-256'
            },
            pwdKey,
            {
              name: 'HMAC',
              hash: 'SHA-256'
            },
            true,
            ['sign']
          )
        );
    } else {
      this.authKeyPromise = this.secretKeyPromise.then(sk => {
        const encoder = new TextEncoder();
        return window.crypto.subtle.deriveKey(
          {
            name: 'HKDF',
            salt: new Uint8Array(),
            info: encoder.encode('authentication'),
            hash: 'SHA-256'
          },
          sk,
          {
            name: 'HMAC',
            hash: { name: 'SHA-256' }
          },
          false,
          ['sign']
        );
      });
    }
    this.metaKeyPromise = this.secretKeyPromise.then(sk => {
      const encoder = new TextEncoder();
      return window.crypto.subtle.deriveKey(
        {
          name: 'HKDF',
          salt: new Uint8Array(),
          info: encoder.encode('metadata'),
          hash: 'SHA-256'
        },
        sk,
        {
          name: 'AES-GCM',
          length: 128
        },
        false,
        ['decrypt']
      );
    });
    this.file = file;
    this.url = url;
    this.msg = 'fileSizeProgress';
    this.state = 'initialized';
    this.progress = [0, 1];
  }

  get progressRatio() {
    return this.progress[0] / this.progress[1];
  }

  get sizes() {
    return {
      partialSize: bytes(this.progress[0]),
      totalSize: bytes(this.progress[1])
    };
  }

  cancel() {
    // TODO
  }

  async fetchMetadata(nonce) {
    const authHeader = await this.getAuthHeader(nonce);
    return new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.onreadystatechange = () => {
        if (xhr.readyState === XMLHttpRequest.DONE) {
          const nonce = xhr.getResponseHeader('WWW-Authenticate').split(' ')[1];
          this.file.nonce = nonce;
          if (xhr.status === 200) {
            return resolve(xhr.response);
          }
          const err = new Error(xhr.status);
          err.nonce = nonce;
          reject(err);
        }
      };
      xhr.onerror = () => reject(new Error(0));
      xhr.ontimeout = () => reject(new Error(0));
      xhr.open('get', `/api/metadata/${this.file.id}`);
      xhr.setRequestHeader('Authorization', authHeader);
      xhr.responseType = 'json';
      xhr.timeout = 2000;
      xhr.send();
    });
  }

  async getMetadata(nonce) {
    let data = null;
    try {
      try {
        data = await this.fetchMetadata(nonce);
      } catch (e) {
        if (e.message === '401' && nonce !== e.nonce) {
          // allow one retry for changed nonce
          data = await this.fetchMetadata(e.nonce);
        } else {
          throw e;
        }
      }
      const metaKey = await this.metaKeyPromise;
      const json = await window.crypto.subtle.decrypt(
        {
          name: 'AES-GCM',
          iv: new Uint8Array(12),
          tagLength: 128
        },
        metaKey,
        b64ToArray(data.metadata)
      );
      const decoder = new TextDecoder();
      const meta = JSON.parse(decoder.decode(json));
      this.file.name = meta.name;
      this.file.type = meta.type;
      this.file.iv = meta.iv;
      this.file.size = data.size;
      this.file.ttl = data.ttl;
      this.state = 'ready';
    } catch (e) {
      this.state = 'invalid';
      throw e;
    }
  }

  async downloadFile(nonce) {
    const authHeader = await this.getAuthHeader(nonce);
    return new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();

      xhr.onprogress = event => {
        if (event.lengthComputable && event.target.status !== 404) {
          this.progress = [event.loaded, event.total];
          this.emit('progress', this.progress);
        }
      };

      xhr.onload = event => {
        if (xhr.status === 404) {
          reject(new Error('notfound'));
          return;
        }
        if (xhr.status !== 200) {
          const err = new Error(xhr.status);
          err.nonce = xhr.getResponseHeader('WWW-Authenticate').split(' ')[1];
          return reject(err);
        }

        const blob = new Blob([xhr.response]);
        const fileReader = new FileReader();
        fileReader.onload = function() {
          resolve(this.result);
        };

        fileReader.readAsArrayBuffer(blob);
      };

      xhr.open('get', this.url);
      xhr.setRequestHeader('Authorization', authHeader);
      xhr.responseType = 'blob';
      xhr.send();
    });
  }

  async getAuthHeader(nonce) {
    const authKey = await this.authKeyPromise;
    const sig = await window.crypto.subtle.sign(
      {
        name: 'HMAC'
      },
      authKey,
      b64ToArray(nonce)
    );
    return `send-v1 ${arrayToB64(new Uint8Array(sig))}`;
  }

  async download(nonce) {
    this.state = 'downloading';
    this.emit('progress', this.progress);
    try {
      const encryptKey = await this.encryptKeyPromise;
      let ciphertext = null;
      try {
        ciphertext = await this.downloadFile(nonce);
      } catch (e) {
        if (e.message === '401' && nonce !== e.nonce) {
          ciphertext = await this.downloadFile(e.nonce);
        } else {
          throw e;
        }
      }
      this.msg = 'decryptingFile';
      this.emit('decrypting');
      const plaintext = await window.crypto.subtle.decrypt(
        {
          name: 'AES-GCM',
          iv: b64ToArray(this.file.iv),
          tagLength: 128
        },
        encryptKey,
        ciphertext
      );
      this.msg = 'downloadFinish';
      this.state = 'complete';
      return {
        plaintext,
        name: decodeURIComponent(this.file.name),
        type: this.file.type
      };
    } catch (e) {
      this.state = 'invalid';
      throw e;
    }
  }
}