import type { Networkish } from "../common/types";
import type { IPrivateKey, PrivateKeyDto } from "../common/interfaces";
import type { Network } from "../core/network/network";
import { networks } from "../core/network/network-manager";
import BN from "../crypto/bn.extension";
import CommonUtils from "../utils/common.utils";
import BufferUtils from "../utils/buffer.utils";
import Point from "../crypto/point";
import Base58Check from "../encoding/base58check";
import ValidationUtils from "../utils/validation.utils";
import PublicKey from "./publickey";
import Address from "../core/address/address";
import { AddressType } from "../core/address/address-formatter";

export type PrivateKeyVariants = BN | string | Uint8Array | PrivateKey | IPrivateKey | PrivateKeyDto | null;

export default class PrivateKey implements IPrivateKey {

  private _pubkey?: PublicKey;

  public bn!: BN;
  public compressed!: boolean;
  public network!: Network;

  /**
   * Instantiate a PrivateKey.
   * 
   * @param data The private key data
   * 
   * @remarks Better to use {@linkcode PrivateKey.from} method to init private key from various formats if the formart unknown.
   *
   * @example
   * ```ts
   * // generate a new random key
   * let key = new PrivateKey();
   *
   * // encode into wallet import format
   * let exported = key.toWIF();
   *
   * // instantiate from the exported (and saved) private key
   * let imported = PrivateKey.fromWIF(exported);
   * ```
   */
  constructor(data?: Partial<IPrivateKey>) {
    if (data instanceof PrivateKey) {
      return data;
    }

    if (data == null) {
      data = {
        bn: PrivateKey._getRandomBN(),
        compressed: true,
        network: networks.defaultNetwork
      };
    }

    if (!PrivateKey._isPrivateKeyParams(data)) {
      throw new TypeError('First argument is an unrecognized data type.');
    }

    // validation
    if (!data.bn || data.bn.cmp(new BN(0)) === 0){
      throw new TypeError('Number can not be equal to zero, undefined, null or false');
    }
    if (!data.bn.lt(Point.getN())) {
      throw new TypeError('Number must be less than N');
    }

    this.bn = data.bn;
    this.compressed = data.compressed == null ? true : data.compressed;
    this.network = data.network || networks.defaultNetwork;
  }

  public get publicKey(): PublicKey {
    return this.toPublicKey();
  }

  /**
   * Will return an address for the private key with its defined network
   * 
   * @param type - optional parameter specifying the desired type of the address.
   *  default {@link AddressType.PayToScriptTemplate}
   * 
   * @returns An address generated from the private key
   */
  public toAddress(type = AddressType.PayToScriptTemplate): Address {
    return Address.fromPublicKey(this.publicKey, this.network, type);
  }

  /**
   * Will output the PrivateKey encoded as hex string
   */
  public toString(): string {
    return BufferUtils.bufferToHex(this.toBuffer());
  }

  /**
   * Will encode the PrivateKey to a WIF string
   *
   * @returns A WIF representation of the private key
   */
  public toWIF(): string {
    let buf: Uint8Array;
    if (this.compressed) {
      buf = BufferUtils.concat([Uint8Array.from([this.network.privatekey]),
                          this.bn.toByteArray({size: 32}),
                          Uint8Array.from([0x01])]);
    } else {
      buf = BufferUtils.concat([Uint8Array.from([this.network.privatekey]),
                          this.bn.toByteArray({size: 32})]);
    }

    return Base58Check.encode(buf);
  }

  /**
   * Will return the private key as a BN instance
   *
   * @returns A BN instance of the private key
   */
  public toBigNumber(): BN {
    return this.bn;
  }

  /**
   * Will return the private key as a BN buffer
   *
   * @returns A buffer of the private key
   */
  public toBuffer(): Uint8Array {
    return this.bn.toByteArray({size: 32});
  }

  /**
   * Will return the private key as a BN buffer without leading zero padding
   *
   * @returns A buffer of the private key
   */
  public toBufferNoPadding(): Uint8Array {
    return this.bn.toByteArray();
  }

  /**
   * Will return the corresponding public key
   *
   * @returns A public key generated from the private key
   */
  public toPublicKey(): PublicKey {
    if (!this._pubkey) {
      this._pubkey = PublicKey.fromPrivateKey(this);
    }
    return this._pubkey;
  }

  public toObject = this.toJSON;

  /**
   * @returns A plain object representation
   */
  public toJSON(): PrivateKeyDto {
    return {
      bn: this.bn.toString('hex'),
      compressed: this.compressed,
      network: this.network.toString()
    };
  }

  /**
   * Will return a string formatted for the console
   *
   * @returns Private key details
   */
  public inspect(): string {
    let uncompressed = !this.compressed ? ', uncompressed' : '';
    return `<PrivateKey: ${this.toString()}, network: ${this.network}${uncompressed}>`;
  }

  /**
   * Instantiate a PrivateKey from a Uint8Array with the DER or WIF representation
   */
  public static fromBuffer(buf: Uint8Array, network?: Networkish): PrivateKey {
    ValidationUtils.validateArgument(BufferUtils.isBuffer(buf), 'First argument is expected to be a buffer.');
    let data = this._transformBuffer(buf, network);
    return new PrivateKey(data);
  }

  public static fromString = this.fromWIF;

  /**
   * Instantiate a PrivateKey from a WIF string
   *
   * @param str - The WIF encoded private key string
   * @returns A new valid instance of PrivateKey
   */
  public static fromWIF(str: string, network?: Networkish): PrivateKey {
    ValidationUtils.validateArgument(typeof str === 'string', 'First argument is expected to be a string.');

    let data: Partial<IPrivateKey>;
    if (CommonUtils.isHexa(str)) {
      data = {
        bn: new BN(BufferUtils.hexToBuffer(str)),
        compressed: true,
        network: networks.get(network) || networks.defaultNetwork
      };
    } else {
      data = PrivateKey._transformWIF(str, network);
    }
    return new PrivateKey(data);
  }

  public static fromObject = this.fromJSON;

  /**
   * Instantiate a PrivateKey from a plain JavaScript object
   *
   * @param obj - The output from privateKey.toObject()
   */
  public static fromJSON(obj: PrivateKeyDto): PrivateKey {
    ValidationUtils.validateArgument(typeof obj === 'object' && obj !== null, 'First argument is expected to be an object.');
    let data = this._transformObject(obj);
    return new PrivateKey(data);
  }

  /**
   * Instantiate a PrivateKey from random bytes
   *
   * @param network - Either "mainnet" or "testnet"
   * @returns A new valid instance of PrivateKey
   */
  public static fromRandom(network?: Networkish): PrivateKey {
    let data = {
      bn: PrivateKey._getRandomBN(),
      compressed: true,
      network: networks.get(network) || networks.defaultNetwork
    }
    return new PrivateKey(data);
  }

  /**
   * Check if there would be any errors when initializing a PrivateKey
   *
   * @param data - The encoded data in various formats
   * @param network - Either "mainnet" or "testnet"
   * @returns An error if exists
   */

  public static getValidationError(data: PrivateKeyVariants, network?: Networkish): Error | undefined {
    try {
      this.from(data, network);
    } catch (e) {
      return e as Error;
    }
    return undefined;
  }

  /**
   * Check if the parameters are valid
   *
   * @param data - The encoded data in various formats
   * @param network - Either "mainnet" or "testnet"
   * @returns true If the private key would be valid
   */
  public static isValid(data?: PrivateKeyVariants, network?: Networkish): boolean {
    if (!data) {
      return false;
    }
    return !this.getValidationError(data, network);
  }

  /**
   * Helper to instantiate PrivateKey from different kinds of arguments.
   */
  public static from(data?: PrivateKeyVariants, network?: Networkish): PrivateKey {
    // detect type of data
    if (data == null){
      return this.fromRandom(network);
    } else if (data instanceof PrivateKey) {
      return data;
    } else if (data instanceof BN) {
      let info: Partial<IPrivateKey> = {
        bn: data,
        compressed: true,
        network: networks.get(network) || networks.defaultNetwork
      }
      return new PrivateKey(info);
    } else if (BufferUtils.isBuffer(data)) {
      return this.fromBuffer(data, network);
    } else if (this._isPrivateKeyParams(data) && data.bn && data.network){
      return this.fromObject(data);
    } else if (typeof data === 'string') {
      return this.fromString(data, network);
    } else {
      throw new TypeError('First argument is an unrecognized data type.');
    }
  }

  private static _isPrivateKeyParams(data: unknown): data is PrivateKeyDto {
    return typeof data === 'object' && data !== null && 'bn' in data && 'network' in data;
  }

  private static _getRandomBN(): BN {
    let bn: BN;
    do {
      let privbuf = BufferUtils.getRandomBuffer(32);
      bn = BN.fromBuffer(privbuf);
    } while (!bn.lt(Point.getN()));
    return bn;
  }

  /**
   * Internal function to transform a WIF Uint8Array into a private key
   */
  private static _transformBuffer(buf: Uint8Array, network?: Networkish): IPrivateKey {
    if (buf.length === 32) {
      return {
        network: networks.get(network) || networks.defaultNetwork,
        bn: BN.fromBuffer(buf),
        compressed: false,
      } as IPrivateKey;
    }

    let info: Partial<IPrivateKey> = {};

    info.network = networks.get(buf[0], 'privatekey');
    if (!info.network) {
      throw new Error(`Invalid network`);
    }
    if (network && info.network !== networks.get(network)) {
      throw new TypeError('Private key network mismatch');
    }

    if (buf.length === 1 + 32 + 1 && buf[1 + 32 + 1 - 1] === 1) {
      info.compressed = true;
    } else if (buf.length === 1 + 32) {
      info.compressed = false;
    } else {
      throw new Error('Length of buffer must be 33 (uncompressed) or 34 (compressed)');
    }

    info.bn = BN.fromBuffer(buf.subarray(1, 32 + 1));

    return info as IPrivateKey;
  }

  /**
   * Internal function to transform a JSON object into a private key
   */
  private static _transformObject(data: PrivateKeyDto): IPrivateKey {
    return {
      bn: new BN(data.bn, 'hex'),
      network: networks.get(data.network) || networks.defaultNetwork,
      compressed: data.compressed
    } as IPrivateKey;
  }

  /**
   * Internal function to transform a WIF string into a private key
   */
  private static _transformWIF(str: string, network?: Networkish): IPrivateKey {
    return this._transformBuffer(Base58Check.decode(str), network);
  }
}