import type { StorageProvider, PeerVerificationSource } from '@shade/core';
import { FingerprintNotVerifiedError } from '@shade/core';

/**
 * Reason a gate was triggered. Surfaced to the registered handler so apps
 * can render different UI per gate.
 */
export type FingerprintGate =
  | 'first-large-file'
  | 'backup-import'
  | 'new-device-trust'
  | 'inbox-fanout';

/**
 * Context handed to a fingerprint-gate handler. The handler decides
 * whether the operation may proceed (return `true`) or must be aborted
 * (return `false` or throw).
 */
export interface FingerprintGateContext {
  /** Peer address being acted on (own address for `backup-import`). */
  peerAddress: string;
  /** Safety number to display. 60 digits, 12 groups of 5. */
  fingerprint: string;
  /** Which gate fired. */
  gate: FingerprintGate;
  /** For `first-large-file`, the file size in bytes. */
  fileSize?: number;
}

/** Sync or async predicate. `true` allows; `false` (or throw) rejects. */
export type FingerprintGateHandler = (
  ctx: FingerprintGateContext,
) => boolean | Promise<boolean>;

/**
 * Registry that tracks gate handlers and the persisted verification state
 * for peers. Used internally by `Shade` to wrap critical operations.
 *
 * Decision tree for every gate check:
 *   1. Peer already verified (fingerprint + identityVersion match) → allow.
 *   2. Handler registered → invoke; on `true` mark verified, on `false`
 *      throw `FingerprintNotVerifiedError`.
 *   3. No handler registered → log a one-time warning, mark
 *      `tofu-after-warning`, and allow.
 *
 * Step 3 satisfies the V3.3 acceptance criterion that apps without
 * registered gates get sane defaults instead of hard-failing.
 */
export class FingerprintGateRegistry {
  private firstLargeFile: { threshold: number; handler: FingerprintGateHandler } | null = null;
  private backupImport: FingerprintGateHandler | null = null;
  private newDeviceTrust: FingerprintGateHandler | null = null;
  private inboxFanout: FingerprintGateHandler | null = null;
  private warnedPeers = new Set<string>();

  constructor(private readonly storage: StorageProvider) {}

  registerFirstLargeFile(threshold: number, handler: FingerprintGateHandler): void {
    if (!Number.isFinite(threshold) || threshold < 0) {
      throw new Error('beforeFirstLargeFile: threshold must be a non-negative number');
    }
    this.firstLargeFile = { threshold, handler };
  }

  registerBackupImport(handler: FingerprintGateHandler): void {
    this.backupImport = handler;
  }

  registerNewDeviceTrust(handler: FingerprintGateHandler): void {
    this.newDeviceTrust = handler;
  }

  registerInboxFanout(handler: FingerprintGateHandler): void {
    this.inboxFanout = handler;
  }

  /** Default first-large-file threshold: 10 MiB. */
  static readonly DEFAULT_LARGE_FILE_THRESHOLD = 10 * 1024 * 1024;

  /** Returns the configured threshold (default 10 MiB if not configured). */
  getFirstLargeFileThreshold(): number {
    return this.firstLargeFile?.threshold ?? FingerprintGateRegistry.DEFAULT_LARGE_FILE_THRESHOLD;
  }

  /**
   * Check whether the recorded verification for `address` is still valid
   * against `currentFingerprint` and the peer's current identity-version.
   */
  async isVerified(address: string, currentFingerprint: string): Promise<boolean> {
    const v = await this.storage.getPeerVerification(address);
    if (v === null) return false;
    if (v.fingerprint !== currentFingerprint) return false;
    const currentVersion = await this.storage.getPeerIdentityVersion(address);
    return v.identityVersion === currentVersion;
  }

  /**
   * Persist that `address` has been verified at `fingerprint`. Called
   * by the SDK after a handler returns `true`, or directly by the app
   * via `Shade.markPeerVerified`.
   */
  async markVerified(
    address: string,
    fingerprint: string,
    source: PeerVerificationSource = 'user',
  ): Promise<void> {
    const identityVersion = await this.storage.getPeerIdentityVersion(address);
    await this.storage.savePeerVerification({
      peerAddress: address,
      fingerprint,
      verifiedAt: Date.now(),
      verifiedBy: source,
      identityVersion,
    });
  }

  /** Removes any persisted verification for `address`. */
  async revoke(address: string): Promise<void> {
    await this.storage.removePeerVerification(address);
  }

  /**
   * Run the first-large-file gate. Returns silently when the file is
   * under threshold, when the peer is already verified, or when the
   * handler approves. Throws `FingerprintNotVerifiedError` on rejection.
   */
  async checkFirstLargeFile(
    address: string,
    fingerprint: string,
    fileSize: number,
  ): Promise<void> {
    const threshold = this.getFirstLargeFileThreshold();
    if (fileSize < threshold) return;
    await this.runGate({
      peerAddress: address,
      fingerprint,
      gate: 'first-large-file',
      fileSize,
    }, this.firstLargeFile?.handler ?? null);
  }

  /**
   * Run the backup-import gate. Always invoked regardless of any
   * configurable threshold — the spec mandates an irremovable minimum
   * gate for backup-import.
   */
  async checkBackupImport(address: string, fingerprint: string): Promise<void> {
    await this.runGate(
      { peerAddress: address, fingerprint, gate: 'backup-import' },
      this.backupImport,
    );
  }

  /** Run the new-device (post-rotation) gate. Always invoked. */
  async checkNewDeviceTrust(address: string, fingerprint: string): Promise<void> {
    await this.runGate(
      { peerAddress: address, fingerprint, gate: 'new-device-trust' },
      this.newDeviceTrust,
    );
  }

  /** Run the inbox-fanout gate (V3.6). Always invoked per recipient. */
  async checkInboxFanout(address: string, fingerprint: string): Promise<void> {
    await this.runGate(
      { peerAddress: address, fingerprint, gate: 'inbox-fanout' },
      this.inboxFanout,
    );
  }

  private async runGate(
    ctx: FingerprintGateContext,
    handler: FingerprintGateHandler | null,
  ): Promise<void> {
    if (await this.isVerified(ctx.peerAddress, ctx.fingerprint)) return;

    if (handler !== null) {
      let approved: boolean;
      try {
        approved = await handler(ctx);
      } catch (err) {
        throw new FingerprintNotVerifiedError(
          ctx.peerAddress,
          ctx.gate,
          `gate handler threw: ${(err as Error).message}`,
        );
      }
      if (!approved) {
        throw new FingerprintNotVerifiedError(ctx.peerAddress, ctx.gate);
      }
      await this.markVerified(ctx.peerAddress, ctx.fingerprint, 'user');
      return;
    }

    if (!this.warnedPeers.has(ctx.peerAddress)) {
      this.warnedPeers.add(ctx.peerAddress);
      console.warn(
        `[Shade] gate=${ctx.gate} fired for ${ctx.peerAddress} but no handler is registered — ` +
          `allowing on TOFU. Register Shade.before${gateMethodSuffix(ctx.gate)}() to require explicit verification.`,
      );
    }
    await this.markVerified(ctx.peerAddress, ctx.fingerprint, 'tofu-after-warning');
  }
}

function gateMethodSuffix(gate: FingerprintGate): string {
  switch (gate) {
    case 'first-large-file':
      return 'FirstLargeFile';
    case 'backup-import':
      return 'BackupImport';
    case 'new-device-trust':
      return 'NewDeviceTrust';
    case 'inbox-fanout':
      return 'InboxFanout';
  }
}
