import Redis, { type RedisOptions } from "ioredis";
import { defaultReconnectOnError } from "@internal/redis";
import { env } from "~/env.server";
import { subDays } from "date-fns";

const DEV_RECENT_DEBOUNCE_SEC = 60;
const DEV_RECENT_TTL = 7 * 24 * 60 * 60; // 7 days
const RECENCY_DAYS = 3;

export class DevPresence {
  private redis: Redis;

  constructor(options: RedisOptions) {
    this.redis = new Redis({ reconnectOnError: defaultReconnectOnError, ...options });
  }

  async isConnected(environmentId: string) {
    const presenceKey = this.getPresenceKey(environmentId);
    const presenceValue = await this.redis.get(presenceKey);
    return !!presenceValue;
  }

  async isConnectedMany(environmentIds: string[]): Promise<Map<string, boolean>> {
    if (environmentIds.length === 0) return new Map();
    const keys = environmentIds.map((id) => this.getPresenceKey(id));
    const values = await this.redis.mget(keys);
    return new Map(environmentIds.map((id, i) => [id, !!values[i]]));
  }

  async setConnected({
    userId,
    projectId,
    environmentId,
    ttl,
  }: {
    userId: string;
    projectId: string;
    environmentId: string;
    ttl: number;
  }) {
    const presenceKey = this.getPresenceKey(environmentId);
    await this.redis.setex(presenceKey, ttl, new Date().toISOString());

    const touchKey = this.getTouchKey(environmentId);
    const acquired = await this.redis.set(touchKey, "1", "EX", DEV_RECENT_DEBOUNCE_SEC, "NX");

    if (acquired !== null) {
      const recentKey = this.getRecentKey(userId, projectId);
      const now = new Date();
      const threeDaysAgo = subDays(now, RECENCY_DAYS);
      await this.redis
        .multi()
        .zadd(recentKey, now.getTime(), environmentId)
        .zremrangebyscore(recentKey, 0, threeDaysAgo.getTime())
        .zremrangebyrank(recentKey, 0, -51)
        .expire(recentKey, DEV_RECENT_TTL)
        .exec();
    }
  }

  async getRecentBranchIds(userId: string, projectId: string) {
    const recentKey = this.getRecentKey(userId, projectId);
    const threeDaysAgo = subDays(Date.now(), RECENCY_DAYS);
    const raw = await this.redis.zrevrangebyscore(
      recentKey,
      "+inf",
      threeDaysAgo.getTime(),
      "WITHSCORES"
    );

    const branches = new Map<string, Date>();
    for (let i = 0; i < raw.length; i += 2) {
      branches.set(raw[i], new Date(Number(raw[i + 1])));
    }
    return branches;
  }

  private getPresenceKey(environmentId: string) {
    return `dev-presence:connection:${environmentId}`;
  }

  private getRecentKey(userId: string, projectId: string) {
    return `dev-recent:${userId}:${projectId}`;
  }

  private getTouchKey(environmentId: string) {
    return `dev-recent-touch:${environmentId}`;
  }
}

export const devPresence = new DevPresence({
  port: env.RUN_ENGINE_DEV_PRESENCE_REDIS_PORT ?? undefined,
  host: env.RUN_ENGINE_DEV_PRESENCE_REDIS_HOST ?? undefined,
  username: env.RUN_ENGINE_DEV_PRESENCE_REDIS_USERNAME ?? undefined,
  password: env.RUN_ENGINE_DEV_PRESENCE_REDIS_PASSWORD ?? undefined,
  enableAutoPipelining: true,
  ...(env.RUN_ENGINE_DEV_PRESENCE_REDIS_TLS_DISABLED === "true" ? {} : { tls: {} }),
});
