import { concat, equal, hexToBytes } from '@xrplf/isomorphic/utils'

import {
  codec,
  encodeSeed,
  decodeSeed,
  encodeAccountID,
  decodeAccountID,
  encodeNodePublic,
  decodeNodePublic,
  encodeAccountPublic,
  decodeAccountPublic,
  isValidClassicAddress,
} from './xah-codec'

const PREFIX_BYTES = {
  // 5, 68
  main: Uint8Array.from([0x05, 0x44]),
  // 4, 147
  test: Uint8Array.from([0x04, 0x93]),
}

const MAX_32_BIT_UNSIGNED_INT = 4294967295

function classicAddressToXAddress(
  classicAddress: string,
  tag: number | false,
  test: boolean,
): string {
  const accountId = decodeAccountID(classicAddress)
  return encodeXAddress(accountId, tag, test)
}

function encodeXAddress(
  accountId: Uint8Array,
  tag: number | false,
  test: boolean,
): string {
  if (accountId.length !== 20) {
    // RIPEMD160 is 160 bits = 20 bytes
    throw new Error('Account ID must be 20 bytes')
  }
  if (tag !== false && tag > MAX_32_BIT_UNSIGNED_INT) {
    throw new Error('Invalid tag')
  }
  const theTag = tag || 0
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Passing null is a common js mistake
  const flag = tag === false || tag == null ? 0 : 1
  /* eslint-disable no-bitwise ---
   * need to use bitwise operations here */
  const bytes = concat([
    test ? PREFIX_BYTES.test : PREFIX_BYTES.main,
    accountId,
    Uint8Array.from([
      // 0x00 if no tag, 0x01 if 32-bit tag
      flag,
      // first byte
      theTag & 0xff,
      // second byte
      (theTag >> 8) & 0xff,
      // third byte
      (theTag >> 16) & 0xff,
      // fourth byte
      (theTag >> 24) & 0xff,
      0,
      0,
      0,
      // four zero bytes (reserved for 64-bit tags)
      0,
    ]),
  ])
  /* eslint-enable no-bitwise */
  return codec.encodeChecked(bytes)
}

function xAddressToClassicAddress(xAddress: string): {
  classicAddress: string
  tag: number | false
  test: boolean
} {
  /* eslint-disable @typescript-eslint/naming-convention --
   * TODO 'test' should be something like 'isTest', do this later
   */
  const { accountId, tag, test } = decodeXAddress(xAddress)
  /* eslint-enable @typescript-eslint/naming-convention */
  const classicAddress = encodeAccountID(accountId)
  return {
    classicAddress,
    tag,
    test,
  }
}

function decodeXAddress(xAddress: string): {
  accountId: Uint8Array
  tag: number | false
  test: boolean
} {
  const decoded = codec.decodeChecked(xAddress)
  /* eslint-disable @typescript-eslint/naming-convention --
   * TODO 'test' should be something like 'isTest', do this later
   */
  const test = isUint8ArrayForTestAddress(decoded)
  /* eslint-enable @typescript-eslint/naming-convention */
  const accountId = decoded.slice(2, 22)
  const tag = tagFromUint8Array(decoded)
  return {
    accountId,
    tag,
    test,
  }
}

function isUint8ArrayForTestAddress(buf: Uint8Array): boolean {
  const decodedPrefix = buf.slice(0, 2)
  if (equal(PREFIX_BYTES.main, decodedPrefix)) {
    return false
  }
  if (equal(PREFIX_BYTES.test, decodedPrefix)) {
    return true
  }

  throw new Error('Invalid X-address: bad prefix')
}

function tagFromUint8Array(buf: Uint8Array): number | false {
  const flag = buf[22]
  if (flag >= 2) {
    // No support for 64-bit tags at this time
    throw new Error('Unsupported X-address')
  }
  if (flag === 1) {
    // Little-endian to big-endian
    return buf[23] + buf[24] * 0x100 + buf[25] * 0x10000 + buf[26] * 0x1000000
  }
  if (flag !== 0) {
    throw new Error('flag must be zero to indicate no tag')
  }
  if (!equal(hexToBytes('0000000000000000'), buf.slice(23, 23 + 8))) {
    throw new Error('remaining bytes must be zero')
  }
  return false
}

function isValidXAddress(xAddress: string): boolean {
  try {
    decodeXAddress(xAddress)
  } catch (_error) {
    return false
  }
  return true
}

export {
  // Codec with XAH alphabet
  codec,
  // Encode entropy as a "seed"
  encodeSeed,
  // Decode a seed into an object with its version, type, and bytes
  decodeSeed,
  // Encode bytes as a classic address (r...)
  encodeAccountID,
  // Decode a classic address to its raw bytes
  decodeAccountID,
  // Encode bytes to XAH Ledger node public key format
  encodeNodePublic,
  // Decode an XAH Ledger node public key into its raw bytes
  decodeNodePublic,
  // Encode a public key, as for payment channels
  encodeAccountPublic,
  // Decode a public key, as for payment channels
  decodeAccountPublic,
  // Check whether a classic address (r...) is valid
  isValidClassicAddress,
  // Derive X-address from classic address, tag, and network ID
  classicAddressToXAddress,
  // Encode account ID, tag, and network ID to X-address
  encodeXAddress,
  // Decode X-address to account ID, tag, and network ID
  xAddressToClassicAddress,
  // Convert X-address to classic address, tag, and network ID
  decodeXAddress,
  // Check whether an X-address (X...) is valid
  isValidXAddress,
}
