import { Injectable, Inject } from "@nestjs/common";
import { Logger } from "nestjs-pino";
import { hashToken } from "../../utils/hash-token.util.js";
import { TokenStorageService } from "./token-storage.service.js";

/**
 * Token Revocation Service
 *
 * Handles token revocation operations including:
 * - Single token revocation
 * - Family invalidation
 * - Bulk user token revocation
 */
@Injectable()
export class TokenRevocationService {
  constructor(
    private readonly storage: TokenStorageService,
    @Inject(Logger) private readonly logger: Logger
  ) {}

  /**
   * Revoke a specific refresh token
   */
  async revokeRefreshToken(refreshToken: string): Promise<void> {
    try {
      const tokenHash = hashToken(refreshToken);
      const tokenRecord = await this.storage.getTokenByHash(tokenHash);

      if (tokenRecord) {
        await this.storage.deleteToken(tokenHash);
        await this.storage.deleteFamily(tokenRecord.familyId);
        this.logger.debug("Revoked refresh token", { tokenHash: tokenHash.slice(0, 8) });
      }
    } catch (error) {
      this.logger.error("Failed to revoke refresh token", {
        error: error instanceof Error ? error.message : String(error),
      });
    }
  }

  /**
   * Invalidate an entire token family (used for security violations)
   */
  async invalidateTokenFamily(familyId: string): Promise<void> {
    try {
      const family = await this.storage.getFamily(familyId);

      if (family) {
        await this.storage.deleteFamily(familyId);
        await this.storage.deleteToken(family.tokenHash);
        await this.storage.removeFromUserSet(family.userId, familyId);

        this.logger.warn("Invalidated token family due to security concern", {
          familyId: familyId.slice(0, 8),
          userId: family.userId,
        });
      }
    } catch (error) {
      this.logger.error("Failed to invalidate token family", {
        error: error instanceof Error ? error.message : String(error),
      });
    }
  }

  /**
   * Revoke all refresh tokens for a user
   */
  async revokeAllUserTokens(userId: string): Promise<void> {
    try {
      const familyIds = await this.storage.getUserFamilyIds(userId);

      if (familyIds.length === 0) {
        this.logger.debug("No tokens found for user", { userId });
        return;
      }

      // Get all family data and delete tokens in parallel
      const families = await Promise.all(
        familyIds.map(async familyId => this.storage.getFamily(familyId))
      );

      const deleteOperations = familyIds.flatMap((familyId, index) => {
        const family = families[index];
        const ops = [this.storage.deleteFamily(familyId)];
        if (family?.tokenHash) {
          ops.push(this.storage.deleteToken(family.tokenHash));
        }
        return ops;
      });

      await Promise.all(deleteOperations);

      // Delete the user's token set
      await this.storage.deleteUserSet(userId);

      this.logger.debug("Revoked all tokens for user", {
        userId,
        tokenCount: familyIds.length,
      });
    } catch (error) {
      this.logger.error("Failed to revoke all user tokens", {
        error: error instanceof Error ? error.message : String(error),
        userId,
      });

      // Fallback to scan method
      await this.revokeAllUserTokensFallback(userId);
    }
  }

  /**
   * Fallback method for revoking all user tokens using scan
   */
  private async revokeAllUserTokensFallback(userId: string): Promise<void> {
    try {
      await this.scanAndRevokeUserTokens(userId);
      this.logger.debug("Revoked all tokens for user (fallback method)", { userId });
    } catch (error) {
      this.logger.error("Failed to revoke all user tokens (fallback)", {
        error: error instanceof Error ? error.message : String(error),
        userId,
      });
    }
  }

  /**
   * Scan Redis keys and revoke tokens belonging to a specific user.
   * Uses Redis SCAN which requires sequential cursor iteration.
   */
  private async scanAndRevokeUserTokens(userId: string): Promise<void> {
    let cursor = "0";
    const pattern = `${this.storage.REFRESH_TOKEN_FAMILY_PREFIX}*`;

    /* eslint-disable no-await-in-loop -- Redis SCAN requires sequential cursor iteration; each iteration depends on previous cursor */
    do {
      const [nextCursor, keys] = await this.storage.redis.scan(
        cursor,
        "MATCH",
        pattern,
        "COUNT",
        100
      );
      cursor = nextCursor;

      if (keys?.length) {
        await this.processKeysForUser(keys, userId);
      }
    } while (cursor !== "0");
    /* eslint-enable no-await-in-loop */
  }

  /**
   * Process a batch of keys and delete tokens belonging to the specified user
   */
  private async processKeysForUser(keys: string[], userId: string): Promise<void> {
    const keyDataPairs = await Promise.all(
      keys.map(async key => ({ key, data: await this.storage.redis.get(key) }))
    );

    const deleteOperations = keyDataPairs.flatMap(({ key, data }) => {
      if (!data) return [];

      const family = this.storage.parseRefreshTokenFamilyRecord(data);
      if (!family || family.userId !== userId) return [];

      return [this.storage.redis.del(key), this.storage.deleteToken(family.tokenHash)];
    });

    await Promise.all(deleteOperations);
  }

  /**
   * Get all active refresh token families for a user
   */
  async getUserRefreshTokenFamilies(userId: string): Promise<
    Array<{
      familyId: string;
      deviceId?: string | undefined;
      userAgent?: string | undefined;
      createdAt?: string | undefined;
    }>
  > {
    try {
      const familyIds = await this.storage.getUserFamilyIds(userId);

      const familyResults = await Promise.all(
        familyIds.map(async familyId => {
          const family = await this.storage.getFamily(familyId);
          if (!family) return null;

          return {
            familyId,
            deviceId: family.deviceId,
            userAgent: family.userAgent,
            createdAt: family.createdAt,
          };
        })
      );

      const families = familyResults.filter((f): f is NonNullable<typeof f> => f !== null);

      return families.sort((a, b) => {
        const timeA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
        const timeB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
        return timeB - timeA;
      });
    } catch (error) {
      this.logger.error("Failed to get user refresh token families", {
        error: error instanceof Error ? error.message : String(error),
        userId,
      });
      return [];
    }
  }
}
