import { utils } from '@coolwallet/core';
import * as types from '../config/types';
import * as params from '../config/params';
import * as stringUtil from './stringUtil';
import * as txUtil from './tracsactionUtil';
import rlp from 'rlp';

type HexInput = string | number;
type PaymentInput = HexInput | undefined;

export const toHexValue = (value: HexInput, byteLength?: number): string => {
  const rawHex = typeof value === 'string' ? stringUtil.removeHex0x(value.trim()) : BigInt(value).toString(16);

  if (rawHex.startsWith('-')) {
    throw new Error(`Negative value is not supported: ${value}`);
  }

  const normalizedHex = stringUtil.handleHex(rawHex);
  if (!byteLength) return normalizedHex;

  const targetLength = byteLength * 2;
  if (normalizedHex.length > targetLength) {
    throw new Error(`Value exceeds ${byteLength} bytes: ${value}`);
  }

  return normalizedHex.padStart(targetLength, '0');
};

export const toRlpBytes = (value: PaymentInput, byteLength?: number): Uint8Array => {
  if (value === undefined) {
    return Uint8Array.from(Buffer.from('', 'hex'));
  }
  return Uint8Array.from(Buffer.from(toHexValue(value, byteLength), 'hex'));
};

export const encodeMemoField = (value?: string): Uint8Array => {
  if (value === undefined) {
    return Uint8Array.from(Buffer.from('', 'hex'));
  }

  const dataHex = stringUtil.handleHex(stringUtil.removeHex0x(value));
  const dataLengthHex = toHexValue(dataHex.length / 2);
  return Uint8Array.from(Buffer.from(dataLengthHex + dataHex, 'hex'));
};

export const getPaymentArgument = async (
  addressIndex: number,
  payment: types.Payment,
  newScript: boolean
): Promise<string> => {
  const SEPath = `15${await utils.getPath(params.COIN_TYPE, addressIndex)}`;
  if (!payment.Account || !payment.SigningPubKey) {
    throw new Error('Account or SigningPubKey is not set');
  }
  let argument;
  if (!newScript) {
    argument =
      stringUtil.handleHex(txUtil.getAccount(payment.Account)) +
      stringUtil.handleHex(payment.SigningPubKey) +
      stringUtil.handleHex(txUtil.getAccount(payment.Destination)) +
      stringUtil.handleHex(BigInt(payment.Amount).toString(16).padStart(16, '0')) +
      stringUtil.handleHex(BigInt(payment.Fee).toString(16).padStart(16, '0')) +
      stringUtil.handleHex(payment.Sequence.toString(16).padStart(8, '0')) +
      stringUtil.handleHex(payment.LastLedgerSequence.toString(16).padStart(8, '0')) +
      stringUtil.handleHex(payment.DestinationTag!.toString(16).padStart(8, '0')) +
      stringUtil.handleHex(payment.Flags!.toString(16).padStart(8, '0'));
  } else {
    const transaction: Array<Uint8Array | Uint8Array[]> = [];
    transaction.push(toRlpBytes(payment.Flags, 4));
    transaction.push(toRlpBytes(payment.Sequence, 4));
    transaction.push(toRlpBytes(payment.DestinationTag, 4));
    transaction.push(toRlpBytes(payment.LastLedgerSequence, 4));
    transaction.push(toRlpBytes(parseInt(payment.Amount), 7));
    transaction.push(toRlpBytes(parseInt(payment.Fee), 7));
    transaction.push(toRlpBytes(payment.SigningPubKey, 33));
    transaction.push(toRlpBytes(txUtil.getAccount(payment.Account), 20));
    transaction.push(toRlpBytes(txUtil.getAccount(payment.Destination), 20));
    const memos: Uint8Array[] = [];
    if (payment.Memos) {
      if (payment.Memos.length > 1) {
        throw new Error('Only one memo is supported at this time.');
      }
      const memo = payment.Memos[0]?.Memo;
      if (memo) {
        memos.push(encodeMemoField(memo.MemoType));
        memos.push(encodeMemoField(memo.MemoData));
        memos.push(encodeMemoField(memo.MemoFormat));
      }
    }
    transaction.push(memos);
    argument = Buffer.from(rlp.encode(transaction)).toString('hex');
  }
  return SEPath + argument;
};

export const getMessageArgument = async (addressIndex: number, message: string): Promise<string> => {
  const SEPath = `15${await utils.getPath(params.COIN_TYPE, addressIndex)}`;
  const argument = Buffer.from(message, 'utf8').toString('hex');
  return SEPath + argument;
};

/**
 * Convert mantissa (bigint) into a 54-byte buffer where each byte represents
 * one bit of the 54-bit mantissa value.
 *
 * Example flow:
 *   mantissa = 1_000_000_000_000_000n
 *   hex      = 0x038D7EA4C68000
 *   54-bit binary = 000011100011010111111010100100110001101000000000000000
 *   each bit -> 1 byte (0 -> 0x00, 1 -> 0x01)
 *   result   = "000000000000010100000101000001010001010101000001010100010100..."
 *              (108 hex chars / 54 bytes)
 */
export const MANTISSA_BIT_LENGTH = 54;

export const mantissaToBitBytes = (mantissa: bigint): string => {
  const absMantissa = mantissa < 0n ? -mantissa : mantissa;
  const binaryStr = absMantissa.toString(2);

  if (binaryStr.length > MANTISSA_BIT_LENGTH) {
    throw new Error(`Mantissa exceeds ${MANTISSA_BIT_LENGTH} bits: ${mantissa}`);
  }

  const paddedBinary = binaryStr.padStart(MANTISSA_BIT_LENGTH, '0');
  return paddedBinary
    .split('')
    .map((bit) => (bit === '1' ? '01' : '00'))
    .join('');
};

export const encodeIouAmount = (amount: string): { mantissa: bigint; exponent: number; encoded: string } => {
  const isNegative = amount.startsWith('-');
  const absStr = isNegative ? amount.slice(1) : amount;

  const [intStr, fracStr = ''] = absStr.split('.');
  const cleanFrac = fracStr.replace(/0+$/, '');

  let mantissa = BigInt(intStr + cleanFrac);
  let exponent = -cleanFrac.length;

  if (mantissa === 0n) {
    return { mantissa: 0n, exponent: 0, encoded: '8000000000000000' };
  }

  while (mantissa % 10n === 0n) {
    mantissa /= 10n;
    exponent += 1;
  }

  const minMantissa = 1000000000000000n; // 10^15
  const maxMantissa = 9999999999999999n; // 10^16 - 1

  while (mantissa < minMantissa) {
    mantissa *= 10n;
    exponent -= 1;
  }
  while (mantissa > maxMantissa) {
    mantissa /= 10n;
    exponent += 1;
  }

  // Bit 63 = IOU marker, Bit 62 = sign, Bits 54-61 = exponent+97, Bits 0-53 = mantissa
  let encoded = 1n << 63n;
  if (!isNegative) encoded |= 1n << 62n;
  encoded |= BigInt(exponent + 97) << 54n;
  encoded |= mantissa;

  return {
    mantissa: isNegative ? -mantissa : mantissa,
    exponent,
    encoded: encoded.toString(16).padStart(16, '0').toUpperCase(),
  };
};

export const getTrustSetArgument = async (
  addressIndex: number,
  payment: types.TokenPayment,
  isRLUSD: boolean
): Promise<string> => {
  const SEPath = `15${await utils.getPath(params.COIN_TYPE, addressIndex)}`;
  if (!payment.Account || !payment.SigningPubKey) {
    throw new Error('Account or SigningPubKey is not set');
  }
  const { encoded } = encodeIouAmount(payment.Token.value);

  const transaction: Array<Uint8Array | Uint8Array[]> = [];
  transaction.push(toRlpBytes(payment.Flags, 4));
  transaction.push(toRlpBytes(payment.Sequence, 4));
  transaction.push(toRlpBytes(payment.DestinationTag, 4));
  transaction.push(toRlpBytes(payment.LastLedgerSequence, 4));
  transaction.push(toRlpBytes(encoded, 8));
  transaction.push(toRlpBytes(parseInt(payment.Fee), 7));
  transaction.push(toRlpBytes(payment.SigningPubKey, 33));
  transaction.push(toRlpBytes(txUtil.getAccount(payment.Account), 20));

  if (!isRLUSD) {
    const { Token: token } = payment;
    const tokenNameLength = toHexValue(token.name.length, 1);
    const tokenNameHex = Buffer.from(token.name, 'ascii').toString('hex').padEnd(14, '0').toUpperCase();
    const issuerHex = txUtil.getAccount(token.issuer);
    const tokenInfo = tokenNameLength + tokenNameHex + token.code + issuerHex;
    transaction.push(toRlpBytes(tokenInfo, 48));
  }
  const argument = Buffer.from(rlp.encode(transaction)).toString('hex');

  return SEPath + argument;
};

export const getIouTransferArgument = async (
  addressIndex: number,
  payment: types.IouTransferPayment,
  isRLUSD: boolean
): Promise<string> => {
  const SEPath = `15${await utils.getPath(params.COIN_TYPE, addressIndex)}`;
  if (!payment.Account || !payment.SigningPubKey) {
    throw new Error('Account or SigningPubKey is not set');
  }

  const { mantissa, exponent } = encodeIouAmount(payment.Token.value);
  console.log('mantissa', mantissa);
  console.log('exponent', exponent);

  const mantissaHex = mantissaToBitBytes(mantissa);
  const mantissaBytes = Uint8Array.from(Buffer.from(mantissaHex, 'hex'));

  const transaction: Array<Uint8Array | Uint8Array[]> = [];
  transaction.push(toRlpBytes(payment.Flags, 4));
  transaction.push(toRlpBytes(payment.Sequence, 4));
  transaction.push(toRlpBytes(payment.DestinationTag, 4));
  transaction.push(toRlpBytes(payment.LastLedgerSequence, 4));
  transaction.push(mantissaBytes);
  transaction.push(toRlpBytes(exponent * -1, 1));
  transaction.push(toRlpBytes(parseInt(payment.Fee), 7));
  transaction.push(toRlpBytes(payment.SigningPubKey, 33));
  transaction.push(toRlpBytes(txUtil.getAccount(payment.Account), 20));
  transaction.push(toRlpBytes(txUtil.getAccount(payment.Destination), 20));
  const memos: Uint8Array[] = [];
  if (payment.Memos) {
    if (payment.Memos.length > 1) {
      throw new Error('Only one memo is supported at this time.');
    }
    const memo = payment.Memos[0]?.Memo;
    if (memo) {
      memos.push(encodeMemoField(memo.MemoType));
      memos.push(encodeMemoField(memo.MemoData));
      memos.push(encodeMemoField(memo.MemoFormat));
    }
  }
  transaction.push(memos);
  if (!isRLUSD) {
    const { Token: token } = payment;
    const tokenNameLength = toHexValue(token.name.length, 1);
    const tokenNameHex = Buffer.from(token.name, 'ascii').toString('hex').padEnd(14, '0').toUpperCase();
    const issuerHex = txUtil.getAccount(token.issuer);
    const tokenInfo = tokenNameLength + tokenNameHex + token.code + issuerHex;
    transaction.push(toRlpBytes(tokenInfo, 48));
  }

  const argument = Buffer.from(rlp.encode(transaction)).toString('hex');
  return SEPath + argument;
};
