import * as crypto from 'crypto'
import { keccak256, sha256, toBuffer } from 'ethereumjs-util'
import { scrypt } from 'scrypt-js'

import Wallet from './index'

const utf8 = require('utf8')
const aesjs = require('aes-js')

function runCipherBuffer(cipher: crypto.Cipher | crypto.Decipher, data: Buffer): Buffer {
  return Buffer.concat([cipher.update(data), cipher.final()])
}

// evp_kdf

export interface EvpKdfOpts {
  count: number
  keysize: number
  ivsize: number
  digest: string
}

const evpKdfDefaults: EvpKdfOpts = {
  count: 1,
  keysize: 16,
  ivsize: 16,
  digest: 'md5',
}

function mergeEvpKdfOptsWithDefaults(opts?: Partial<EvpKdfOpts>): EvpKdfOpts {
  if (!opts) {
    return evpKdfDefaults
  }
  return {
    count: opts.count || evpKdfDefaults.count,
    keysize: opts.keysize || evpKdfDefaults.keysize,
    ivsize: opts.ivsize || evpKdfDefaults.ivsize,
    digest: opts.digest || evpKdfDefaults.digest,
  }
}

/*
 * opts:
 * - digest - digest algorithm, defaults to md5
 * - count - hash iterations
 * - keysize - desired key size
 * - ivsize - desired IV size
 *
 * Algorithm form https://www.openssl.org/docs/manmaster/crypto/EVP_BytesToKey.html
 *
 * FIXME: not optimised at all
 */
function evp_kdf(data: Buffer, salt: Buffer, opts?: Partial<EvpKdfOpts>) {
  const params = mergeEvpKdfOptsWithDefaults(opts)

  // A single EVP iteration, returns `D_i`, where block equlas to `D_(i-1)`
  function iter(block: Buffer) {
    let hash = crypto.createHash(params.digest)
    hash.update(block)
    hash.update(data)
    hash.update(salt)
    block = hash.digest()

    for (let i = 1, len = params.count; i < len; i++) {
      hash = crypto.createHash(params.digest)
      hash.update(block)
      block = hash.digest()
    }
    return block
  }

  const ret: Buffer[] = []
  let i = 0
  while (Buffer.concat(ret).length < params.keysize + params.ivsize) {
    ret[i] = iter(i === 0 ? Buffer.alloc(0) : ret[i - 1])
    i++
  }
  const tmp = Buffer.concat(ret)

  return {
    key: tmp.slice(0, params.keysize),
    iv: tmp.slice(params.keysize, params.keysize + params.ivsize),
  }
}

// http://stackoverflow.com/questions/25288311/cryptojs-aes-pattern-always-ends-with
function decodeCryptojsSalt(input: string): { ciphertext: Buffer; salt?: Buffer } {
  const ciphertext = Buffer.from(input, 'base64')
  if (ciphertext.slice(0, 8).toString() === 'Salted__') {
    return {
      salt: ciphertext.slice(8, 16),
      ciphertext: ciphertext.slice(16),
    }
  }
  return { ciphertext }
}

// {
//   "address": "0x169aab499b549eac087035e640d3f7d882ef5e2d",
//   "encrypted": true,
//   "locked": true,
//   "hash": "342f636d174cc1caa49ce16e5b257877191b663e0af0271d2ea03ac7e139317d",
//   "private": "U2FsdGVkX19ZrornRBIfl1IDdcj6S9YywY8EgOeOtLj2DHybM/CHL4Jl0jcwjT+36kDnjj+qEfUBu6J1mGQF/fNcD/TsAUgGUTEUEOsP1CKDvNHfLmWLIfxqnYHhHsG5",
//   "public": "U2FsdGVkX19EaDNK52q7LEz3hL/VR3dYW5VcoP04tcVKNS0Q3JINpM4XzttRJCBtq4g22hNDrOR8RWyHuh3nPo0pRSe9r5AUfEiCLaMBAhI16kf2KqCA8ah4brkya9ZLECdIl0HDTMYfDASBnyNXd87qodt46U0vdRT3PppK+9hsyqP8yqm9kFcWqMHktqubBE937LIU0W22Rfw6cJRwIw=="
// }

export interface EtherWalletOptions {
  address: string
  encrypted: boolean
  locked: boolean
  hash: string
  private: string
  public: string
}

/*
 * Third Party API: Import a wallet generated by EtherWallet
 * This wallet format is created by https://github.com/SilentCicero/ethereumjs-accounts
 * and used on https://www.myetherwallet.com/
 */
export function fromEtherWallet(input: string | EtherWalletOptions, password: string): Wallet {
  const json: EtherWalletOptions = typeof input === 'object' ? input : JSON.parse(input)

  let privateKey: Buffer
  if (!json.locked) {
    if (json.private.length !== 64) {
      throw new Error('Invalid private key length')
    }
    privateKey = Buffer.from(json.private, 'hex')
  } else {
    if (typeof password !== 'string') {
      throw new Error('Password required')
    }

    if (password.length < 7) {
      throw new Error('Password must be at least 7 characters')
    }

    // the "encrypted" version has the low 4 bytes
    // of the hash of the address appended
    const hash = json.encrypted ? json.private.slice(0, 128) : json.private

    // decode openssl ciphertext + salt encoding
    const cipher = decodeCryptojsSalt(hash)
    if (!cipher.salt) {
      throw new Error('Unsupported EtherWallet key format')
    }

    // derive key/iv using OpenSSL EVP as implemented in CryptoJS
    const evp = evp_kdf(Buffer.from(password), cipher.salt, { keysize: 32, ivsize: 16 })

    const decipher = crypto.createDecipheriv('aes-256-cbc', evp.key, evp.iv)
    privateKey = runCipherBuffer(decipher, Buffer.from(cipher.ciphertext))

    // NOTE: yes, they've run it through UTF8
    privateKey = Buffer.from(utf8.decode(privateKey.toString()), 'hex')
  }

  const wallet = new Wallet(privateKey)
  if (wallet.getAddressString() !== json.address) {
    throw new Error('Invalid private key or address')
  }
  return wallet
}

/**
 * Third Party API: Import a brain wallet used by Ether.Camp
 */
export function fromEtherCamp(passphrase: string): Wallet {
  return new Wallet(keccak256(Buffer.from(passphrase)))
}

/**
 * Third Party API: Import a wallet from a KryptoKit seed
 */
export async function fromKryptoKit(entropy: string, password: string): Promise<Wallet> {
  function kryptoKitBrokenScryptSeed(buf: Buffer) {
    // js-scrypt calls `Buffer.from(String(salt), 'utf8')` on the seed even though it is a buffer
    //
    // The `buffer`` implementation used does the below transformation (doesn't matches the current version):
    // https://github.com/feross/buffer/blob/67c61181b938b17d10dbfc0a545f713b8bd59de8/index.js

    function decodeUtf8Char(str: string) {
      try {
        return decodeURIComponent(str)
      } catch (err) {
        return String.fromCharCode(0xfffd) // UTF 8 invalid char
      }
    }

    let res = '',
      tmp = ''
    for (let i = 0; i < buf.length; i++) {
      if (buf[i] <= 0x7f) {
        res += decodeUtf8Char(tmp) + String.fromCharCode(buf[i])
        tmp = ''
      } else {
        tmp += '%' + buf[i].toString(16)
      }
    }
    return Buffer.from(res + decodeUtf8Char(tmp))
  }

  if (entropy[0] === '#') {
    entropy = entropy.slice(1)
  }

  const type = entropy[0]
  entropy = entropy.slice(1)

  let privateKey: Buffer
  if (type === 'd') {
    privateKey = sha256(toBuffer(entropy))
  } else if (type === 'q') {
    if (typeof password !== 'string') {
      throw new Error('Password required')
    }

    const encryptedSeed = sha256(Buffer.from(entropy.slice(0, 30)))
    const checksum = entropy.slice(30, 46)

    const salt = kryptoKitBrokenScryptSeed(encryptedSeed)
    const aesKey = await scrypt(Buffer.from(password, 'utf8'), salt, 16384, 8, 1, 32)

    /* FIXME: try to use `crypto` instead of `aesjs`

    // NOTE: ECB doesn't use the IV, so it can be anything
    var decipher = crypto.createDecipheriv("aes-256-ecb", aesKey, Buffer.from(0))

    // FIXME: this is a clear abuse, but seems to match how ECB in aesjs works
    privKey = Buffer.concat([
      decipher.update(encryptedSeed).slice(0, 16),
      decipher.update(encryptedSeed).slice(0, 16),
    ])
    */

    const decipher = new aesjs.ModeOfOperation.ecb(aesKey)
    /* decrypt returns an Uint8Array, perhaps there is a better way to concatenate */
    privateKey = Buffer.concat([
      Buffer.from(decipher.decrypt(encryptedSeed.slice(0, 16))),
      Buffer.from(decipher.decrypt(encryptedSeed.slice(16, 32))),
    ])

    if (checksum.length > 0) {
      if (checksum !== sha256(sha256(privateKey)).slice(0, 8).toString('hex')) {
        throw new Error('Failed to decrypt input - possibly invalid passphrase')
      }
    }
  } else {
    throw new Error('Unsupported or invalid entropy type')
  }

  return new Wallet(privateKey)
}

/**
 * Third Party API: Import a brain wallet used by Quorum Wallet
 */
export function fromQuorumWallet(passphrase: string, userid: string): Wallet {
  if (passphrase.length < 10) {
    throw new Error('Passphrase must be at least 10 characters')
  }
  if (userid.length < 10) {
    throw new Error('User id must be at least 10 characters')
  }

  const merged = passphrase + userid
  const seed = crypto.pbkdf2Sync(merged, merged, 2000, 32, 'sha256')
  return new Wallet(seed)
}

const Thirdparty = {
  fromEtherWallet,
  fromEtherCamp,
  fromKryptoKit,
  fromQuorumWallet,
}

export default Thirdparty
