import { createClient } from 'redis';
import { config } from './config.js';

class InMemoryRedisClient {
  constructor() {
    this.mode = 'memory';
    this._strings = new Map();
    this._sortedSets = new Map();
  }

  _now() {
    return Date.now();
  }

  _purgeExpiredStrings() {
    const now = this._now();
    for (const [key, entry] of this._strings.entries()) {
      if (entry.expiresAt !== null && entry.expiresAt <= now) {
        this._strings.delete(key);
      }
    }
  }

  _getStringEntry(key) {
    this._purgeExpiredStrings();
    return this._strings.get(key) ?? null;
  }

  _getSortedSet(key) {
    let set = this._sortedSets.get(key);
    if (!set) {
      set = new Map();
      this._sortedSets.set(key, set);
    }
    return set;
  }

  async connect() {
    return this;
  }

  on() {
    return this;
  }

  async zAdd(key, entries) {
    const set = this._getSortedSet(key);
    for (const entry of entries) {
      set.set(String(entry.value), Number(entry.score));
    }
  }

  async zCard(key) {
    return this._getSortedSet(key).size;
  }

  async zRem(key, ...values) {
    const set = this._getSortedSet(key);
    let removed = 0;
    for (const value of values) {
      if (set.delete(String(value))) {
        removed += 1;
      }
    }
    return removed;
  }

  async zRange(key, start, stop) {
    const items = Array.from(this._getSortedSet(key).entries())
      .sort((left, right) => left[1] - right[1] || left[0].localeCompare(right[0]))
      .map(([value]) => value);

    const normalizedStop = stop < 0 ? items.length + stop : stop;
    return items.slice(start, normalizedStop + 1);
  }

  async set(key, value, options = {}) {
    const existing = this._getStringEntry(key);
    if (options.NX && existing) {
      return null;
    }

    let expiresAt = null;
    if (typeof options.PX === 'number') {
      expiresAt = this._now() + options.PX;
    } else if (typeof options.EX === 'number') {
      expiresAt = this._now() + (options.EX * 1000);
    }

    this._strings.set(key, { value: String(value), expiresAt });
    return 'OK';
  }

  async get(key) {
    const entry = this._getStringEntry(key);
    return entry ? entry.value : null;
  }

  async del(key) {
    this._purgeExpiredStrings();
    const existed = this._strings.delete(key);
    return existed ? 1 : 0;
  }
}

export async function createRedis() {
  if (!config.redisUrl) {
    console.warn('[redis] REDIS_URL not set, using in-memory fallback');
    return new InMemoryRedisClient();
  }
  const client = createClient({
    url: config.redisUrl,
    socket: {
      connectTimeout: 3000,
      reconnectStrategy: false,
    },
  });
  client.on('error', (err) => {
    console.error('[redis] error', err);
  });

  try {
    await client.connect();
    client.mode = 'redis';
    console.log('[redis] connected');
    return client;
  } catch (error) {
    try {
      if (typeof client.disconnect === 'function') {
        await client.disconnect();
      } else if (typeof client.quit === 'function') {
        await client.quit();
      }
    } catch {
      // Ignore cleanup errors and continue with in-memory fallback.
    }
    console.warn('[redis] unavailable, using in-memory fallback:', error instanceof Error ? error.message : String(error));
    return new InMemoryRedisClient();
  }
}

export function key(...parts) {
  return config.redisKeyPrefix + parts.join('');
}
