/*
 * 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';
import { debug } from './debug';
import { KeyStoreData, KeyStoreExport } from './Key';
import { redis } from './Redis';

const logger = debug ('keystore');

const renew_interval = 3600;

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

  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');
    logger ('created keystore instance %s', this._instance);
  }

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

  private async create_key (index: string, valid_for: number): Promise<void> {
    const log = logger.extend ('create_key');
    log ('generating new key');
    const time = (new Date)
      .getTime ();
    const pair = await generate_keypair ();
    const result = {
      private_key: {
        key:         pair.private_key,
        valid_until: time + (renew_interval * 1000)
      },
      public_key: {
        key:         pair.public_key,
        valid_until: time + (valid_for * 1000)
      }
    };
    if (this._sync_redis)
      await redis.set_key ({ ...result.public_key, index });
    this._keys[index] = result;
  }

  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
      ) {
        logger ('deleting expired private key');
        delete entry.private_key;
      }

      if (entry.public_key.valid_until < time) {
        logger ('deleting expired key pair');
        delete this._keys[index];
      }
    }
  }

  public async get_sign_key (
    iat: number,
    valid_for: number,
    instance?: string
  ): Promise<string> {
    logger (
      'querying key from %s for timestamp %d, valid for %d',
      instance,
      iat,
      valid_for
    );
    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') {
      logger ('loading existing key');
      const key = this._keys[index];

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

      if (key.public_key.valid_until < valid_until) {
        logger ('updating key valid timespan to match new value');
        key.public_key.valid_until = valid_until;
      }

      return key.private_key?.key as string;
    }

    logger ('key does not exist, creating a new one');
    await this.create_key (index, valid_for);
    return this._keys[index].private_key?.key as string;
  }

  public async get_key (iat: number, instance?: string): Promise<string> {
    logger ('querying public key from %s for timestamp %d', instance, iat);
    const index = this.get_index (iat, instance);

    let key = null;

    if (typeof this._keys[index] === 'undefined') {
      if (this._sync_redis)
        key = await redis.get_key (index);
    }
    else { key = this._keys[index].public_key; }

    if (key === null)
      throw new Error ('key could not be found');

    return key.key;
  }

  public export_verification_data (): KeyStoreExport {
    logger ('exporting public keys');
    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 {
    logger ('importing %d public keys', data.length);
    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 {
    logger ('resetting keystore');
    this._instance = to_b58 (random_hex (16), 'hex');
    this._keys = {};
    this._sync_redis = false;
    redis.disconnect ();
  }

  public sync_redis (url: string): void {
    redis.connect (url);
    this._sync_redis = true;
  }
}

const ks: KeyStore = (new KeyStore);
export default ks;
export { KeyStore };