import { getRedis } from '../config/redis';

/** Keep busy cache longer than the 10-minute scheduler interval */
const BUSY_TTL = 1800;
const REDIS_OP_TIMEOUT_MS = 2000;
const REDIS_BACKOFF_MS = 60_000;

let redisUnavailableUntil = 0;

export class BusyCacheService {
  private keyPrefix(userId: string, date: string): string {
    return `busy:${userId}:${date}`;
  }

  async getBusyBlocks(userId: string, from: string, to: string): Promise<Array<{ start: Date; end: Date }>> {
    const dates = this.datesBetween(from, to);
    const blocks: Array<{ start: Date; end: Date }> = [];

    for (const date of dates) {
      const raw = await this.redisGet(this.keyPrefix(userId, date));
      if (raw) {
        const parsed = JSON.parse(raw) as Array<{ start: string; end: string }>;
        blocks.push(...parsed.map((b) => ({ start: new Date(b.start), end: new Date(b.end) })));
      }
    }

    return blocks;
  }

  async setBusyBlocks(
    userId: string,
    date: string,
    blocks: Array<{ start: Date; end: Date }>,
  ): Promise<void> {
    const payload = blocks.map((b) => ({
      start: b.start.toISOString(),
      end: b.end.toISOString(),
    }));
    await this.redisSetex(this.keyPrefix(userId, date), BUSY_TTL, JSON.stringify(payload));
  }

  async invalidateUser(userId: string): Promise<void> {
    const keys = await this.redisKeys(`busy:${userId}:*`);
    if (keys.length > 0) {
      await this.redisDel(...keys);
    }
  }

  private async redisGet(key: string): Promise<string | null> {
    return this.withFallback(
      () => getRedis().get(key),
      null,
      'get',
    );
  }

  private async redisSetex(key: string, ttl: number, value: string): Promise<void> {
    await this.withFallback(
      () => getRedis().setex(key, ttl, value).then(() => undefined),
      undefined,
      'setex',
    );
  }

  private async redisKeys(pattern: string): Promise<string[]> {
    return this.withFallback(
      () => getRedis().keys(pattern),
      [],
      'keys',
    );
  }

  private async redisDel(...keys: string[]): Promise<void> {
    await this.withFallback(
      () => getRedis().del(...keys).then(() => undefined),
      undefined,
      'del',
    );
  }

  private async withFallback<T>(
    op: () => Promise<T>,
    fallback: T,
    label: string,
  ): Promise<T> {
    if (Date.now() < redisUnavailableUntil) {
      return fallback;
    }

    try {
      return await Promise.race([
        op(),
        new Promise<T>((_, reject) =>
          setTimeout(() => reject(new Error(`Redis ${label} timed out`)), REDIS_OP_TIMEOUT_MS),
        ),
      ]);
    } catch (err) {
      redisUnavailableUntil = Date.now() + REDIS_BACKOFF_MS;
      const message = err instanceof Error ? err.message : String(err);
      console.warn(`[BusyCache] ${label} skipped (${message}); continuing without cache`);
      return fallback;
    }
  }

  private datesBetween(from: string, to: string): string[] {
    const dates: string[] = [];
    const start = new Date(from);
    const end = new Date(to);
    const cur = new Date(start);
    while (cur <= end) {
      dates.push(cur.toISOString().slice(0, 10));
      cur.setDate(cur.getDate() + 1);
    }
    return dates;
  }
}
