import { randomBytes } from 'crypto';

export interface PairingConfig {
  enabled: boolean;
  codeTtl: number;      // milliseconds
  codeLength: number;    // number of characters
}

interface PendingCode {
  code: string;
  createdAt: number;
  expiresAt: number;
  /** Optional label for the code (e.g. "for alice"). */
  label?: string;
}

export interface ApprovedSender {
  channel: string;
  senderId: string;
  approvedAt: number;
  /** The code that was used. */
  codeUsed: string;
}

export interface PairingStore {
  loadApproved(): ApprovedSender[];
  saveApproved(sender: ApprovedSender): void;
  removeApproved(channel: string, senderId: string): void;
}

/**
 * Manages DM pairing codes for authenticating unknown senders.
 *
 * Flow:
 * 1. Admin generates a pairing code via gateway API or TUI command.
 * 2. Unknown sender DMs the bot with the code as their first message.
 * 3. If the code is valid and not expired, the sender is approved.
 * 4. Approved senders bypass the allowlist check for subsequent messages.
 */
export class PairingManager {
  private config: PairingConfig;
  private pendingCodes: Map<string, PendingCode> = new Map();
  private approvedSenders: Map<string, ApprovedSender> = new Map();
  private store?: PairingStore;

  constructor(config: PairingConfig, store?: PairingStore) {
    this.config = config;
    this.store = store;
    if (store) {
      for (const sender of store.loadApproved()) {
        const key = `${sender.channel}:${sender.senderId}`;
        this.approvedSenders.set(key, sender);
      }
    }
  }

  /** Generate a new pairing code. Returns the code string. */
  generateCode(label?: string): string {
    this.cleanup();
    const code = randomBytes(Math.ceil(this.config.codeLength / 2))
      .toString('hex')
      .slice(0, this.config.codeLength)
      .toUpperCase();

    const now = Date.now();
    this.pendingCodes.set(code, {
      code,
      createdAt: now,
      expiresAt: now + this.config.codeTtl,
      label,
    });

    return code;
  }

  /**
   * Validate a code for a given channel+sender.
   * If valid, adds the sender to the approved list and removes the code.
   * Returns true if the code was valid.
   */
  validateCode(channel: string, senderId: string, code: string): boolean {
    this.cleanup();
    const normalizedCode = code.trim().toUpperCase();
    const pending = this.pendingCodes.get(normalizedCode);

    if (!pending) {return false;}
    if (Date.now() > pending.expiresAt) {
      this.pendingCodes.delete(normalizedCode);
      return false;
    }

    // Code is valid — approve the sender
    const key = `${channel}:${senderId}`;
    const approved: ApprovedSender = {
      channel,
      senderId,
      approvedAt: Date.now(),
      codeUsed: normalizedCode,
    };
    this.approvedSenders.set(key, approved);
    this.store?.saveApproved(approved);

    // Remove the used code
    this.pendingCodes.delete(normalizedCode);
    return true;
  }

  /** Check if a sender is already approved. */
  isApproved(channel: string, senderId: string): boolean {
    const key = `${channel}:${senderId}`;
    return this.approvedSenders.has(key);
  }

  /** Revoke approval for a sender. Returns true if the sender was found and removed. */
  revokeApproval(channel: string, senderId: string): boolean {
    const key = `${channel}:${senderId}`;
    const deleted = this.approvedSenders.delete(key);
    if (deleted) {
      this.store?.removeApproved(channel, senderId);
    }
    return deleted;
  }

  /** List all currently approved senders. */
  listApproved(): ApprovedSender[] {
    return Array.from(this.approvedSenders.values());
  }

  /** List all pending (non-expired) codes. */
  listPendingCodes(): Array<{ code: string; expiresAt: number; label?: string }> {
    this.cleanup();
    return Array.from(this.pendingCodes.values()).map(p => ({
      code: p.code,
      expiresAt: p.expiresAt,
      label: p.label,
    }));
  }

  /** Remove expired codes. */
  cleanup(): void {
    const now = Date.now();
    for (const [code, pending] of this.pendingCodes) {
      if (now > pending.expiresAt) {
        this.pendingCodes.delete(code);
      }
    }
  }

  /** Whether pairing is enabled. */
  get enabled(): boolean {
    return this.config.enabled;
  }
}
