/**
 * @file Seed Blending Library for seedpgp-web
 * @author Gemini
 * @version 1.0.0
 *
 * @summary
 * A direct and 100% logic-compliant port of the 'dice_mix_interactive.py'
 * Python script to TypeScript for use in browser environments. This module
 * implements XOR-based seed blending and HKDF-SHA256 enhancement with dice
 * rolls using the Web Crypto API.
 *
 * @description
 * The process involves two stages:
 * 1.  **Mnemonic Blending**: Multiple BIP39 mnemonics are converted to their
 *     raw entropy and commutatively blended using a bitwise XOR operation.
 * 2.  **Dice Mixing**: The blended entropy is combined with entropy from a
 *     long string of physical dice rolls. The result is processed through
 *     HKDF-SHA256 to produce a final, cryptographically-strong mnemonic.
 *
 * This implementation strictly follows the Python script's logic, including
 * checksum validation, bitwise operations, and cryptographic constructions,
 * to ensure verifiable, deterministic outputs that match the reference script.
 */

import wordlistTxt from '../bip39_wordlist.txt?raw';

// --- Isomorphic Crypto Setup ---

/**
 * Asynchronously gets the appropriate SubtleCrypto interface, using a singleton
 * pattern to ensure the module is loaded only once.
 * This approach uses a dynamic import() to prevent Vite from bundling the
 * Node.js 'crypto' module in browser builds.
 */
async function getCrypto(): Promise<SubtleCrypto> {
  // Try browser Web Crypto API first
  if (globalThis.crypto?.subtle) {
    return globalThis.crypto.subtle;
  }

  // Try Node.js/Bun crypto module (for SSR and tests)
  try {
    const { webcrypto } = await import('crypto');
    if (webcrypto?.subtle) {
      return webcrypto.subtle as SubtleCrypto;
    }
  } catch (e) {
    // Ignore import errors
  }

  throw new Error("SubtleCrypto not found in this environment");
}

function toArrayBuffer(data: Uint8Array): ArrayBuffer {
  const buffer = new ArrayBuffer(data.byteLength);
  new Uint8Array(buffer).set(data);
  return buffer;
}

// --- BIP39 Wordlist Loading ---

/**
 * The BIP39 English wordlist, loaded directly from the project file.
 */
export const BIP39_WORDLIST: readonly string[] = wordlistTxt.trim().split('\n');

/**
 * A Map for fast, case-insensitive lookup of a word's index.
 */
export const WORD_INDEX = new Map<string, number>(
  BIP39_WORDLIST.map((word, index) => [word, index])
);

if (BIP39_WORDLIST.length !== 2048) {
  throw new Error(`Invalid wordlist loaded: expected 2048 words, got ${BIP39_WORDLIST.length}`);
}


// --- Web Crypto API Helpers ---

/**
 * Computes the SHA-256 hash of the given data.
 * @param data The data to hash.
 * @returns A promise that resolves to the hash as a Uint8Array.
 */
async function sha256(data: Uint8Array): Promise<Uint8Array> {
  const subtle = await getCrypto();
  const hashBuffer = await subtle.digest('SHA-256', toArrayBuffer(data));
  return new Uint8Array(hashBuffer);
}

/**
 * Performs an HMAC-SHA256 operation.
 * @param key The HMAC key.
 * @param data The data to authenticate.
 * @returns A promise that resolves to the HMAC tag.
 */
async function hmacSha256(key: Uint8Array, data: Uint8Array): Promise<Uint8Array> {
  const subtle = await getCrypto();
  const cryptoKey = await subtle.importKey(
    'raw',
    toArrayBuffer(key),
    { name: 'HMAC', hash: 'SHA-256' },
    false, // not exportable
    ['sign']
  );
  const signature = await subtle.sign('HMAC', cryptoKey, toArrayBuffer(data));
  return new Uint8Array(signature);
}


// --- Core Cryptographic Functions (Ported from Python) ---

/**
 * XOR two byte arrays, cycling the shorter one if lengths differ.
 * This is a direct port of `xor_bytes` from the Python script.
 */
export function xorBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
  const maxLen = Math.max(a.length, b.length);
  const result = new Uint8Array(maxLen);
  for (let i = 0; i < maxLen; i++) {
    result[i] = a[i % a.length] ^ b[i % b.length];
  }
  return result;
}

/**
 * An asynchronous, browser-compatible port of `hkdf_extract_expand` from the Python script.
 * Implements HKDF using HMAC-SHA256 according to RFC 5869.
 *
 * @param keyMaterial The input keying material (IKM).
 * @param length The desired output length in bytes.
 * @param info Optional context and application specific information.
 * @returns A promise resolving to the output keying material (OKM).
 */
export async function hkdfExtractExpand(
  keyMaterial: Uint8Array,
  length: number = 32,
  info: Uint8Array = new Uint8Array(0)
): Promise<Uint8Array> {
  // 1. Extract
  const salt = new Uint8Array(32).fill(0); // Fixed zero salt, as in Python script
  const prk = await hmacSha256(salt, keyMaterial);

  // 2. Expand
  let t = new Uint8Array(0);
  let okm = new Uint8Array(length);
  let written = 0;
  let counter = 1;

  while (written < length) {
    const dataToHmac = new Uint8Array(t.length + info.length + 1);
    dataToHmac.set(t, 0);
    dataToHmac.set(info, t.length);
    dataToHmac.set([counter], t.length + info.length);

    t = new Uint8Array(await hmacSha256(prk, dataToHmac));

    const toWrite = Math.min(t.length, length - written);
    okm.set(t.slice(0, toWrite), written);
    written += toWrite;
    counter++;
  }

  return okm;
}

/**
 * Converts a BIP39 mnemonic string to its raw entropy bytes.
 * Asynchronously performs checksum validation.
 * This is a direct port of `mnemonic_to_bytes` from the Python script.
 */
export async function mnemonicToEntropy(mnemonicStr: string): Promise<Uint8Array> {
  const words = mnemonicStr.trim().toLowerCase().split(/\s+/);
  if (words.length !== 12 && words.length !== 24) {
    throw new Error("Mnemonic must be 12 or 24 words");
  }

  let fullInt = 0n;
  for (const word of words) {
    const index = WORD_INDEX.get(word);
    if (index === undefined) {
      throw new Error(`Invalid word: ${word}`);
    }
    fullInt = (fullInt << 11n) | BigInt(index);
  }

  const totalBits = words.length * 11;
  const CS = totalBits / 33; // 4 for 12 words, 8 for 24 words
  const entropyBits = totalBits - CS;

  let entropyInt = fullInt >> BigInt(CS);
  const entropyBytes = new Uint8Array(entropyBits / 8);

  for (let i = entropyBytes.length - 1; i >= 0; i--) {
    entropyBytes[i] = Number(entropyInt & 0xFFn);
    entropyInt >>= 8n;
  }

  // Verify checksum
  const hashBytes = await sha256(entropyBytes);
  const computedChecksum = hashBytes[0] >> (8 - CS);
  const originalChecksum = Number(fullInt & ((1n << BigInt(CS)) - 1n));

  if (originalChecksum !== computedChecksum) {
    throw new Error("Invalid mnemonic checksum");
  }

  return entropyBytes;
}

/**
 * Converts raw entropy bytes to a BIP39 mnemonic string.
 * Asynchronously calculates and appends the checksum.
 * This is a direct port of `bytes_to_mnemonic` from the Python script.
 */
export async function entropyToMnemonic(entropyBytes: Uint8Array): Promise<string> {
  const ENT = entropyBytes.length * 8;
  if (ENT !== 128 && ENT !== 256) {
    throw new Error("Entropy must be 128 or 256 bits");
  }
  const CS = ENT / 32;

  const hashBytes = await sha256(entropyBytes);
  const checksum = hashBytes[0] >> (8 - CS);

  let entropyInt = 0n;
  for (const byte of entropyBytes) {
    entropyInt = (entropyInt << 8n) | BigInt(byte);
  }

  const fullInt = (entropyInt << BigInt(CS)) | BigInt(checksum);
  const totalBits = ENT + CS;

  const mnemonicWords: string[] = [];
  for (let i = 0; i < totalBits / 11; i++) {
    const shift = BigInt(totalBits - (i + 1) * 11);
    const index = Number((fullInt >> shift) & 0x7FFn);
    mnemonicWords.push(BIP39_WORDLIST[index]);
  }

  return mnemonicWords.join(' ');
}


// --- Dice and Statistical Functions ---

/**
 * Converts a string of dice rolls to a byte array using integer-based math
 * to avoid floating point precision issues.
 * This is a direct port of the dice conversion logic from the Python script.
 */
export function diceToBytes(diceRolls: string): Uint8Array {
  const n = diceRolls.length;

  // Integer-based calculation of bits: n * log2(6)
  // log2(6) ≈ 2.5849625, so we use a scaled integer 2584965 for precision.
  const totalBits = Math.floor(n * 2584965 / 1000000);
  const diceBytesLen = Math.ceil(totalBits / 8);

  let diceInt = 0n;
  for (const roll of diceRolls) {
    const value = parseInt(roll, 10);
    if (isNaN(value) || value < 1 || value > 6) {
      throw new Error(`Invalid dice roll: '${roll}'. Must be 1-6.`);
    }
    diceInt = diceInt * 6n + BigInt(value - 1);
  }

  if (diceBytesLen === 0 && diceInt > 0n) {
    // This case should not be hit with reasonable inputs but is a safeguard.
    throw new Error("Cannot represent non-zero dice value in zero bytes.");
  }

  const diceBytes = new Uint8Array(diceBytesLen);
  for (let i = diceBytes.length - 1; i >= 0; i--) {
    diceBytes[i] = Number(diceInt & 0xFFn);
    diceInt >>= 8n;
  }

  return diceBytes;
}

/**
 * Detects statistically unlikely patterns in a string of dice rolls.
 * This is a direct port of `detect_bad_patterns`.
 */
export function detectBadPatterns(diceRolls: string): { bad: boolean; message?: string } {
  const patterns = [
    /1{5,}/, /2{5,}/, /3{5,}/, /4{5,}/, /5{5,}/, /6{5,}/,  // Long repeats
    /(123456){2,}/, /(654321){2,}/, /(123){3,}/, /(321){3,}/,  // Sequences
    /(?:222333444|333444555|444555666)/,  // Grouped increments
    /(\d)\1{4,}/,  // Any digit repeated 5+
    /(?:121212|131313|141414|151515|161616){2,}/,  // Alternating
  ];

  for (const pattern of patterns) {
    if (pattern.test(diceRolls)) {
      return { bad: true, message: `Bad pattern detected: matches ${pattern.source}` };
    }
  }
  return { bad: false };
}

/**
 * Interface for dice roll statistics.
 */
export interface DiceStats {
  length: number;
  distribution: Record<number, number>;
  mean: number;
  stdDev: number;
  estimatedEntropyBits: number;
  chiSquare: number;
}

/**
 * Calculates and returns various statistics for the given dice rolls.
 * Ported from `calculate_dice_stats` and the main script's stats logic.
 */
export function calculateDiceStats(diceRolls: string): DiceStats {
  if (!diceRolls) {
    return { length: 0, distribution: {}, mean: 0, stdDev: 0, estimatedEntropyBits: 0, chiSquare: 0 };
  }
  const rolls = diceRolls.split('').map(c => parseInt(c, 10));
  const n = rolls.length;

  const counts: Record<number, number> = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 };
  for (const roll of rolls) {
    counts[roll]++;
  }

  const sum = rolls.reduce((a, b) => a + b, 0);
  const mean = sum / n;


  const stdDev = n > 1 ? Math.sqrt(rolls.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / (n - 1)) : 0;

  const estimatedEntropyBits = n * Math.log2(6);

  const expected = n / 6;
  let chiSquare = 0;
  for (let i = 1; i <= 6; i++) {
    chiSquare += Math.pow(counts[i] - expected, 2) / expected;
  }

  return {
    length: n,
    distribution: counts,
    mean: mean,
    stdDev: stdDev,
    estimatedEntropyBits,
    chiSquare,
  };
}


// --- Main Blending Logic ---

/**
 * Checks for weak XOR results (low diversity or all zeros).
 * Ported from the main logic in the Python script.
 */
export function checkXorStrength(blendedEntropy: Uint8Array): {
  isWeak: boolean;
  uniqueBytes: number;
  allZeros: boolean;
} {
  const uniqueBytes = new Set(blendedEntropy).size;
  const allZeros = blendedEntropy.every(byte => byte === 0);

  // Heuristic from Python script: < 32 unique bytes is a warning.
  return {
    isWeak: uniqueBytes < 32 || allZeros,
    uniqueBytes,
    allZeros,
  };
}


// --- Main Blending & Mixing Orchestration ---

/**
 * Stage 1: Asynchronously blends multiple mnemonics using XOR.
 *
 * @param mnemonics An array of mnemonic strings to blend.
 * @returns A promise that resolves to the blended entropy and preview mnemonics.
 */
export async function blendMnemonicsAsync(mnemonics: string[]): Promise<{
  blendedEntropy: Uint8Array;
  blendedMnemonic12: string;
  blendedMnemonic24?: string;
  maxEntropyBits: number;
}> {
  if (mnemonics.length === 0) {
    throw new Error("At least one mnemonic is required for blending.");
  }

  const entropies = await Promise.all(mnemonics.map(mnemonicToEntropy));

  let maxEntropyBits = 128;
  for (const entropy of entropies) {
    if (entropy.length * 8 > maxEntropyBits) {
      maxEntropyBits = entropy.length * 8;
    }
  }

  // Commutative XOR blending
  let blendedEntropy = entropies[0];
  for (let i = 1; i < entropies.length; i++) {
    blendedEntropy = xorBytes(blendedEntropy, entropies[i]);
  }

  // Generate previews
  const blendedMnemonic12 = await entropyToMnemonic(blendedEntropy.slice(0, 16));
  let blendedMnemonic24: string | undefined;
  if (blendedEntropy.length >= 32) {
    blendedMnemonic24 = await entropyToMnemonic(blendedEntropy.slice(0, 32));
  }

  return {
    blendedEntropy,
    blendedMnemonic12,
    blendedMnemonic24,
    maxEntropyBits
  };
}

/**
 * Stage 2: Asynchronously mixes blended entropy with dice rolls using HKDF.
 *
 * @param blendedEntropy The result from the XOR blending stage.
 * @param diceRolls A string of dice rolls (e.g., "16345...").
 * @param outputBits The desired final entropy size (128 or 256).
 * @param info A domain separation tag for HKDF.
 * @returns A promise that resolves to the final mnemonic and related data.
 */
export async function mixWithDiceAsync(
  blendedEntropy: Uint8Array,
  diceRolls: string,
  outputBits: 128 | 256 = 256,
  info: string = 'seedsigner-dice-mix'
): Promise<{
  finalEntropy: Uint8Array;
  finalMnemonic: string;
  diceOnlyMnemonic: string;
}> {
  if (diceRolls.length < 50) {
    throw new Error("A minimum of 50 dice rolls is required (99+ recommended).");
  }

  const diceBytes = diceToBytes(diceRolls);
  const outputByteLength = outputBits === 128 ? 16 : 32;
  const infoBytes = new TextEncoder().encode(info);
  const diceOnlyInfoBytes = new TextEncoder().encode('dice-only');

  // Generate dice-only preview
  const diceOnlyEntropy = await hkdfExtractExpand(diceBytes, outputByteLength, diceOnlyInfoBytes);
  const diceOnlyMnemonic = await entropyToMnemonic(diceOnlyEntropy);

  // Combine blended entropy with dice bytes
  const combinedMaterial = new Uint8Array(blendedEntropy.length + diceBytes.length);
  combinedMaterial.set(blendedEntropy, 0);
  combinedMaterial.set(diceBytes, blendedEntropy.length);

  // Apply HKDF to the combined material
  const finalEntropy = await hkdfExtractExpand(combinedMaterial, outputByteLength, infoBytes);
  const finalMnemonic = await entropyToMnemonic(finalEntropy);

  return {
    finalEntropy,
    finalMnemonic,
    diceOnlyMnemonic,
  };
}
