import { Buffer } from '@stacks/common';
import {
  AddressHashMode,
  AddressVersion,
  COMPRESSED_PUBKEY_LENGTH_BYTES,
  UNCOMPRESSED_PUBKEY_LENGTH_BYTES,
  StacksMessageType,
  TransactionVersion,
  PubKeyEncoding,
} from './constants';

import {
  BufferArray,
  hash160,
  hashP2PKH,
  hexStringToInt,
  intToHexString,
  leftPadHexToLength,
  randomBytes,
} from './utils';

import { ec as EC } from 'elliptic';

import {
  MessageSignature,
  createMessageSignature,
  addressHashModeToVersion,
  addressFromVersionHash,
  addressToString,
} from './common';
import { BufferReader } from './bufferReader';
import { c32address } from 'c32check';

export interface StacksPublicKey {
  readonly type: StacksMessageType.PublicKey;
  readonly data: Buffer;
}

/** Creates a P2PKH address string from the given private key and tx version. */
export function getAddressFromPrivateKey(
  /** Private key buffer or hex string */
  privateKey: string | Buffer,
  transactionVersion = TransactionVersion.Mainnet
): string {
  const pubKey = pubKeyfromPrivKey(privateKey);
  return getAddressFromPublicKey(pubKey.data, transactionVersion);
}

/** Creates a P2PKH address string from the given public key and tx version. */
export function getAddressFromPublicKey(
  /** Public key buffer or hex string */
  publicKey: string | Buffer,
  transactionVersion = TransactionVersion.Mainnet
): string {
  publicKey = typeof publicKey === 'string' ? publicKey : publicKey.toString('hex');
  const addrVer = addressHashModeToVersion(AddressHashMode.SerializeP2PKH, transactionVersion);
  const addr = addressFromVersionHash(addrVer, hashP2PKH(Buffer.from(publicKey, 'hex')));
  const addrString = addressToString(addr);
  return addrString;
}

export function createStacksPublicKey(key: string): StacksPublicKey {
  return {
    type: StacksMessageType.PublicKey,
    data: Buffer.from(key, 'hex'),
  };
}

export function publicKeyFromSignature(
  message: string,
  messageSignature: MessageSignature,
  pubKeyEncoding = PubKeyEncoding.Compressed
): string {
  const ec = new EC('secp256k1');
  const messageBN = ec.keyFromPrivate(message, 'hex').getPrivate().toString(10);

  const parsedSignature = parseRecoverableSignature(messageSignature.data);

  const publicKey = ec.recoverPubKey(
    messageBN,
    parsedSignature,
    parsedSignature.recoveryParam,
    'hex'
  );

  if (pubKeyEncoding == PubKeyEncoding.Uncompressed) {
    return publicKey.encode('hex');
  }

  return publicKey.encodeCompressed('hex');
}

export function publicKeyFromBuffer(data: Buffer): StacksPublicKey {
  return { type: StacksMessageType.PublicKey, data };
}

export function isCompressed(key: StacksPublicKey): boolean {
  return !key.data.toString('hex').startsWith('04');
}

export function publicKeyToString(key: StacksPublicKey): string {
  return key.data.toString('hex');
}

export function serializePublicKey(key: StacksPublicKey): Buffer {
  const bufferArray: BufferArray = new BufferArray();
  bufferArray.push(key.data);
  return bufferArray.concatBuffer();
}

export function pubKeyfromPrivKey(privateKey: string | Buffer): StacksPublicKey {
  const privKey = createStacksPrivateKey(privateKey);
  const ec = new EC('secp256k1');
  const keyPair = ec.keyFromPrivate(privKey.data.toString('hex').slice(0, 64), 'hex');
  const pubKey = keyPair.getPublic(privKey.compressed, 'hex');
  return createStacksPublicKey(pubKey);
}

export function compressPublicKey(publicKey: string | Buffer): StacksPublicKey {
  const ec = new EC('secp256k1');
  const key = ec.keyFromPublic(publicKey);
  const pubKey = key.getPublic(true, 'hex');
  return createStacksPublicKey(pubKey);
}

export function deserializePublicKey(bufferReader: BufferReader): StacksPublicKey {
  const fieldId = bufferReader.readUInt8();
  const keyLength =
    fieldId !== 4 ? COMPRESSED_PUBKEY_LENGTH_BYTES : UNCOMPRESSED_PUBKEY_LENGTH_BYTES;
  return publicKeyFromBuffer(
    Buffer.concat([Buffer.from([fieldId]), bufferReader.readBuffer(keyLength)])
  );
}

export interface StacksPrivateKey {
  data: Buffer;
  compressed: boolean;
}

export function createStacksPrivateKey(key: string | Buffer): StacksPrivateKey {
  const data = typeof key === 'string' ? Buffer.from(key, 'hex') : key;
  let compressed: boolean;
  if (data.length === 33) {
    if (data[data.length - 1] !== 1) {
      throw new Error(
        'Improperly formatted private-key. 33 byte length usually ' +
          'indicates compressed key, but last byte must be == 0x01'
      );
    }
    compressed = true;
  } else if (data.length === 32) {
    compressed = false;
  } else {
    throw new Error(
      `Improperly formatted private-key hex string: length should be 32 or 33 bytes, provided with length ${data.length}`
    );
  }
  return { data, compressed };
}

export function makeRandomPrivKey(entropy?: Buffer): StacksPrivateKey {
  const ec = new EC('secp256k1');
  const options = { entropy: entropy || randomBytes(32) };
  const keyPair = ec.genKeyPair(options);
  const privateKey = keyPair.getPrivate().toString('hex', 32);
  return createStacksPrivateKey(privateKey);
}

export function signWithKey(privateKey: StacksPrivateKey, input: string): MessageSignature {
  const ec = new EC('secp256k1');
  const key = ec.keyFromPrivate(privateKey.data.toString('hex').slice(0, 64), 'hex');
  const signature = key.sign(input, 'hex', { canonical: true });
  const coordinateValueBytes = 32;
  const r = leftPadHexToLength(signature.r.toString('hex'), coordinateValueBytes * 2);
  const s = leftPadHexToLength(signature.s.toString('hex'), coordinateValueBytes * 2);
  if (signature.recoveryParam === undefined || signature.recoveryParam === null) {
    throw new Error('"signature.recoveryParam" is not set');
  }
  const recoveryParam = intToHexString(signature.recoveryParam, 1);
  const recoverableSignatureString = recoveryParam + r + s;
  return createMessageSignature(recoverableSignatureString);
}

export function getSignatureRecoveryParam(signature: string) {
  const coordinateValueBytes = 32;
  if (signature.length < coordinateValueBytes * 2 * 2 + 1) {
    throw new Error('Invalid signature');
  }
  const recoveryParamHex = signature.substr(0, 2);
  return hexStringToInt(recoveryParamHex);
}

export function parseRecoverableSignature(signature: string) {
  const coordinateValueBytes = 32;
  if (signature.length < coordinateValueBytes * 2 * 2 + 1) {
    throw new Error('Invalid signature');
  }
  const recoveryParamHex = signature.substr(0, 2);
  const r = signature.substr(2, coordinateValueBytes * 2);
  const s = signature.substr(2 + coordinateValueBytes * 2, coordinateValueBytes * 2);
  return {
    recoveryParam: hexStringToInt(recoveryParamHex),
    r,
    s,
  };
}

export function getPublicKey(privateKey: StacksPrivateKey): StacksPublicKey {
  return pubKeyfromPrivKey(privateKey.data);
}

export function privateKeyToString(privateKey: StacksPrivateKey): string {
  return privateKey.data.toString('hex');
}

export function publicKeyToAddress(version: AddressVersion, publicKey: StacksPublicKey): string {
  return c32address(version, hash160(publicKey.data).toString('hex'));
}
