import type { ShadeSessionManager } from './session.js';
import type { ShadeEnvelope } from './types.js';

/**
 * Multi-device fan-out (simplified Sesame).
 *
 * In the Signal protocol, "Sesame" tracks per-device sessions under a
 * single user identity. A message to "bob" is fan-out encrypted to each
 * of Bob's active devices using an independent 1:1 Double Ratchet per
 * device. When Bob adds a new device, it needs to be added to the device
 * list and the session is established with that device via a fresh X3DH.
 *
 * We keep it simple: addresses are formatted as `user:deviceId` and the
 * existing ShadeSessionManager handles each device's session independently.
 * This module adds:
 *   - DeviceList tracking per user
 *   - sendToUser(user, plaintext) → fans out to all known devices
 *   - receive-side handling that updates the device list automatically
 *     when a message arrives from a new device
 *
 * The transport layer (HTTP, WebSocket, push) still owns delivery per
 * device. Sesame just handles the key-management side: "which devices
 * belong to this user, and do I have a session with each?"
 */

export interface DeviceList {
  /** Each device is addressed as "user:deviceId" */
  devices: Set<string>;
  /** Last time this list was refreshed (for staleness checks) */
  lastUpdated: number;
}

export class DeviceListManager {
  private lists = new Map<string, DeviceList>();

  /** Get all known devices for a user */
  getDevices(user: string): string[] {
    return Array.from(this.lists.get(user)?.devices ?? []);
  }

  /** Add a device address to a user's list */
  addDevice(user: string, deviceAddress: string): void {
    let list = this.lists.get(user);
    if (!list) {
      list = { devices: new Set(), lastUpdated: Date.now() };
      this.lists.set(user, list);
    }
    list.devices.add(deviceAddress);
    list.lastUpdated = Date.now();
  }

  /** Remove a device from a user's list */
  removeDevice(user: string, deviceAddress: string): void {
    const list = this.lists.get(user);
    if (list) {
      list.devices.delete(deviceAddress);
      list.lastUpdated = Date.now();
    }
  }

  /** Replace the full device list for a user (e.g. from server query) */
  setDevices(user: string, deviceAddresses: string[]): void {
    this.lists.set(user, {
      devices: new Set(deviceAddresses),
      lastUpdated: Date.now(),
    });
  }

  /** Serialize for persistence */
  toJSON(): Record<string, { devices: string[]; lastUpdated: number }> {
    const out: Record<string, { devices: string[]; lastUpdated: number }> = {};
    for (const [user, list] of this.lists) {
      out[user] = {
        devices: Array.from(list.devices),
        lastUpdated: list.lastUpdated,
      };
    }
    return out;
  }

  /** Restore from serialized form */
  fromJSON(data: Record<string, { devices: string[]; lastUpdated: number }>): void {
    this.lists.clear();
    for (const [user, entry] of Object.entries(data)) {
      this.lists.set(user, {
        devices: new Set(entry.devices),
        lastUpdated: entry.lastUpdated,
      });
    }
  }
}

/**
 * Fan-out a message to every device belonging to a user.
 *
 * Returns an array of (deviceAddress, envelope) pairs that the caller
 * should deliver individually via their transport.
 */
export async function fanOutEncrypt(
  manager: ShadeSessionManager,
  deviceList: DeviceListManager,
  user: string,
  plaintext: string,
): Promise<Array<{ deviceAddress: string; envelope: ShadeEnvelope }>> {
  const devices = deviceList.getDevices(user);
  if (devices.length === 0) {
    throw new Error(`No known devices for user: ${user}`);
  }

  const results: Array<{ deviceAddress: string; envelope: ShadeEnvelope }> = [];
  for (const deviceAddress of devices) {
    const envelope = await manager.encrypt(deviceAddress, plaintext);
    results.push({ deviceAddress, envelope });
  }
  return results;
}

/**
 * Parse a device address (format: "user:deviceId") into its user part.
 */
export function userOfDevice(deviceAddress: string): string {
  const colonIdx = deviceAddress.indexOf(':');
  if (colonIdx < 0) return deviceAddress;
  return deviceAddress.substring(0, colonIdx);
}

/**
 * Parse a device address into its device-ID part.
 */
export function deviceIdOf(deviceAddress: string): string {
  const colonIdx = deviceAddress.indexOf(':');
  if (colonIdx < 0) return '';
  return deviceAddress.substring(colonIdx + 1);
}

/**
 * Observe an incoming message and auto-register the sending device.
 * Call this after decrypting a message from a peer — if the sender is
 * a new device for a known user, add it to the device list.
 */
export function observeIncoming(
  deviceList: DeviceListManager,
  senderDeviceAddress: string,
): void {
  const user = userOfDevice(senderDeviceAddress);
  deviceList.addDevice(user, senderDeviceAddress);
}
