/**
 * Transaction building utilities.
 *
 * ## Hybrid Signature Support
 *
 * Synor transactions require hybrid signatures combining:
 * - Ed25519 (classical, 64 bytes)
 * - Dilithium3/ML-DSA-65 (post-quantum, ~3.3KB)
 *
 * Both signatures must verify for a transaction to be valid.
 * This provides defense-in-depth against both classical and quantum attacks.
 */

import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
import {
  hash,
  type Keypair,
  createHybridSignatureLocal,
  createHybridSignatureSmart,
  type SigningConfig,
} from './crypto';
import { getClient, type Utxo } from './rpc';

export interface TxInput {
  previousTxId: string;
  outputIndex: number;
  /** Ed25519 signature (64 bytes hex) - for backwards compatibility */
  signature?: string;
  /** Hybrid signature containing both Ed25519 and Dilithium components */
  hybridSignature?: {
    ed25519: string;
    dilithium: string;
  };
  publicKey?: string;
  /** Dilithium public key (1952 bytes hex) - required for hybrid verification */
  dilithiumPublicKey?: string;
}

export interface TxOutput {
  address: string;
  amount: bigint;
}

export interface UnsignedTransaction {
  version: number;
  inputs: TxInput[];
  outputs: TxOutput[];
  lockTime: number;
}

export interface SignedTransaction extends UnsignedTransaction {
  id: string;
  /** Indicates if this transaction uses hybrid signatures */
  isHybrid?: boolean;
}

// Synor uses 8 decimal places (like satoshis)
const SYNOR_DECIMALS = 8;
const SYNOR_UNIT = BigInt(10 ** SYNOR_DECIMALS);

/**
 * Convert human-readable amount to smallest unit (somas).
 */
export function toSomas(amount: string): bigint {
  const [whole, decimal = ''] = amount.split('.');
  const paddedDecimal = decimal.padEnd(SYNOR_DECIMALS, '0').slice(0, SYNOR_DECIMALS);
  return BigInt(whole) * SYNOR_UNIT + BigInt(paddedDecimal);
}

/**
 * Convert somas to human-readable amount.
 */
export function fromSomas(somas: bigint): string {
  const str = somas.toString().padStart(SYNOR_DECIMALS + 1, '0');
  const whole = str.slice(0, -SYNOR_DECIMALS) || '0';
  const decimal = str.slice(-SYNOR_DECIMALS).replace(/0+$/, '');
  return decimal ? `${whole}.${decimal}` : whole;
}

/**
 * Select UTXOs for a transaction using simple accumulator.
 * Returns selected UTXOs and change amount.
 *
 * For hybrid signatures, we account for the larger signature size (~3.4KB per input)
 */
export function selectUtxos(
  utxos: Utxo[],
  targetAmount: bigint,
  feePerByte: bigint = BigInt(1),
  isHybrid: boolean = true
): { selected: Utxo[]; change: bigint; fee: bigint } | null {
  // Sort by amount descending for efficiency
  const sorted = [...utxos].sort((a, b) => {
    const amtA = toSomas(a.amount);
    const amtB = toSomas(b.amount);
    return amtA > amtB ? -1 : amtA < amtB ? 1 : 0;
  });

  const selected: Utxo[] = [];
  let accumulated = BigInt(0);

  // Signature size: Ed25519 = 64 bytes, Dilithium = 3293 bytes
  // Public key size: Ed25519 = 32 bytes, Dilithium = 1952 bytes
  const inputSize = isHybrid
    ? 150 + 64 + 3293 + 32 + 1952 // ~5.5KB per hybrid input
    : 150; // ~150 bytes for Ed25519-only

  for (const utxo of sorted) {
    selected.push(utxo);
    accumulated += toSomas(utxo.amount);

    // Estimate fee based on tx size
    const estimatedSize = BigInt(selected.length * inputSize + 2 * 34 + 10);
    const fee = estimatedSize * feePerByte;
    const totalNeeded = targetAmount + fee;

    if (accumulated >= totalNeeded) {
      const change = accumulated - totalNeeded;
      return { selected, change, fee };
    }
  }

  return null; // Insufficient funds
}

/**
 * Build an unsigned transaction.
 */
export function buildTransaction(
  inputs: { utxo: Utxo }[],
  outputs: { address: string; amount: bigint }[],
  changeAddress: string,
  changeAmount: bigint
): UnsignedTransaction {
  const txInputs: TxInput[] = inputs.map(({ utxo }) => ({
    previousTxId: utxo.txId,
    outputIndex: utxo.outputIndex,
  }));

  const txOutputs: TxOutput[] = outputs.map(({ address, amount }) => ({
    address,
    amount,
  }));

  // Add change output if non-zero
  if (changeAmount > 0) {
    txOutputs.push({
      address: changeAddress,
      amount: changeAmount,
    });
  }

  return {
    version: 1,
    inputs: txInputs,
    outputs: txOutputs,
    lockTime: 0,
  };
}

/**
 * Serialize transaction for signing/hashing.
 * Uses a simple format: version || inputs || outputs || lockTime
 */
export function serializeForSigning(tx: UnsignedTransaction): Uint8Array {
  const parts: Uint8Array[] = [];

  // Version (4 bytes, little-endian)
  const versionBytes = new Uint8Array(4);
  new DataView(versionBytes.buffer).setUint32(0, tx.version, true);
  parts.push(versionBytes);

  // Input count (varint - simplified as 1 byte for now)
  parts.push(new Uint8Array([tx.inputs.length]));

  // Inputs
  for (const input of tx.inputs) {
    parts.push(hexToBytes(input.previousTxId));
    const indexBytes = new Uint8Array(4);
    new DataView(indexBytes.buffer).setUint32(0, input.outputIndex, true);
    parts.push(indexBytes);
  }

  // Output count
  parts.push(new Uint8Array([tx.outputs.length]));

  // Outputs
  for (const output of tx.outputs) {
    // Amount (8 bytes, little-endian)
    const amountBytes = new Uint8Array(8);
    new DataView(amountBytes.buffer).setBigUint64(0, output.amount, true);
    parts.push(amountBytes);

    // Address (as UTF-8 bytes with length prefix)
    const addressBytes = new TextEncoder().encode(output.address);
    parts.push(new Uint8Array([addressBytes.length]));
    parts.push(addressBytes);
  }

  // Lock time (4 bytes)
  const lockTimeBytes = new Uint8Array(4);
  new DataView(lockTimeBytes.buffer).setUint32(0, tx.lockTime, true);
  parts.push(lockTimeBytes);

  // Concatenate all parts
  const totalLength = parts.reduce((sum, p) => sum + p.length, 0);
  const result = new Uint8Array(totalLength);
  let offset = 0;
  for (const part of parts) {
    result.set(part, offset);
    offset += part.length;
  }

  return result;
}

/**
 * Sign a transaction with hybrid signatures (Ed25519 + Dilithium3).
 *
 * This is the recommended signing method for quantum-resistant transactions.
 * Both signatures must verify for the transaction to be valid on the network.
 *
 * @param tx - The unsigned transaction to sign
 * @param seed - The 64-byte BIP-39 seed for key derivation
 * @param ed25519PublicKey - The Ed25519 public key bytes
 * @param dilithiumPublicKey - The Dilithium public key bytes (1952 bytes)
 * @param config - Optional signing configuration
 */
export async function signTransactionHybrid(
  tx: UnsignedTransaction,
  seed: Uint8Array,
  ed25519PublicKey: Uint8Array,
  dilithiumPublicKey: Uint8Array,
  config?: SigningConfig
): Promise<SignedTransaction> {
  const serialized = serializeForSigning(tx);
  const txHash = hash(serialized);

  // Sign each input with hybrid signatures
  const signedInputs: TxInput[] = [];
  for (const input of tx.inputs) {
    // Create hybrid signature (Ed25519 + Dilithium)
    const hybridSig = config
      ? await createHybridSignatureSmart(txHash, seed, config)
      : await createHybridSignatureLocal(txHash, seed);

    signedInputs.push({
      ...input,
      // Include both signature formats for compatibility
      signature: bytesToHex(hybridSig.ed25519),
      hybridSignature: {
        ed25519: bytesToHex(hybridSig.ed25519),
        dilithium: bytesToHex(hybridSig.dilithium),
      },
      publicKey: bytesToHex(ed25519PublicKey),
      dilithiumPublicKey: bytesToHex(dilithiumPublicKey),
    });
  }

  return {
    ...tx,
    inputs: signedInputs,
    id: bytesToHex(txHash),
    isHybrid: true,
  };
}

/**
 * Sign a transaction with Ed25519 only (legacy mode).
 *
 * @deprecated Use signTransactionHybrid for quantum-resistant transactions.
 * This function is provided for backwards compatibility only.
 */
export async function signTransaction(
  tx: UnsignedTransaction,
  keypair: Keypair
): Promise<SignedTransaction> {
  // Import the sign function for Ed25519-only signing
  const { sign } = await import('./crypto');

  const serialized = serializeForSigning(tx);
  const txHash = hash(serialized);

  // Sign each input with Ed25519 only
  const signedInputs: TxInput[] = [];
  for (const input of tx.inputs) {
    const signature = await sign(txHash, keypair.privateKey);

    signedInputs.push({
      ...input,
      signature: bytesToHex(signature),
      publicKey: bytesToHex(keypair.publicKey),
    });
  }

  return {
    ...tx,
    inputs: signedInputs,
    id: bytesToHex(txHash),
    isHybrid: false,
  };
}

/**
 * Serialize signed transaction for submission.
 */
export function serializeTransaction(tx: SignedTransaction): string {
  return JSON.stringify({
    version: tx.version,
    inputs: tx.inputs.map((input) => ({
      previousTxId: input.previousTxId,
      outputIndex: input.outputIndex,
      signature: input.signature,
      hybridSignature: input.hybridSignature,
      publicKey: input.publicKey,
      dilithiumPublicKey: input.dilithiumPublicKey,
    })),
    outputs: tx.outputs.map((o) => ({
      address: o.address,
      amount: o.amount.toString(),
    })),
    lockTime: tx.lockTime,
    isHybrid: tx.isHybrid,
  });
}

/**
 * Wallet data with both Ed25519 and Dilithium keys.
 */
export interface WalletWithDilithium {
  seed: Uint8Array;
  keypair: Keypair;
  address: string;
  dilithiumPublicKey: Uint8Array;
}

/**
 * High-level send function with hybrid signatures.
 *
 * This is the recommended way to send transactions for quantum resistance.
 *
 * @param fromAddress - The sender's address
 * @param toAddress - The recipient's address
 * @param amount - Amount to send in human-readable format
 * @param wallet - Wallet containing seed, keypair, and Dilithium public key
 * @param config - Optional signing configuration
 */
export async function createSendTransactionHybrid(
  fromAddress: string,
  toAddress: string,
  amount: string,
  wallet: WalletWithDilithium,
  config?: SigningConfig
): Promise<SignedTransaction> {
  const client = getClient();

  // Get UTXOs
  const utxos = await client.getUtxos(fromAddress);
  const targetAmount = toSomas(amount);

  // Select UTXOs (account for larger hybrid signature size)
  const selection = selectUtxos(utxos, targetAmount, BigInt(1), true);
  if (!selection) {
    throw new Error('Insufficient funds');
  }

  // Build transaction
  const tx = buildTransaction(
    selection.selected.map((utxo) => ({ utxo })),
    [{ address: toAddress, amount: targetAmount }],
    fromAddress,
    selection.change
  );

  // Sign with hybrid signatures
  return signTransactionHybrid(
    tx,
    wallet.seed,
    wallet.keypair.publicKey,
    wallet.dilithiumPublicKey,
    config
  );
}

/**
 * High-level send function (legacy Ed25519 only).
 *
 * @deprecated Use createSendTransactionHybrid for quantum-resistant transactions.
 */
export async function createSendTransaction(
  fromAddress: string,
  toAddress: string,
  amount: string,
  keypair: Keypair
): Promise<SignedTransaction> {
  const client = getClient();

  // Get UTXOs
  const utxos = await client.getUtxos(fromAddress);
  const targetAmount = toSomas(amount);

  // Select UTXOs (Ed25519-only mode)
  const selection = selectUtxos(utxos, targetAmount, BigInt(1), false);
  if (!selection) {
    throw new Error('Insufficient funds');
  }

  // Build transaction
  const tx = buildTransaction(
    selection.selected.map((utxo) => ({ utxo })),
    [{ address: toAddress, amount: targetAmount }],
    fromAddress,
    selection.change
  );

  // Sign transaction
  return signTransaction(tx, keypair);
}
