/**
 * Primary-device flow: distribute Shamir shares of an identity backup
 * to a quorum of guardians.
 *
 * The setup happens entirely over existing 1:1 Shade sessions — no
 * server-side coordination, no separate transport. The primary calls
 * `setupRecovery({ shade, guardians, threshold, deliver })` once;
 * under the hood we:
 *
 *   1. Generate a 32-byte uniformly-random `recoveryKey`.
 *   2. Encode it as a base64url passphrase (`shade-rk:<encoded>`).
 *   3. Call `shade.exportBackup(passphrase, knownAddresses)` — that
 *      gives us an AES-GCM-protected backup blob whose key is derived
 *      from the recoveryKey via HKDF inside the SDK helper.
 *   4. Shamir-split the recoveryKey into `guardians.length` shares
 *      with threshold `threshold`.
 *   5. For each guardian, call `shade.send(...)` to encrypt a
 *      `share-deposit` envelope, then hand it to the caller-supplied
 *      `deliver` callback for transport. Failures on individual
 *      guardians do NOT abort the loop — partial-distribution is
 *      reported back so the caller can surface it.
 *   6. Zero the recoveryKey and the in-memory share buffers.
 */

import type { ShadeEnvelope } from '@shade/core';
import { SubtleCryptoProvider } from '@shade/crypto-web';
import type { Shade } from '@shade/sdk';
import {
  bytesToBase64Url,
  recoveryKeyToBackupPassphrase,
} from './encoding.js';
import { encodeRecoveryEnvelope, type ShareDepositEnvelope } from './protocol.js';
import { encodeShare, splitSecret } from './shamir.js';

/**
 * Caller-supplied envelope-delivery callback. The recovery package is
 * transport-agnostic by design (Shade itself is); whatever channel
 * the host application uses for plaintext messages — WebSocket, push
 * notification, polling, in-process pipe in tests — the caller
 * implements `deliver` to put the encrypted envelope on it. The
 * receiving side is expected to call `shade.receive(from, envelope)`
 * which will fire the `onMessage` handler that `attachGuardian` /
 * `requestRecovery` register.
 */
export type RecoveryDeliver = (to: string, envelope: ShadeEnvelope) => Promise<void>;

export interface SetupRecoveryOptions {
  /** Initialized Shade instance whose identity will be backed up. */
  shade: Shade;
  /**
   * Guardian addresses, in stable order. The order determines each
   * guardian's Shamir x-coordinate (1..n), so callers should keep the
   * list stable across re-runs of setup if they want predictable share
   * indices. Length must equal `n` and be in [1, 255].
   */
  guardians: ReadonlyArray<string>;
  /**
   * Reconstruction threshold `k`. Any subset of `k` guardians can
   * recover the identity; any subset of `k-1` reveals nothing. Must
   * satisfy `1 ≤ threshold ≤ guardians.length`.
   */
  threshold: number;
  /** Outbound transport — see {@link RecoveryDeliver}. */
  deliver: RecoveryDeliver;
  /**
   * Peer addresses whose Double-Ratchet sessions should be included in
   * the backup. Forwarded verbatim to `Shade.exportBackup`. Pass `[]`
   * (default) to back up identity + prekeys only.
   */
  knownAddresses?: ReadonlyArray<string>;
  /**
   * Optional caller-supplied setupId. Defaults to a fresh random id.
   * Useful when re-running setup to refresh share material in-place
   * without rotating the setupId (e.g. after replacing one guardian).
   */
  setupId?: string;
  /**
   * Wall-clock timestamp embedded in each share-deposit envelope.
   * Defaults to `Date.now()`. Tests inject a fixed value for
   * determinism.
   */
  now?: () => number;
}

export interface SetupRecoveryResult {
  /** Stable id the user records alongside their guardian roster. */
  setupId: string;
  /** k (threshold) and n (guardians.length), echoed for caller convenience. */
  threshold: number;
  guardianCount: number;
  /** Per-guardian outcome of the share-deposit send. */
  deliveries: ReadonlyArray<GuardianDelivery>;
  /** True iff every guardian got the deposit — convenience flag. */
  allDelivered: boolean;
  /** Original device's safety number at setup time, for the user's records. */
  setupFingerprint: string;
}

export interface GuardianDelivery {
  guardianAddress: string;
  shareIndex: number;
  /**
   * `null` when the deposit was sent successfully. Otherwise the
   * `Error` raised by `Shade.send` or the `deliver` callback. Failed
   * deliveries DO NOT abort the remaining sends — partial-distribution
   * is recoverable by retrying just the failures or removing the
   * unreachable guardian and re-running setup.
   */
  error: Error | null;
}

/**
 * Distribute a fresh Shamir-split recovery setup to the supplied
 * guardians. See module-level docs for the full flow. Idempotent only
 * insofar as the caller pins `setupId` across re-runs; without a
 * pinned id every call generates a new (recoveryKey, setupId) pair so
 * old shares are silently superseded.
 */
export async function setupRecovery(opts: SetupRecoveryOptions): Promise<SetupRecoveryResult> {
  validateOptions(opts);
  const crypto = new SubtleCryptoProvider();
  const guardianCount = opts.guardians.length;
  const threshold = opts.threshold;
  const setupId = opts.setupId ?? bytesToBase64Url(crypto.randomBytes(16));
  const now = opts.now ?? Date.now;

  // 1. Random recoveryKey + 2/3. Backup blob keyed by recoveryKey-derived passphrase.
  const recoveryKey = crypto.randomBytes(32);
  let setupFingerprint: string;
  let backupBlob: string;
  try {
    const passphrase = recoveryKeyToBackupPassphrase(recoveryKey);
    setupFingerprint = await opts.shade.fingerprint;
    backupBlob = await opts.shade.exportBackup(passphrase, [...(opts.knownAddresses ?? [])]);
  } catch (err) {
    crypto.zeroize(recoveryKey);
    throw err;
  }

  // 4. Shamir-split the recoveryKey.
  let shares;
  try {
    shares = splitSecret(
      recoveryKey,
      threshold,
      guardianCount,
      (length) => crypto.randomBytes(length),
    );
  } finally {
    // The recoveryKey lives in `splitSecret` only as the constant term
    // of each per-byte polynomial; once split returns we can wipe it.
    crypto.zeroize(recoveryKey);
  }

  // 5. Deliver one share-deposit per guardian.
  const flowId = `setup:${setupId}`;
  const createdAt = now();
  const deliveries: GuardianDelivery[] = [];

  for (let i = 0; i < guardianCount; i++) {
    const guardianAddress = opts.guardians[i]!;
    const share = shares[i]!;
    const shareBytes = bytesToBase64Url(encodeShare(share));
    const envelope: ShareDepositEnvelope = {
      shadeRecovery: 1,
      type: 'share-deposit',
      flowId,
      originalAddress: opts.shade.myAddress,
      setupId,
      threshold,
      guardianCount,
      shareIndex: share.x,
      shareBytes,
      backupBlob,
      setupFingerprint,
      createdAt,
    };
    const plaintext = encodeRecoveryEnvelope(envelope);
    let error: Error | null = null;
    try {
      const env = await opts.shade.send(guardianAddress, plaintext);
      await opts.deliver(guardianAddress, env);
    } catch (err) {
      error = err instanceof Error ? err : new Error(String(err));
    }
    deliveries.push({ guardianAddress, shareIndex: share.x, error });
    // Wipe the share's y-buffer once it has been encoded + sent so it
    // doesn't linger in JS memory longer than necessary.
    share.y.fill(0);
  }

  return {
    setupId,
    threshold,
    guardianCount,
    deliveries,
    allDelivered: deliveries.every((d) => d.error === null),
    setupFingerprint,
  };
}

function validateOptions(opts: SetupRecoveryOptions): void {
  if (typeof opts.threshold !== 'number' || !Number.isInteger(opts.threshold)) {
    throw new TypeError('setupRecovery: threshold must be an integer');
  }
  if (opts.threshold < 1) {
    throw new RangeError('setupRecovery: threshold must be ≥ 1');
  }
  if (!Array.isArray(opts.guardians) || opts.guardians.length === 0) {
    throw new RangeError('setupRecovery: guardians must be a non-empty array');
  }
  if (opts.guardians.length > 255) {
    throw new RangeError('setupRecovery: guardians.length must be ≤ 255 (GF(2^8))');
  }
  if (opts.threshold > opts.guardians.length) {
    throw new RangeError('setupRecovery: threshold must be ≤ guardians.length');
  }
  if (new Set(opts.guardians).size !== opts.guardians.length) {
    throw new RangeError('setupRecovery: guardians must be unique');
  }
  if (opts.guardians.includes(opts.shade.myAddress)) {
    throw new RangeError(
      'setupRecovery: cannot use your own address as a guardian (the share would die with the device)',
    );
  }
  if (typeof opts.deliver !== 'function') {
    throw new TypeError('setupRecovery: deliver must be a function');
  }
}
