import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import Redis from 'ioredis';

/**
 * Session state tracked in Redis for distributed processing.
 * Enables horizontal scaling across multiple processor instances.
 */
export interface SessionState {
  sessionId: string;
  userId?: string | null;
  firstEventAt: Date;
  lastEventAt: Date;
  pageViews: number;
  totalEvents: number;
  hasConversion: boolean;
  isNew: boolean;
  trafficSource?: string;
  deviceType?: string;
  country?: string;
  browser?: string;
  os?: string;
}

/**
 * Redis-based session state management for analytics processor.
 * Provides distributed session tracking with automatic TTL.
 */
@Injectable()
export class RedisSessionService implements OnModuleInit, OnModuleDestroy {
  private readonly logger = new Logger(RedisSessionService.name);
  private redis: Redis;

  private readonly SESSION_TTL_SECONDS = 30 * 60; // 30 minutes
  private readonly SESSION_KEY_PREFIX = 'analytics:session:';
  private readonly SEEN_USERS_KEY = 'analytics:seen_users';

  constructor(private readonly config: ConfigService) {}

  async onModuleInit() {
    const host = this.config.get('REDIS_HOST', 'localhost');
    const port = this.config.get('REDIS_PORT', 6379);
    const password = this.config.get<string>('REDIS_PASSWORD');

    this.redis = new Redis({
      host,
      port,
      ...(password ? { password } : {}),
      retryStrategy: (times) => {
        const delay = Math.min(times * 50, 2000);
        return delay;
      },
      maxRetriesPerRequest: 3,
    });

    this.redis.on('connect', () => {
      this.logger.log(`Connected to Redis at ${host}:${port}`);
    });

    this.redis.on('error', (error) => {
      this.logger.error(`Redis connection error: ${error.message}`);
    });

    // Wait for connection
    await new Promise<void>((resolve, reject) => {
      const timeout = setTimeout(() => {
        reject(new Error('Redis connection timeout'));
      }, 5000);

      this.redis.once('connect', () => {
        clearTimeout(timeout);
        resolve();
      });

      this.redis.once('error', (error) => {
        clearTimeout(timeout);
        reject(error);
      });
    });
  }

  async onModuleDestroy() {
    await this.redis.quit();
  }

  /**
   * Get session state from Redis.
   * Returns null if session doesn't exist.
   */
  async getSession(sessionId: string): Promise<SessionState | null> {
    try {
      const key = this.getSessionKey(sessionId);
      const data = await this.redis.get(key);

      if (!data) {
        return null;
      }

      const parsed = JSON.parse(data);
      return {
        ...parsed,
        firstEventAt: new Date(parsed.firstEventAt),
        lastEventAt: new Date(parsed.lastEventAt),
      };
    } catch (error) {
      this.logger.error(`Failed to get session ${sessionId}: ${error}`);
      throw error;
    }
  }

  /**
   * Set session state in Redis with TTL.
   * Automatically expires after 30 minutes of inactivity.
   */
  async setSession(sessionId: string, state: SessionState): Promise<void> {
    try {
      const key = this.getSessionKey(sessionId);
      const data = JSON.stringify({
        ...state,
        firstEventAt: state.firstEventAt.toISOString(),
        lastEventAt: state.lastEventAt.toISOString(),
      });

      await this.redis.setex(key, this.SESSION_TTL_SECONDS, data);
    } catch (error) {
      this.logger.error(`Failed to set session ${sessionId}: ${error}`);
      throw error;
    }
  }

  /**
   * Delete session state from Redis.
   */
  async deleteSession(sessionId: string): Promise<void> {
    try {
      const key = this.getSessionKey(sessionId);
      await this.redis.del(key);
    } catch (error) {
      this.logger.error(`Failed to delete session ${sessionId}: ${error}`);
      throw error;
    }
  }

  /**
   * Check if user has been seen before.
   * Uses Redis Set for efficient membership testing.
   */
  async hasSeenUser(userId: string): Promise<boolean> {
    try {
      const result = await this.redis.sismember(this.SEEN_USERS_KEY, userId);
      return result === 1;
    } catch (error) {
      this.logger.error(`Failed to check seen user ${userId}: ${error}`);
      throw error;
    }
  }

  /**
   * Mark user as seen.
   * Uses Redis Set for persistent tracking across restarts.
   */
  async markUserSeen(userId: string): Promise<void> {
    try {
      await this.redis.sadd(this.SEEN_USERS_KEY, userId);
    } catch (error) {
      this.logger.error(`Failed to mark user seen ${userId}: ${error}`);
      throw error;
    }
  }

  /**
   * Get Redis client for advanced operations.
   * Use sparingly - prefer dedicated methods.
   */
  getClient(): Redis {
    return this.redis;
  }

  private getSessionKey(sessionId: string): string {
    return `${this.SESSION_KEY_PREFIX}${sessionId}`;
  }
}
