import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import Redis from 'ioredis';
import { isRedisEnabled, resolveRedisUrl } from '../config/runtime-config';

export const LIVE_LOCATION_PREFIX = 'live:location:';
export const LIVE_TRIP_PREFIX = 'live:trip:';
export const EMPLOYEE_PRESENCE_PREFIX = 'presence:';

interface MemoryEntry {
  value: string;
  expiresAt?: number;
}

@Injectable()
export class RedisService implements OnModuleDestroy {
  private readonly logger = new Logger(RedisService.name);
  private readonly memory = new Map<string, MemoryEntry>();
  readonly client: Redis | null;
  readonly enabled: boolean;

  constructor(cfg: ConfigService) {
    this.enabled = isRedisEnabled(cfg);
    if (!this.enabled) {
      this.client = null;
      this.logger.warn(
        'Redis disabled — using in-memory store (fine for local dev; enable REDIS_ENABLED=true in production)',
      );
      return;
    }

    this.client = new Redis(resolveRedisUrl(cfg), {
      maxRetriesPerRequest: null,
      enableReadyCheck: true,
      lazyConnect: true,
      retryStrategy: (times) => (times > 3 ? null : Math.min(times * 500, 2000)),
    });

    this.client.on('error', (err) => {
      this.logger.warn(`Redis unavailable (${err.message}) — falling back to in-memory store`);
    });
  }

  async onModuleDestroy() {
    if (this.client?.status === 'ready') {
      await this.client.quit();
    }
  }

  private async useClient<T>(fn: (client: Redis) => Promise<T>): Promise<T | null> {
    if (!this.client) return null;
    try {
      if (this.client.status !== 'ready') {
        await this.client.connect();
      }
      return await fn(this.client);
    } catch {
      return null;
    }
  }

  private memoryGet(key: string): string | null {
    const entry = this.memory.get(key);
    if (!entry) return null;
    if (entry.expiresAt && entry.expiresAt < Date.now()) {
      this.memory.delete(key);
      return null;
    }
    return entry.value;
  }

  private memorySet(key: string, value: string, ttlSeconds?: number) {
    this.memory.set(key, {
      value,
      expiresAt: ttlSeconds ? Date.now() + ttlSeconds * 1000 : undefined,
    });
  }

  async setJson<T>(key: string, value: T, ttlSeconds?: number): Promise<void> {
    const payload = JSON.stringify(value);
    const viaRedis = await this.useClient(async (client) => {
      if (ttlSeconds) {
        await client.setex(key, ttlSeconds, payload);
      } else {
        await client.set(key, payload);
      }
      return true;
    });
    if (!viaRedis) {
      this.memorySet(key, payload, ttlSeconds);
    }
  }

  async getJson<T>(key: string): Promise<T | null> {
    const viaRedis = await this.useClient(async (client) => client.get(key));
    const raw = viaRedis ?? this.memoryGet(key);
    if (!raw) return null;
    return JSON.parse(raw) as T;
  }

  async setLiveLocation(
    userId: string,
    data: Record<string, unknown>,
    ttlSeconds = 300,
  ) {
    await this.setJson(`${LIVE_LOCATION_PREFIX}${userId}`, data, ttlSeconds);
  }

  async getLiveLocation(userId: string) {
    return this.getJson<Record<string, unknown>>(
      `${LIVE_LOCATION_PREFIX}${userId}`,
    );
  }

  async getAllLiveLocations(): Promise<
    Array<{ userId: string; data: Record<string, unknown> }>
  > {
    const viaRedis = await this.useClient(async (client) => {
      const keys = await client.keys(`${LIVE_LOCATION_PREFIX}*`);
      if (!keys.length) return [];
      const values = await client.mget(...keys);
      return keys.map((key, i) => ({
        userId: key.replace(LIVE_LOCATION_PREFIX, ''),
        data: values[i]
          ? (JSON.parse(values[i]!) as Record<string, unknown>)
          : {},
      }));
    });
    if (viaRedis) return viaRedis;

    const prefix = LIVE_LOCATION_PREFIX;
    return [...this.memory.entries()]
      .filter(([key]) => key.startsWith(prefix))
      .map(([key, entry]) => ({
        userId: key.replace(prefix, ''),
        data: entry.value
          ? (JSON.parse(entry.value) as Record<string, unknown>)
          : {},
      }));
  }
}
