/**
 * Guardian-side receiver.
 *
 * `attachGuardian` wires a `Shade.onMessage` handler that handles every
 * recovery envelope addressed to this device:
 *
 *   - `share-deposit`: persist the share + backup blob in the supplied
 *     `RecoveryStore`. Idempotent on (originalAddress, setupId).
 *
 *   - `recovery-request`: invoke the `approve` callback with the
 *     ratcheted requester address + the safety number embedded in the
 *     envelope. The approve callback is the user-confronted gate — it
 *     SHOULD pop UI showing both fingerprints and require an explicit
 *     OOB-confirmed click. Returning `true` ships a `share-grant`;
 *     returning `false` (or throwing) ships a `share-decline` with the
 *     reason. Recovery-requests for unknown originalAddress/setupId
 *     pairs are auto-declined with `"unknown setup"`.
 *
 *   - All other recovery types are ignored (we only care about the
 *     guardian's inbound side; share-grant + share-decline target the
 *     new device, not the guardian).
 *
 * The handler returns nothing — it never blocks `Shade.receive`. All
 * outbound replies happen via the same `deliver` callback the caller
 * supplied, so the caller's transport layer is the only piece that
 * touches the wire.
 *
 * Returned function is the detach handle — calling it removes the
 * onMessage handler and frees the registry.
 */

import type { Shade } from '@shade/sdk';
import {
  encodeRecoveryEnvelope,
  tryParseRecoveryEnvelope,
  type RecoveryEnvelope,
  type ShareDeclineEnvelope,
  type ShareGrantEnvelope,
} from './protocol.js';
import type { GuardianShareEntry, RecoveryStore } from './store.js';
import type { RecoveryDeliver } from './setup.js';

export interface GuardianApproveContext {
  /** Address of the device requesting recovery (the new device's temporary identity). */
  requesterAddress: string;
  /** Address of the original (lost) device whose share is being requested. */
  originalAddress: string;
  /** Setup id from the request — matches a deposit in the store. */
  setupId: string;
  /** Safety number of the new device's TEMPORARY identity. Show this to the user. */
  requesterFingerprint: string;
  /** Safety number of the original device at deposit time. Show this for comparison. */
  setupFingerprint: string;
  /** When the deposit was originally made. */
  depositCreatedAt: number;
  /** When the request was received. */
  requestReceivedAt: number;
}

/**
 * Async predicate the caller registers to authorize a `recovery-request`.
 * Return `true` to release the share, `false` (or throw) to decline.
 *
 * Implementations MUST be irrevocably user-driven — never auto-approve,
 * because the social-engineering threat (V3.10 risk #2) is exactly that
 * an attacker imitates the original user. The default
 * `<RecoveryApprove />` widget enforces an explicit OOB-confirmation
 * checkbox + "I have verified the safety numbers match" gate before
 * resolving `true`.
 */
export type GuardianApproveHandler = (ctx: GuardianApproveContext) => Promise<boolean>;

export interface AttachGuardianOptions {
  /** Initialized Shade instance whose onMessage will be subscribed. */
  shade: Shade;
  /** Persistent storage for received shares. */
  store: RecoveryStore;
  /** User-driven approval predicate. See {@link GuardianApproveHandler}. */
  approve: GuardianApproveHandler;
  /** Outbound transport for share-grant / share-decline replies. */
  deliver: RecoveryDeliver;
  /**
   * Optional hook fired when the guardian persists a fresh deposit.
   * Used by widget layers to reactively re-render the deposit list.
   */
  onDeposit?: (entry: GuardianShareEntry) => void;
  /**
   * Optional logger for protocol-level anomalies. Defaults to
   * `console.warn`. Pass a no-op for silent operation in tests.
   */
  onProtocolError?: (err: Error, source: string) => void;
  /** Wall-clock source for `receivedAt`. Defaults to `Date.now`. */
  now?: () => number;
}

export interface AttachedGuardian {
  /** Detach the onMessage handler. Idempotent. */
  stop: () => void;
}

/**
 * Wire a Shade instance to act as a guardian. Returns a detach handle
 * the caller invokes on shutdown.
 */
export function attachGuardian(opts: AttachGuardianOptions): AttachedGuardian {
  if (typeof opts.deliver !== 'function') {
    throw new TypeError('attachGuardian: deliver must be a function');
  }
  if (typeof opts.approve !== 'function') {
    throw new TypeError('attachGuardian: approve must be a function');
  }
  const onProtocolError = opts.onProtocolError ?? defaultProtocolErrorLogger;
  const now = opts.now ?? Date.now;

  const detach = opts.shade.onMessage(async (from, plaintext) => {
    let env: RecoveryEnvelope | null;
    try {
      env = tryParseRecoveryEnvelope(plaintext);
    } catch (err) {
      onProtocolError(err as Error, `from=${from}`);
      return;
    }
    if (env === null) return; // not a recovery message — ignore.

    try {
      switch (env.type) {
        case 'share-deposit':
          await handleDeposit(opts, env, now());
          return;
        case 'recovery-request':
          await handleRequest(opts, from, env, now());
          return;
        case 'share-grant':
        case 'share-decline':
          // Replies belong to the new-device flow — guardians don't act on them.
          return;
      }
    } catch (err) {
      onProtocolError(err as Error, `type=${env.type} from=${from}`);
    }
  });

  let stopped = false;
  return {
    stop: () => {
      if (stopped) return;
      stopped = true;
      detach();
    },
  };
}

async function handleDeposit(
  opts: AttachGuardianOptions,
  env: import('./protocol.js').ShareDepositEnvelope,
  receivedAt: number,
): Promise<void> {
  const entry: GuardianShareEntry = {
    originalAddress: env.originalAddress,
    setupId: env.setupId,
    shareIndex: env.shareIndex,
    shareBytes: env.shareBytes,
    // The backupBlob string contains the AES-GCM-protected payload; we
    // split it into "shareSecret" fields per the V3.10 schema for
    // legibility while keeping the original blob string intact.
    shareSecretCiphertext: env.backupBlob,
    shareSecretNonce: '',
    setupFingerprint: env.setupFingerprint,
    guardianCount: env.guardianCount,
    threshold: env.threshold,
    receivedAt,
  };
  await opts.store.save(entry);
  opts.onDeposit?.(entry);
}

async function handleRequest(
  opts: AttachGuardianOptions,
  from: string,
  env: import('./protocol.js').RecoveryRequestEnvelope,
  receivedAt: number,
): Promise<void> {
  const stored = await opts.store.get(env.originalAddress, env.setupId);
  if (stored === null) {
    await sendDecline(opts, from, env, 'unknown setup');
    return;
  }

  let approved: boolean;
  try {
    approved = await opts.approve({
      requesterAddress: from,
      originalAddress: env.originalAddress,
      setupId: env.setupId,
      requesterFingerprint: env.requesterFingerprint,
      setupFingerprint: stored.setupFingerprint,
      depositCreatedAt: stored.receivedAt,
      requestReceivedAt: receivedAt,
    });
  } catch (err) {
    await sendDecline(
      opts,
      from,
      env,
      `approve handler threw: ${(err as Error).message}`,
    );
    return;
  }
  if (!approved) {
    await sendDecline(opts, from, env, 'user declined');
    return;
  }

  const grant: ShareGrantEnvelope = {
    shadeRecovery: 1,
    type: 'share-grant',
    flowId: env.flowId,
    originalAddress: env.originalAddress,
    setupId: env.setupId,
    shareIndex: stored.shareIndex,
    shareBytes: stored.shareBytes,
    backupBlob: stored.shareSecretCiphertext,
  };
  const plaintext = encodeRecoveryEnvelope(grant);
  const envelope = await opts.shade.send(from, plaintext);
  await opts.deliver(from, envelope);
}

async function sendDecline(
  opts: AttachGuardianOptions,
  to: string,
  env: import('./protocol.js').RecoveryRequestEnvelope,
  reason: string,
): Promise<void> {
  const decline: ShareDeclineEnvelope = {
    shadeRecovery: 1,
    type: 'share-decline',
    flowId: env.flowId,
    originalAddress: env.originalAddress,
    setupId: env.setupId,
    reason,
  };
  const plaintext = encodeRecoveryEnvelope(decline);
  const envelope = await opts.shade.send(to, plaintext);
  await opts.deliver(to, envelope);
}

function defaultProtocolErrorLogger(err: Error, source: string): void {
  console.warn(`[shade-recovery] guardian dropped malformed envelope (${source}): ${err.message}`);
}
