/**
 * Shamir Secret Sharing over GF(2^8).
 *
 * Splits a secret byte-array into `n` shares such that any `k` (the
 * threshold) reconstruct the original, but any combination of `k-1` or
 * fewer reveals nothing about the secret beyond its length. Each byte of
 * the secret is shared independently using a random polynomial of degree
 * `k-1` whose constant term is the secret byte; shares are points on that
 * polynomial evaluated at `x = 1..n`.
 *
 * Field: GF(2^8) with the irreducible polynomial 0x11b (AES'). Tables for
 * exp and log are precomputed at module load time and reused for both
 * multiplication and inversion. All field operations are constant-time
 * (table lookups + xor / mod), so split + combine leak nothing about the
 * secret bytes through timing.
 *
 * The wire format for a share is:
 *
 *   1 byte   x-coordinate (1..255)
 *   N bytes  y-coordinates, one per secret byte
 *
 * `splitSecret` returns an array of length `n`. `combineShares` accepts
 * any subset of shares; the threshold is implicit in the polynomial
 * degree the sender chose. Combining `k-1` shares yields a different
 * (random-looking) result — there's no way to detect under-threshold
 * combination from the shares alone, so callers must enforce the
 * threshold separately (e.g. by waiting for `k` arrivals before combining)
 * AND verify the reconstructed key against the AEAD tag of the
 * ciphertext it was meant to decrypt.
 */

const FIELD_SIZE = 256;

const EXP = new Uint8Array(FIELD_SIZE * 2);
const LOG = new Uint8Array(FIELD_SIZE);

(function buildTables(): void {
  // Generator: 0x03. Standard AES-style table buildup.
  let x = 1;
  for (let i = 0; i < 255; i++) {
    EXP[i] = x;
    LOG[x] = i;
    // Multiply x by the generator (0x03) in GF(2^8) with the AES
    // reduction polynomial 0x1b on overflow.
    let next = x ^ ((x << 1) & 0xff);
    if (x & 0x80) next ^= 0x1b;
    x = next & 0xff;
  }
  // Mirror the first half so `EXP[i + j]` works for any 0..510 without
  // an extra modulus.
  for (let i = 255; i < FIELD_SIZE * 2; i++) EXP[i] = EXP[i - 255]!;
})();

function gfMul(a: number, b: number): number {
  if (a === 0 || b === 0) return 0;
  return EXP[LOG[a]! + LOG[b]!]!;
}

function gfDiv(a: number, b: number): number {
  if (b === 0) throw new Error('Shamir: division by zero');
  if (a === 0) return 0;
  // a/b = exp(log(a) - log(b)) — keep the index non-negative by adding 255.
  return EXP[LOG[a]! - LOG[b]! + 255]!;
}

/**
 * Evaluate a polynomial `coeffs[0] + coeffs[1]*x + … + coeffs[d]*x^d`
 * at point `x` using Horner's method. Constant-time in the coefficients
 * (no early-out on zero terms).
 */
function evalPoly(coeffs: Uint8Array, x: number): number {
  let acc = coeffs[coeffs.length - 1]!;
  for (let i = coeffs.length - 2; i >= 0; i--) {
    acc = gfMul(acc, x) ^ coeffs[i]!;
  }
  return acc;
}

/**
 * One Shamir share.
 *
 *  - `x`: the x-coordinate, 1..255 (unique per share within a split).
 *  - `y`: a y-coordinate for each byte of the original secret.
 */
export interface ShamirShare {
  x: number;
  y: Uint8Array;
}

/**
 * Split `secret` into `n` shares; any `k` reconstructs.
 *
 * @param secret  the bytes to split (any length ≥ 1)
 * @param k       threshold (1 ≤ k ≤ n ≤ 255)
 * @param n       total number of shares
 * @param random  RNG callback (length → bytes). Must be cryptographically
 *                secure. Inject `crypto.randomBytes.bind(crypto)` from a
 *                `CryptoProvider` to reuse the SDK's RNG.
 * @returns       array of `n` shares with x-coordinates 1..n (skipping 0
 *                because P(0) IS the secret).
 */
export function splitSecret(
  secret: Uint8Array,
  k: number,
  n: number,
  random: (length: number) => Uint8Array,
): ShamirShare[] {
  if (!Number.isInteger(k) || !Number.isInteger(n)) {
    throw new Error('Shamir: k and n must be integers');
  }
  if (k < 1) throw new Error('Shamir: threshold must be ≥ 1');
  if (n < k) throw new Error('Shamir: n must be ≥ k');
  if (n > 255) throw new Error('Shamir: n must be ≤ 255 (GF(2^8) limit)');
  if (secret.length === 0) throw new Error('Shamir: secret must be non-empty');

  // Pre-allocate per-share y-buffers. We fill column-wise: for each byte
  // of the secret we draw a fresh random polynomial of degree k-1 with
  // constant term equal to the secret byte, then emit y[byteIndex] for
  // each share at its x-coordinate.
  const shares: ShamirShare[] = [];
  for (let i = 0; i < n; i++) {
    shares.push({ x: i + 1, y: new Uint8Array(secret.length) });
  }

  // For each column (byte of the secret), build a fresh polynomial.
  const coeffs = new Uint8Array(k);
  for (let byteIdx = 0; byteIdx < secret.length; byteIdx++) {
    coeffs[0] = secret[byteIdx]!;
    if (k > 1) {
      const rnd = random(k - 1);
      for (let j = 0; j < k - 1; j++) coeffs[j + 1] = rnd[j]!;
    }
    for (let i = 0; i < n; i++) {
      const share = shares[i]!;
      share.y[byteIdx] = evalPoly(coeffs, share.x);
    }
    // Zero the polynomial buffer between columns. Doesn't help against a
    // V8 GC adversary but is the right discipline.
    coeffs.fill(0);
  }
  return shares;
}

/**
 * Reconstruct the original secret from a subset of shares using
 * Lagrange interpolation evaluated at x = 0.
 *
 * `shares.length` MUST equal the threshold the secret was split with
 * (the algorithm doesn't know `k`; supplying fewer or more shares either
 * yields garbage or wastes work). The returned secret has the same
 * length as each share's y-buffer.
 *
 * The secret cannot be authenticated from the shares alone — the caller
 * is expected to verify the reconstructed key against an AEAD tag (e.g.
 * the AES-GCM ciphertext it was used to encrypt). Without that
 * authentication, an attacker who supplied a forged share at any of the
 * sample points can flip the reconstructed key to anything they want.
 */
export function combineShares(shares: ShamirShare[]): Uint8Array {
  if (shares.length === 0) throw new Error('Shamir: need at least one share');
  const len = shares[0]!.y.length;
  for (const s of shares) {
    if (s.y.length !== len) throw new Error('Shamir: share length mismatch');
    if (s.x === 0) throw new Error('Shamir: x-coordinate 0 is reserved for the secret');
  }
  // Reject duplicate x-coordinates: two shares with the same x but
  // different y collide as polynomial evaluations and produce nonsense.
  const xs = new Set<number>();
  for (const s of shares) {
    if (xs.has(s.x)) throw new Error('Shamir: duplicate x-coordinate in share set');
    xs.add(s.x);
  }

  const out = new Uint8Array(len);

  // Precompute Lagrange basis weights at x=0:
  //   L_i(0) = ∏_{j ≠ i}  -x_j / (x_i - x_j)
  // In GF(2^8) negation is identity, so this simplifies to ∏ x_j / (x_i + x_j).
  const weights = new Uint8Array(shares.length);
  for (let i = 0; i < shares.length; i++) {
    let num = 1;
    let den = 1;
    const xi = shares[i]!.x;
    for (let j = 0; j < shares.length; j++) {
      if (i === j) continue;
      const xj = shares[j]!.x;
      num = gfMul(num, xj);
      den = gfMul(den, xi ^ xj);
    }
    weights[i] = gfDiv(num, den);
  }

  for (let byteIdx = 0; byteIdx < len; byteIdx++) {
    let acc = 0;
    for (let i = 0; i < shares.length; i++) {
      acc ^= gfMul(shares[i]!.y[byteIdx]!, weights[i]!);
    }
    out[byteIdx] = acc;
  }
  return out;
}

/**
 * Encode a `ShamirShare` to a single Uint8Array on the wire:
 *
 *   1 byte   x-coordinate
 *   N bytes  y-coordinates
 *
 * `decodeShare` is the inverse. Callers ship this byte sequence as
 * opaque payload — the encoding is stable across platforms and is what
 * `@shade/recovery` puts on the wire.
 */
export function encodeShare(share: ShamirShare): Uint8Array {
  if (share.x < 1 || share.x > 255) throw new Error('Shamir: x out of range [1..255]');
  const out = new Uint8Array(1 + share.y.length);
  out[0] = share.x;
  out.set(share.y, 1);
  return out;
}

export function decodeShare(bytes: Uint8Array): ShamirShare {
  if (bytes.length < 2) throw new Error('Shamir: encoded share must be ≥ 2 bytes');
  const x = bytes[0]!;
  if (x === 0) throw new Error('Shamir: x-coordinate 0 in encoded share');
  return { x, y: bytes.slice(1) };
}
