import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { EventEmitter } from 'node:events';
import { RedisService } from '../../infra/redis.service';

export type StoredTelegramUpdate = Record<string, unknown> & { update_id: number };

@Injectable()
export class BotUpdateQueueService implements OnModuleInit, OnModuleDestroy {
  private readonly logger = new Logger(BotUpdateQueueService.name);
  private readonly emitter = new EventEmitter();
  private subscriberReady = false;

  constructor(private readonly redis: RedisService) {}

  async onModuleInit() {
    try {
      const subscriber = this.redis.client.duplicate();
      await subscriber.connect();
      await subscriber.psubscribe('bot:updates:notify:*');
      subscriber.on('pmessage', (_pattern, channel) => {
        const botId = channel.replace('bot:updates:notify:', '');
        if (botId) {
          this.emitter.emit(this.waitKey(botId));
        }
      });
      this.subscriberReady = true;
    } catch (error) {
      this.logger.warn(
        `Redis pub/sub для long polling недоступен: ${error instanceof Error ? error.message : error}`
      );
    }
  }

  async onModuleDestroy() {
    this.emitter.removeAllListeners();
  }

  private listKey(botId: string) {
    return `bot:updates:${botId}`;
  }

  private waitKey(botId: string) {
    return `bot:updates:wait:${botId}`;
  }

  async enqueue(botId: string, update: StoredTelegramUpdate) {
    const serialized = JSON.stringify(update);
    await this.redis.client.rpush(this.listKey(botId), serialized);
    await this.redis.client.publish(`bot:updates:notify:${botId}`, '1');
    this.emitter.emit(this.waitKey(botId));
  }

  async pendingCount(botId: string) {
    return this.redis.client.llen(this.listKey(botId));
  }

  async clearQueue(botId: string) {
    await this.redis.client.del(this.listKey(botId));
  }

  async acknowledgeBeforeOffset(botId: string, offset: number) {
    if (!offset || offset <= 0) return;
    const key = this.listKey(botId);
    const items = await this.redis.client.lrange(key, 0, -1);
    if (!items.length) return;

    const remaining = items.filter((item) => {
      try {
        const parsed = JSON.parse(item) as StoredTelegramUpdate;
        return parsed.update_id >= offset;
      } catch {
        return true;
      }
    });

    const pipeline = this.redis.client.pipeline();
    pipeline.del(key);
    if (remaining.length) {
      pipeline.rpush(key, ...remaining);
    }
    await pipeline.exec();
  }

  async fetchUpdates(botId: string, offset: number, limit: number) {
    const safeLimit = Math.min(Math.max(limit, 1), 100);
    const items = await this.redis.client.lrange(this.listKey(botId), 0, -1);
    const parsed = items
      .map((item) => {
        try {
          return JSON.parse(item) as StoredTelegramUpdate;
        } catch {
          return null;
        }
      })
      .filter((item): item is StoredTelegramUpdate => Boolean(item))
      .filter((item) => item.update_id >= offset)
      .sort((left, right) => left.update_id - right.update_id)
      .slice(0, safeLimit);

    return parsed;
  }

  async getUpdates(botId: string, offset: number, limit: number, timeoutSeconds: number) {
    const safeTimeout = Math.min(Math.max(timeoutSeconds, 0), 50);
    await this.acknowledgeBeforeOffset(botId, offset);

    const immediate = await this.fetchUpdates(botId, offset, limit);
    if (immediate.length || safeTimeout <= 0) {
      return immediate;
    }

    return new Promise<StoredTelegramUpdate[]>((resolve) => {
      let settled = false;
      const finish = async () => {
        if (settled) return;
        settled = true;
        clearTimeout(timer);
        this.emitter.off(this.waitKey(botId), onWake);
        resolve(await this.fetchUpdates(botId, offset, limit));
      };

      const onWake = () => {
        void finish();
      };

      const timer = setTimeout(() => {
        void finish();
      }, safeTimeout * 1000);

      this.emitter.once(this.waitKey(botId), onWake);

      if (!this.subscriberReady) {
        void this.pollUntilUpdates(botId, offset, safeTimeout * 1000).then(() => finish());
      }
    });
  }

  private async pollUntilUpdates(botId: string, offset: number, maxWaitMs: number) {
    const started = Date.now();
    while (Date.now() - started < maxWaitMs) {
      const updates = await this.fetchUpdates(botId, offset, 1);
      if (updates.length) return;
      await new Promise((resolve) => setTimeout(resolve, 500));
    }
  }
}
