import { randomUUID } from "crypto";

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

import { CacheService } from "@/infra/cache/cache.service.js";
import { JoseJwtService } from "./jose-jwt.service.js";

/**
 * Data stored in Redis for password reset token tracking
 */
interface PasswordResetTokenData {
  /** User ID the token belongs to */
  userId: string;
  /** Timestamp when the token was created */
  createdAt: number;
}

/**
 * JWT payload structure for password reset tokens
 * Extends JWTPayload to satisfy jose library constraints
 */
interface PasswordResetTokenPayload {
  /** User ID (standard JWT subject) */
  sub: string;
  /** Unique token identifier for single-use tracking */
  tokenId: string;
  /** Purpose claim to distinguish from other token types */
  purpose: "password_reset";
  /** Index signature for JWTPayload compatibility */
  [key: string]: unknown;
}

/**
 * Password Reset Token Service
 *
 * Manages password reset tokens with single-use enforcement.
 * Tokens are JWTs that contain a unique tokenId, which is tracked in Redis.
 * Once a token is consumed (used), it's removed from Redis and cannot be reused.
 *
 * Security features:
 * - Single-use enforcement via Redis tracking
 * - 15-minute expiration (configurable)
 * - Purpose claim prevents token type confusion
 * - Token ID in JWT enables revocation without database
 */
@Injectable()
export class PasswordResetTokenService {
  private readonly REDIS_PREFIX = "pwd-reset:";
  private readonly DEFAULT_TTL_SECONDS = 900; // 15 minutes
  private readonly ttlSeconds: number;

  constructor(
    private readonly cache: CacheService,
    private readonly jwtService: JoseJwtService,
    private readonly config: ConfigService,
    @Inject(Logger) private readonly logger: Logger
  ) {
    this.ttlSeconds = this.config.get<number>(
      "PASSWORD_RESET_TTL_SECONDS",
      this.DEFAULT_TTL_SECONDS
    );
  }

  /**
   * Create a new password reset token for a user
   *
   * @param userId - The user ID to create a token for
   * @returns The signed JWT token
   */
  async create(userId: string): Promise<string> {
    const tokenId = randomUUID();

    const token = await this.jwtService.sign(
      {
        sub: userId,
        tokenId,
        purpose: "password_reset",
      } satisfies PasswordResetTokenPayload,
      this.ttlSeconds
    );

    // Track token in Redis for single-use enforcement
    const tokenData: PasswordResetTokenData = {
      userId,
      createdAt: Date.now(),
    };

    await this.cache.set(this.buildKey(tokenId), tokenData, this.ttlSeconds);

    this.logger.log({ userId, tokenId }, "Password reset token created");

    return token;
  }

  /**
   * Consume (use) a password reset token
   *
   * This method verifies the token and marks it as used (removes from Redis).
   * A token can only be consumed once - subsequent attempts will fail.
   *
   * @param token - The JWT token to consume
   * @returns The user ID from the token
   * @throws BadRequestException if token is invalid, expired, or already used
   */
  async consume(token: string): Promise<{ userId: string }> {
    // Verify JWT signature and expiration
    let payload: PasswordResetTokenPayload;
    try {
      payload = await this.jwtService.verify<PasswordResetTokenPayload>(token);
    } catch (error) {
      this.logger.warn({ error }, "Password reset token verification failed");
      throw new BadRequestException("Invalid or expired token");
    }

    // Validate token purpose
    if (payload.purpose !== "password_reset") {
      this.logger.warn({ purpose: payload.purpose }, "Invalid token purpose for password reset");
      throw new BadRequestException("Invalid token");
    }

    const key = this.buildKey(payload.tokenId);

    // Get and delete atomically (get first, then delete)
    // This ensures the token can only be used once
    const tokenData = await this.cache.get<PasswordResetTokenData>(key);

    if (!tokenData) {
      this.logger.warn(
        { tokenId: payload.tokenId, userId: payload.sub },
        "Password reset token already used or expired"
      );
      throw new BadRequestException("Token already used or expired");
    }

    // Delete the token from Redis (mark as consumed)
    await this.cache.del(key);

    this.logger.log(
      { userId: payload.sub, tokenId: payload.tokenId },
      "Password reset token consumed"
    );

    return { userId: payload.sub };
  }

  /**
   * Build Redis key for token storage
   */
  private buildKey(tokenId: string): string {
    return `${this.REDIS_PREFIX}${tokenId}`;
  }
}
