/*
 * Copyright (C) Sapphirecode - All Rights Reserved
 * This file is part of Auth-Server-Helper which is released under MIT.
 * See file 'LICENSE' for full license details.
 * Created by Timo Hocker <timo@scode.ovh>, December 2020
 */

import { generate_keypair, random_hex } from '@sapphirecode/crypto-helper';
import { to_b58 } from '@sapphirecode/encoding-helper';

const renew_interval = 3600;

interface Key {
  key: string;
  valid_until: number;
}

interface LabelledKey extends Key {
  index: string;
}

interface KeyPair {
  private_key?: Key;
  public_key: Key;
}

type KeyStoreData = Record<string, KeyPair>;
type KeyStoreExport = LabelledKey[];

async function create_key (valid_for: number) {
  const time = (new Date)
    .getTime ();
  const pair = await generate_keypair ();
  return {
    private_key: {
      key:         pair.private_key,
      valid_until: time + (renew_interval * 1000)
    },
    public_key: {
      key:         pair.public_key,
      valid_until: time + (valid_for * 1000)
    }
  };
}

class KeyStore {
  private _keys: KeyStoreData = {};
  private _interval: NodeJS.Timeout;
  private _instance: string;

  public get instance_id (): string {
    return this._instance;
  }

  public constructor () {
    this._interval = setInterval (() => {
      this.garbage_collect ();
    }, renew_interval);
    this._instance = to_b58 (random_hex (16), 'hex');
  }

  private get_index (iat: number, instance = this._instance): string {
    return instance + Math.floor (iat / renew_interval)
      .toFixed (0);
  }

  private garbage_collect (): void {
    const time = (new Date)
      .getTime ();
    const keys = Object.keys (this._keys);
    for (const index of keys) {
      const entry = this._keys[index];
      if (typeof entry.private_key !== 'undefined'
        && entry.private_key.valid_until < time
      )
        delete entry.private_key;

      if (entry.public_key.valid_until < time)
        delete this._keys[index];
    }
  }

  public async get_sign_key (
    iat: number,
    valid_for: number,
    instance?: string
  ): Promise<string> {
    if (valid_for <= 0)
      throw new Error ('cannot create infinitely valid key');

    if ((iat + 1) * 1000 < (new Date)
      .getTime ())
      throw new Error ('cannot access already expired keys');

    const index = this.get_index (iat, instance);

    const valid_until = (new Date)
      .getTime () + (valid_for * 1000);

    if (typeof this._keys[index] !== 'undefined') {
      const key = this._keys[index];
      if (key.public_key.valid_until < valid_until)
        key.public_key.valid_until = valid_until;

      if (typeof key.private_key === 'undefined')
        throw new Error ('cannot access already expired keys');

      return key.private_key?.key as string;
    }

    this._keys[index] = await create_key (valid_for);
    return this._keys[index].private_key?.key as string;
  }

  public get_key (iat: number, instance?: string): string {
    const index = this.get_index (iat, instance);

    if (typeof this._keys[index] === 'undefined')
      throw new Error ('key could not be found');

    const key = this._keys[index];
    return key.public_key.key;
  }

  public export_verification_data (): KeyStoreExport {
    this.garbage_collect ();
    const out: KeyStoreExport = [];
    for (const index of Object.keys (this._keys))
      out.push ({ ...this._keys[index].public_key, index });

    return out;
  }

  public import_verification_data (data: KeyStoreExport): void {
    for (const key of data) {
      if (typeof this._keys[key.index] !== 'undefined')
        throw new Error ('cannot import to the same instance');
      this._keys[key.index] = {
        public_key: {
          key:         key.key,
          valid_until: key.valid_until
        }
      };
    }
    this.garbage_collect ();
  }

  public reset_instance (): void {
    this._instance = to_b58 (random_hex (16), 'hex');
    this._keys = {};
  }
}

const ks: KeyStore = (new KeyStore);
export default ks;
export { KeyStore, Key, LabelledKey, KeyStoreExport };