import { randomUUID } from "crypto";

import { Injectable, Inject } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Logger } from "nestjs-pino";

import { CacheService } from "@/infra/cache/cache.service.js";

/**
 * Session data stored in Redis for pending login OTP verification
 */
interface LoginSessionData {
  /** Session ID (UUID) */
  id: string;
  /** Validated user ID from credential check */
  userId: string;
  /** User email for OTP delivery */
  email: string;
  /** User role for token generation */
  role: string;
  /** Timestamp when session was created */
  createdAt: string;
  /** Optional request fingerprint (IP + User-Agent hash) */
  fingerprint?: string;
}

/**
 * Login Session Service
 *
 * Manages temporary sessions for the login OTP verification flow.
 * After credentials are validated, a session is created to track the
 * pending OTP verification state.
 *
 * Security:
 * - Sessions are stored in Redis with automatic TTL (10 minutes, same as OTP)
 * - Session tokens are UUIDs (unguessable)
 * - Sessions are invalidated after successful OTP verification
 */
@Injectable()
export class LoginSessionService {
  private readonly SESSION_PREFIX = "login-session:";
  private readonly ttlSeconds: number;

  constructor(
    private readonly cache: CacheService,
    private readonly config: ConfigService,
    @Inject(Logger) private readonly logger: Logger
  ) {
    // 10 minutes TTL (same as OTP expiry)
    this.ttlSeconds = this.config.get<number>("LOGIN_SESSION_TTL", 600);
  }

  /**
   * Create a new login session after credentials are validated
   *
   * @param user - The validated user info
   * @param fingerprint - Optional request fingerprint for session binding
   * @returns Session token (UUID)
   */
  async create(
    user: { id: string; email: string; role: string },
    fingerprint?: string
  ): Promise<{ sessionToken: string; expiresAt: string }> {
    const sessionId = randomUUID();
    const createdAt = new Date().toISOString();

    const sessionData: LoginSessionData = {
      id: sessionId,
      userId: user.id,
      email: user.email.toLowerCase().trim(),
      role: user.role,
      createdAt,
      ...(fingerprint && { fingerprint }),
    };

    await this.cache.set(this.buildKey(sessionId), sessionData, this.ttlSeconds);

    const expiresAt = this.calculateExpiresAt(createdAt);

    this.logger.debug(
      { userId: user.id, email: user.email, sessionId },
      "Login session created for OTP verification"
    );

    return { sessionToken: sessionId, expiresAt };
  }

  /**
   * Get session by token
   *
   * @param sessionToken - The session token (UUID)
   * @returns Session data if found and valid, null otherwise
   */
  async get(sessionToken: string): Promise<LoginSessionData | null> {
    const sessionData = await this.cache.get<LoginSessionData>(this.buildKey(sessionToken));

    if (!sessionData) {
      this.logger.debug({ sessionToken }, "Login session not found or expired");
      return null;
    }

    return sessionData;
  }

  /**
   * Validate session and return user info for token generation
   *
   * @param sessionToken - The session token
   * @param fingerprint - Optional fingerprint to check against stored value
   * @returns User info if session is valid, null otherwise
   */
  async validate(
    sessionToken: string,
    fingerprint?: string
  ): Promise<{ userId: string; email: string; role: string } | null> {
    const session = await this.get(sessionToken);

    if (!session) {
      return null;
    }

    // Log fingerprint mismatch but don't block (user may switch devices/networks)
    if (session.fingerprint && fingerprint && session.fingerprint !== fingerprint) {
      this.logger.warn(
        { sessionToken, userId: session.userId },
        "Login session OTP verification from different context (fingerprint mismatch)"
      );
    }

    return {
      userId: session.userId,
      email: session.email,
      role: session.role,
    };
  }

  /**
   * Invalidate session after successful OTP verification
   *
   * @param sessionToken - The session token to invalidate
   */
  async invalidate(sessionToken: string): Promise<void> {
    await this.cache.del(this.buildKey(sessionToken));
    this.logger.debug({ sessionToken }, "Login session invalidated after OTP verification");
  }

  /**
   * Build Redis key for session storage
   */
  private buildKey(sessionId: string): string {
    return `${this.SESSION_PREFIX}${sessionId}`;
  }

  /**
   * Calculate session expiry time
   */
  private calculateExpiresAt(createdAt: string): string {
    const created = new Date(createdAt);
    const expires = new Date(created.getTime() + this.ttlSeconds * 1000);
    return expires.toISOString();
  }
}
