/**
 * Resume-store contract for `@shade/transfer`.
 *
 * The engine persists in-flight transfer state through this interface so a
 * crashed sender or receiver can pick up where it left off. v0.2.0 ships
 * three implementations:
 *
 *  - `MemoryResumeStore` — never persists; suitable for ephemeral
 *    transfers where resume is not needed.
 *  - `StorageBackedResumeStore` — wraps a Shade `StorageProvider`, with
 *    `deviceKey`-based AES-GCM encryption of the streamSecret at rest.
 *  - `IndexedDbResumeStore` — browser-side; built on top of the same
 *    `PersistedStreamState` shape.
 */

import type { CryptoProvider, PersistedStreamState, StorageProvider } from '@shade/core';

export interface ResumeStore {
  /** Persist or update a stream's resume record. Idempotent on streamId. */
  save(state: PersistedStreamState): Promise<void>;
  /** Look up a stream's record by streamId. Returns null when absent. */
  get(streamId: string): Promise<PersistedStreamState | null>;
  /** Remove a stream's record (e.g. on completion). */
  remove(streamId: string): Promise<void>;
  /** List all non-final records (status `'active' | 'paused'`). */
  listActive(direction?: 'send' | 'receive'): Promise<PersistedStreamState[]>;
}

/** In-memory store; useful for tests and ephemeral transfers. */
export class MemoryResumeStore implements ResumeStore {
  private map = new Map<string, PersistedStreamState>();

  async save(state: PersistedStreamState): Promise<void> {
    this.map.set(state.streamId, { ...state });
  }
  async get(streamId: string): Promise<PersistedStreamState | null> {
    const s = this.map.get(streamId);
    return s ? { ...s } : null;
  }
  async remove(streamId: string): Promise<void> {
    this.map.delete(streamId);
  }
  async listActive(direction?: 'send' | 'receive'): Promise<PersistedStreamState[]> {
    const out: PersistedStreamState[] = [];
    for (const s of this.map.values()) {
      if (s.status !== 'active' && s.status !== 'paused') continue;
      if (direction !== undefined && s.direction !== direction) continue;
      out.push({ ...s });
    }
    out.sort((a, b) => b.updatedAt - a.updatedAt);
    return out;
  }
}

/**
 * Storage-backed resume store. Delegates persistence to a `StorageProvider`
 * that implements the optional stream-state methods. Throws clearly if the
 * provider does not.
 */
export class StorageBackedResumeStore implements ResumeStore {
  constructor(private readonly storage: StorageProvider) {
    if (
      storage.saveStreamState === undefined ||
      storage.getStreamState === undefined ||
      storage.removeStreamState === undefined ||
      storage.listActiveStreamStates === undefined
    ) {
      throw new Error(
        'StorageBackedResumeStore requires a StorageProvider with stream-state methods',
      );
    }
  }
  async save(state: PersistedStreamState): Promise<void> {
    await this.storage.saveStreamState!(state);
  }
  async get(streamId: string): Promise<PersistedStreamState | null> {
    return this.storage.getStreamState!(streamId);
  }
  async remove(streamId: string): Promise<void> {
    await this.storage.removeStreamState!(streamId);
  }
  async listActive(direction?: 'send' | 'receive'): Promise<PersistedStreamState[]> {
    return this.storage.listActiveStreamStates!(direction);
  }
}

/**
 * Derive a device-specific encryption key from the local identity signing
 * key. Used to wrap the streamSecret before persisting it.
 *
 *   deviceKey = HKDF(
 *     ikm  = signingPrivateKey,
 *     salt = "shade-stream-resume/v1/device",
 *     info = "device-encryption-key",
 *     length = 32,
 *   )
 *
 * Identity rotation invalidates all existing device-keyed records (resume
 * stops working until a fresh transfer is initiated). This is intentional:
 * a stolen pre-rotation DB cannot decrypt a post-rotation in-flight stream.
 */
export async function deriveDeviceKey(
  crypto: CryptoProvider,
  signingPrivateKey: Uint8Array,
): Promise<Uint8Array> {
  const enc = new TextEncoder();
  return crypto.hkdf(
    signingPrivateKey,
    enc.encode('shade-stream-resume/v1/device'),
    enc.encode('device-encryption-key'),
    32,
  );
}

/** Encrypt a streamSecret under a deviceKey. Returns ciphertext + nonce. */
export async function wrapStreamSecret(
  crypto: CryptoProvider,
  deviceKey: Uint8Array,
  streamSecret: Uint8Array,
  streamId: string,
): Promise<{ ciphertext: Uint8Array; nonce: Uint8Array }> {
  const aad = new TextEncoder().encode(`resume:${streamId}`);
  return crypto.aesGcmEncrypt(deviceKey, streamSecret, aad);
}

/** Decrypt a streamSecret under a deviceKey. */
export async function unwrapStreamSecret(
  crypto: CryptoProvider,
  deviceKey: Uint8Array,
  ciphertext: Uint8Array,
  nonce: Uint8Array,
  streamId: string,
): Promise<Uint8Array> {
  const aad = new TextEncoder().encode(`resume:${streamId}`);
  return crypto.aesGcmDecrypt(deviceKey, ciphertext, nonce, aad);
}
