import type { Readable } from 'node:stream';
import type { Redis } from 'ioredis';

/**
 * `connect-redis` v9 was rewritten for `node-redis` v4 and dropped
 * direct support for `ioredis`: it calls `set(key, val, {expiration:
 * {type: 'EX', value}})`, `mGet([keys])`, `del([keys])`, and
 * `scanIterator({MATCH, COUNT})` — all of which are node-redis
 * shapes. ADR-0010 selected `ioredis` for the project's Redis client
 * (Sentinel support, single shared client across BFF features); we
 * keep `ioredis` as primary and shim it for `connect-redis` here, so
 * the impedance mismatch lives in one file instead of leaking into
 * the rest of the codebase.
 *
 * Alternative considered: switch the whole BFF to `node-redis`. Both
 * libraries are battle-tested and Sentinel-capable, but the swap
 * would touch `RedisModule`, every future Redis consumer (OBO token
 * cache, audit indices, etc.), and require an ADR amendment. The
 * adapter is the lower-blast-radius choice — easy to delete later if
 * we ever decide to align on `node-redis`.
 *
 * Surface covered: only the commands `connect-redis` actually calls
 * (read source at `node_modules/connect-redis/dist/connect-redis.js`
 * for the authoritative list). Anything else is intentionally
 * absent — `connect-redis` doesn't reach for it.
 */

interface SetOptions {
  expiration?: { type: 'EX'; value: number };
}

export interface NodeRedisLikeClient {
  get(key: string): Promise<string | null>;
  set(key: string, value: string, options?: SetOptions): Promise<unknown>;
  expire(key: string, seconds: number): Promise<unknown>;
  del(keys: string[]): Promise<unknown>;
  mGet(keys: string[]): Promise<(string | null)[]>;
  scanIterator(options: { MATCH: string; COUNT: number }): AsyncIterable<string[]>;
}

export function adaptIoredisForConnectRedis(client: Redis): NodeRedisLikeClient {
  return {
    get: (key) => client.get(key),

    set: (key, value, options) => {
      const ttl = options?.expiration?.value;
      if (ttl !== undefined) {
        return client.set(key, value, 'EX', ttl);
      }
      return client.set(key, value);
    },

    expire: (key, seconds) => client.expire(key, seconds),

    // `ioredis`'s `del` takes varargs (`del(...keys)`); calling it
    // with an empty list issues a no-op `DEL` and would error on
    // some servers. Short-circuit instead.
    del: (keys) => (keys.length === 0 ? Promise.resolve(0) : client.del(...keys)),

    mGet: (keys) => (keys.length === 0 ? Promise.resolve([]) : client.mget(...keys)),

    // `connect-redis` consumes `scanIterator` with `for await`. The
    // node-redis iterator yields arrays of keys; `ioredis`'s
    // `scanStream` is a Node `Readable` that emits the same shape.
    scanIterator: ({ MATCH, COUNT }) =>
      streamToAsyncIterable<string[]>(client.scanStream({ match: MATCH, count: COUNT })),
  };
}

async function* streamToAsyncIterable<T>(stream: Readable): AsyncGenerator<T> {
  for await (const chunk of stream) {
    yield chunk as T;
  }
}
