import { Logger, type OnModuleDestroy } from '@nestjs/common';
import { Redis } from 'ioredis';
import type { PresencePort } from './presence.port';

/**
 * Redis-backed presence for multi-replica prod: a socket connected to replica A and a message
 * projected on replica B must share the same "who is viewing what" view, which an in-memory Map
 * cannot provide. Wired (in place of {@link PresenceService}) only when REDIS_URL is set.
 *
 * Layout (per user, tiny):
 *   sock:{socketId}  -> userId            (so clearSocket can find the user), EX TTL
 *   user:{userId}    -> HASH socketId=threadId, EX TTL
 * isViewing = does the user hash hold this threadId for any socket. Keys carry a TTL so a crashed
 * replica's entries self-heal; expiry errs toward "not viewing" (push is sent), the safe default.
 */
export class RedisPresenceService implements PresencePort, OnModuleDestroy {
  private readonly logger = new Logger(RedisPresenceService.name);
  private readonly redis: Redis;
  private static readonly TTL_SECONDS = 300;
  private static readonly PREFIX = 'iios:presence';

  constructor(url: string, client?: Redis) {
    this.redis = client ?? new Redis(url, { maxRetriesPerRequest: null });
    this.redis.on('error', (err) => this.logger.error(`redis presence error: ${err.message}`));
  }

  private sockKey(socketId: string): string {
    return `${RedisPresenceService.PREFIX}:sock:${socketId}`;
  }
  private userKey(userId: string): string {
    return `${RedisPresenceService.PREFIX}:user:${userId}`;
  }

  async setFocus(socketId: string, userId: string, threadId: string | null): Promise<void> {
    const ttl = RedisPresenceService.TTL_SECONDS;
    const sock = this.sockKey(socketId);
    const user = this.userKey(userId);
    const pipe = this.redis.multi().set(sock, userId, 'EX', ttl);
    if (threadId === null) {
      // Blurred / no thread open — the socket stays connected but is viewing nothing.
      pipe.hdel(user, socketId);
    } else {
      pipe.hset(user, socketId, threadId).expire(user, ttl);
    }
    await pipe.exec();
  }

  async clearSocket(socketId: string): Promise<void> {
    const sock = this.sockKey(socketId);
    const userId = await this.redis.get(sock);
    const pipe = this.redis.multi().del(sock);
    if (userId) pipe.hdel(this.userKey(userId), socketId);
    await pipe.exec();
  }

  async isViewing(userId: string, threadId: string): Promise<boolean> {
    const threads = await this.redis.hvals(this.userKey(userId));
    return threads.includes(threadId);
  }

  async onModuleDestroy(): Promise<void> {
    await this.redis.quit().catch(() => undefined);
  }
}
