import crypto from 'crypto';
import { ethers } from 'ethers';
import { Network } from '../../domain/entities/network';
import { Wallet } from '../../domain/entities/wallet';
import {
  GeneratedAddress,
  IAddressGenerator,
} from '../../application/ports/address-generator';
import { AddressType } from '../../domain/value-objects/address-type';
import { IEncryptionService } from '../../application/ports/encryption-service';

const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
const BECH32_CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';

const base58Encode = (source: Buffer): string => {
  let num = BigInt('0x' + source.toString('hex'));
  let encoded = '';

  while (num > 0n) {
    const remainder = Number(num % 58n);
    encoded = BASE58_ALPHABET[remainder] + encoded;
    num = num / 58n;
  }

  for (const byte of source) {
    if (byte === 0) {
      encoded = '1' + encoded;
    } else {
      break;
    }
  }

  return encoded;
};

const base58EncodeFixed = (source: Buffer, length: number): string => {
  let num = BigInt('0x' + source.toString('hex'));
  let encoded = '';

  while (encoded.length < length) {
    if (num === 0n) {
      encoded = BASE58_ALPHABET[0] + encoded;
      continue;
    }
    const remainder = Number(num % 58n);
    encoded = BASE58_ALPHABET[remainder] + encoded;
    num = num / 58n;
  }

  return encoded;
};

const bech32EncodeFixed = (source: Buffer, length: number): string => {
  let num = BigInt('0x' + source.toString('hex'));
  let encoded = '';

  while (encoded.length < length) {
    if (num === 0n) {
      encoded = BECH32_CHARSET[0] + encoded;
      continue;
    }
    const remainder = Number(num % 32n);
    encoded = BECH32_CHARSET[remainder] + encoded;
    num = num / 32n;
  }

  return encoded;
};

type UtxoFormat = {
  readonly prefix: string;
  readonly encoding: 'base58' | 'bech32';
  readonly dataLength: number;
};

/**
 * Trust Wallet compatible address formats for UTXO-like networks.
 *
 * These formats are intentionally synthetic: the server does not derive real
 * HD keys from the stored seed phrase, but the generated strings must visually
 * match the prefix and length that Trust Wallet displays so the app can render
 * them consistently.
 */
const UTXO_FORMATS: Record<string, UtxoFormat> = {
  BTC: { prefix: 'bc1q', encoding: 'bech32', dataLength: 38 },
  LTC: { prefix: 'ltc1q', encoding: 'bech32', dataLength: 38 },
  DGB: { prefix: 'dgb1q', encoding: 'bech32', dataLength: 38 },
  DOGE: { prefix: 'D', encoding: 'base58', dataLength: 33 },
  DASH: { prefix: 'X', encoding: 'base58', dataLength: 33 },
  ZEC: { prefix: 't1', encoding: 'base58', dataLength: 32 },
  RVN: { prefix: 'R', encoding: 'base58', dataLength: 33 },
  DCR: { prefix: 'Ds', encoding: 'base58', dataLength: 32 },
  BCH: { prefix: 'bitcoincash:q', encoding: 'bech32', dataLength: 41 },
};

const hashFor = (network: Network, wallet: Wallet): Buffer =>
  crypto.createHash('sha256').update(`${wallet.id}:${network.id}`).digest();

/**
 * Produces the legacy address format (e.g. BTC starting with '1') used before
 * the Trust Wallet format fix. This is exposed so the healing script can
 * identify old addresses and migrate them deterministically.
 */
export const getLegacyAddress = (network: Network, wallet: Wallet): string => {
  const hash = hashFor(network, wallet);
  const base58 = base58Encode(hash);
  return `1${base58.slice(0, 33)}`;
};

const generateUtxoAddress = (network: Network, hash: Buffer): string => {
  const format = UTXO_FORMATS[network.code.toUpperCase()];
  if (!format) {
    return `1${base58Encode(hash).slice(0, 33)}`;
  }

  const data =
    format.encoding === 'base58'
      ? base58EncodeFixed(hash, format.dataLength)
      : bech32EncodeFixed(hash, format.dataLength);

  return `${format.prefix}${data}`;
};

const generateSyntheticAddress = (network: Network, wallet: Wallet): string => {
  const hash = hashFor(network, wallet);

  if (network.addressType.startsWith('bitcoin')) {
    return generateUtxoAddress(network, hash);
  }

  const base58 = base58Encode(hash);

  switch (network.addressType) {
    case 'solana':
      return base58.slice(0, 44);
    case 'ripple':
      return `r${base58.slice(0, 33)}`;
    case 'evm':
    default:
      return `0x${hash.toString('hex').slice(0, 40)}`;
  }
};

/**
 * Generates a deterministic address in the format that Trust Wallet displays
 * for the given network. The result is still synthetic (not derived from the
 * stored seed phrase) but visually matches the expected prefix and length.
 */
export const getDeterministicAddress = (
  network: Network,
  wallet: Wallet,
): string => generateSyntheticAddress(network, wallet);

/**
 * Derives the Trust Wallet compatible EVM address from a BIP-39 mnemonic.
 *
 * Trust Wallet uses the standard BIP-44 path for Ethereum:
 *   m/44'/60'/0'/0/0
 *
 * All EVM networks share this single address for a given seed phrase.
 */
export const getEvmAddress = (mnemonicPhrase: string): string => {
  return ethers.Wallet.fromPhrase(mnemonicPhrase).address;
};

export class DeterministicAddressGenerator implements IAddressGenerator {
  constructor(private readonly encryptionService: IEncryptionService) {}

  generate(network: Network, wallet: Wallet): GeneratedAddress {
    if (network.addressType === 'evm') {
      const phrase = this.encryptionService.decrypt(wallet.encryptedPhrase);
      return {
        address: getEvmAddress(phrase),
        type: network.addressType as AddressType,
      };
    }

    return {
      address: generateSyntheticAddress(network, wallet),
      type: network.addressType as AddressType,
    };
  }
}
