import { Injectable, Inject } from "@nestjs/common";
import { Logger } from "nestjs-pino";
import { ConfigService } from "@nestjs/config";
import { Redis } from "ioredis";
import { getErrorMessage } from "@bff/core/utils/error.util";

export interface MigrationStats {
  totalKeysScanned: number;
  familiesFound: number;
  familiesMigrated: number;
  tokensFound: number;
  tokensMigrated: number;
  orphanedTokens: number;
  errors: number;
  duration: number;
}

interface ParsedFamily {
  userId: string;
}

interface ParsedToken {
  userId: string;
  familyId: string;
}

@Injectable()
export class TokenMigrationService {
  private readonly REFRESH_TOKEN_FAMILY_PREFIX = "refresh_family:";
  private readonly REFRESH_TOKEN_PREFIX = "refresh_token:";
  private readonly REFRESH_USER_SET_PREFIX = "refresh_user:";

  constructor(
    @Inject("REDIS_CLIENT") private readonly redis: Redis,
    @Inject(Logger) private readonly logger: Logger,
    private readonly configService: ConfigService
  ) {}

  /**
   * Migrate existing refresh tokens to the new per-user token set structure
   */
  async migrateExistingTokens(dryRun = true): Promise<MigrationStats> {
    const startTime = Date.now();
    const stats: MigrationStats = {
      totalKeysScanned: 0,
      familiesFound: 0,
      familiesMigrated: 0,
      tokensFound: 0,
      tokensMigrated: 0,
      orphanedTokens: 0,
      errors: 0,
      duration: 0,
    };

    this.logger.log("Starting token migration", { dryRun });

    if (this.redis.status !== "ready") {
      throw new Error("Redis is not ready for migration");
    }

    try {
      // First, scan for all refresh token families
      await this.migrateFamilies(stats, dryRun);

      // Then, scan for orphaned tokens (tokens without families)
      await this.migrateOrphanedTokens(stats, dryRun);

      stats.duration = Date.now() - startTime;

      this.logger.log("Token migration completed", {
        dryRun,
        stats,
      });

      return stats;
    } catch (error) {
      stats.duration = Date.now() - startTime;
      stats.errors++;

      this.logger.error("Token migration failed", {
        error: getErrorMessage(error),
        stats,
      });

      throw error;
    }
  }

  /**
   * Migrate refresh token families to per-user sets
   */
  private async migrateFamilies(stats: MigrationStats, dryRun: boolean): Promise<void> {
    let cursor = "0";
    const pattern = `${this.REFRESH_TOKEN_FAMILY_PREFIX}*`;

    do {
      const [nextCursor, keys] = await this.redis.scan(cursor, "MATCH", pattern, "COUNT", 100);
      cursor = nextCursor;
      stats.totalKeysScanned += keys.length;

      if (keys && keys.length > 0) {
        for (const key of keys) {
          try {
            await this.migrateSingleFamily(key, stats, dryRun);
          } catch (error) {
            stats.errors++;
            this.logger.error("Failed to migrate family", {
              key,
              error: getErrorMessage(error),
            });
          }
        }
      }
    } while (cursor !== "0");
  }

  /**
   * Migrate a single refresh token family
   */
  private async migrateSingleFamily(
    familyKey: string,
    stats: MigrationStats,
    dryRun: boolean
  ): Promise<void> {
    const familyData = await this.redis.get(familyKey);
    if (!familyData) {
      return;
    }

    stats.familiesFound++;

    const family = this.parseFamilyData(familyKey, familyData, stats);
    if (!family) {
      return;
    }

    const familyId = familyKey.replace(this.REFRESH_TOKEN_FAMILY_PREFIX, "");
    const userFamilySetKey = `${this.REFRESH_USER_SET_PREFIX}${family.userId}`;

    // Check if this family is already in the user's set
    const isAlreadyMigrated = await this.redis.sismember(userFamilySetKey, familyId);

    if (isAlreadyMigrated) {
      this.logger.debug("Family already migrated", { familyKey, userId: family.userId });
      return;
    }

    if (!dryRun) {
      // Add family to user's token set
      const pipeline = this.redis.pipeline();
      pipeline.sadd(userFamilySetKey, familyId);

      // Set expiration on the user set (use the same TTL as the family)
      const ttl = await this.redis.ttl(familyKey);
      if (ttl > 0) {
        pipeline.expire(userFamilySetKey, ttl);
      }

      await pipeline.exec();
    }

    stats.familiesMigrated++;

    this.logger.debug("Migrated family to user set", {
      familyKey,
      userId: family.userId,
      dryRun,
    });
  }

  /**
   * Migrate orphaned tokens (tokens without corresponding families)
   */
  private async migrateOrphanedTokens(stats: MigrationStats, dryRun: boolean): Promise<void> {
    let cursor = "0";
    const pattern = `${this.REFRESH_TOKEN_PREFIX}*`;

    do {
      const [nextCursor, keys] = await this.redis.scan(cursor, "MATCH", pattern, "COUNT", 100);
      cursor = nextCursor;
      stats.totalKeysScanned += keys.length;

      if (keys && keys.length > 0) {
        for (const key of keys) {
          try {
            await this.migrateSingleToken(key, stats, dryRun);
          } catch (error) {
            stats.errors++;
            this.logger.error("Failed to migrate token", {
              key,
              error: getErrorMessage(error),
            });
          }
        }
      }
    } while (cursor !== "0");
  }

  /**
   * Migrate a single refresh token
   */
  private async migrateSingleToken(
    tokenKey: string,
    stats: MigrationStats,
    dryRun: boolean
  ): Promise<void> {
    const tokenData = await this.redis.get(tokenKey);
    if (!tokenData) {
      return;
    }

    stats.tokensFound++;

    const token = this.parseTokenData(tokenKey, tokenData, stats);
    if (!token) {
      return;
    }

    // Check if the corresponding family exists
    const familyKey = `${this.REFRESH_TOKEN_FAMILY_PREFIX}${token.familyId}`;
    const familyExists = await this.redis.exists(familyKey);

    if (!familyExists) {
      stats.orphanedTokens++;
      this.logger.warn("Found orphaned token (no corresponding family)", {
        tokenKey,
        familyId: token.familyId,
        userId: token.userId,
      });

      if (!dryRun) {
        // Remove orphaned token
        await this.redis.del(tokenKey);
        this.logger.debug("Removed orphaned token", { tokenKey });
      }
      return;
    }

    // Check if this token's family is already in the user's set
    const userFamilySetKey = `${this.REFRESH_USER_SET_PREFIX}${token.userId}`;
    const isAlreadyMigrated = await this.redis.sismember(userFamilySetKey, token.familyId);

    if (isAlreadyMigrated) {
      stats.tokensMigrated++;
      return;
    }

    if (!dryRun) {
      // Add family to user's token set
      const pipeline = this.redis.pipeline();
      pipeline.sadd(userFamilySetKey, token.familyId);

      // Set expiration on the user set (use the same TTL as the token)
      const ttl = await this.redis.ttl(tokenKey);
      if (ttl > 0) {
        pipeline.expire(userFamilySetKey, ttl);
      }

      await pipeline.exec();
    }

    stats.tokensMigrated++;

    this.logger.debug("Migrated token family to user set", {
      tokenKey,
      familyId: token.familyId,
      userId: token.userId,
      dryRun,
    });
  }

  /**
   * Clean up orphaned tokens and expired families
   */
  async cleanupOrphanedTokens(dryRun = true): Promise<{ removed: number; errors: number }> {
    const stats = { removed: 0, errors: 0 };

    this.logger.log("Starting orphaned token cleanup", { dryRun });

    let cursor = "0";
    const pattern = `${this.REFRESH_TOKEN_PREFIX}*`;

    do {
      const [nextCursor, keys] = await this.redis.scan(cursor, "MATCH", pattern, "COUNT", 100);
      cursor = nextCursor;

      if (keys && keys.length > 0) {
        for (const key of keys) {
          try {
            const tokenData = await this.redis.get(key);
            if (!tokenData) continue;

            const token = this.parseTokenData(key, tokenData, stats);
            if (!token) {
              continue;
            }

            // Check if the corresponding family exists
            const familyKey = `${this.REFRESH_TOKEN_FAMILY_PREFIX}${token.familyId}`;
            const familyExists = await this.redis.exists(familyKey);

            if (!familyExists) {
              if (!dryRun) {
                await this.redis.del(key);
              }
              stats.removed++;

              this.logger.debug("Removed orphaned token", {
                tokenKey: key,
                familyId: token.familyId,
                dryRun,
              });
            }
          } catch (error) {
            stats.errors++;
            this.logger.error("Failed to cleanup token", {
              key,
              error: getErrorMessage(error),
            });
          }
        }
      }
    } while (cursor !== "0");

    this.logger.log("Orphaned token cleanup completed", { dryRun, stats });
    return stats;
  }

  /**
   * Get migration status and statistics
   */
  async getMigrationStatus(): Promise<{
    totalFamilies: number;
    totalTokens: number;
    migratedUsers: number;
    orphanedTokens: number;
    needsMigration: boolean;
  }> {
    const stats = {
      totalFamilies: 0,
      totalTokens: 0,
      migratedUsers: 0,
      orphanedTokens: 0,
      needsMigration: false,
    };

    // Count total families
    let cursor = "0";
    do {
      const [nextCursor, keys] = await this.redis.scan(
        cursor,
        "MATCH",
        `${this.REFRESH_TOKEN_FAMILY_PREFIX}*`,
        "COUNT",
        100
      );
      cursor = nextCursor;
      stats.totalFamilies += keys.length;
    } while (cursor !== "0");

    // Count total tokens
    cursor = "0";
    do {
      const [nextCursor, keys] = await this.redis.scan(
        cursor,
        "MATCH",
        `${this.REFRESH_TOKEN_PREFIX}*`,
        "COUNT",
        100
      );
      cursor = nextCursor;
      stats.totalTokens += keys.length;
    } while (cursor !== "0");

    // Count migrated users (users with token sets)
    cursor = "0";
    do {
      const [nextCursor, keys] = await this.redis.scan(
        cursor,
        "MATCH",
        `${this.REFRESH_USER_SET_PREFIX}*`,
        "COUNT",
        100
      );
      cursor = nextCursor;
      stats.migratedUsers += keys.length;
    } while (cursor !== "0");

    // Estimate if migration is needed
    stats.needsMigration = stats.totalFamilies > 0 && stats.migratedUsers === 0;

    return stats;
  }

  private parseFamilyData(
    familyKey: string,
    raw: string,
    tracker: { errors: number }
  ): ParsedFamily | null {
    let parsed: unknown;
    try {
      parsed = JSON.parse(raw);
    } catch (error) {
      tracker.errors++;
      this.logger.error("Failed to parse family data", {
        familyKey,
        error: getErrorMessage(error),
      });
      return null;
    }

    if (!parsed || typeof parsed !== "object") {
      this.logger.warn("Invalid family structure, skipping", { familyKey });
      return null;
    }

    const record = parsed as Record<string, unknown>;
    const userId = record.userId;

    if (typeof userId !== "string" || userId.length === 0) {
      this.logger.warn("Invalid family structure, skipping", { familyKey });
      return null;
    }

    return { userId };
  }

  private parseTokenData(
    tokenKey: string,
    raw: string,
    tracker: { errors: number }
  ): ParsedToken | null {
    let parsed: unknown;
    try {
      parsed = JSON.parse(raw);
    } catch (error) {
      tracker.errors++;
      this.logger.error("Failed to parse token data", {
        tokenKey,
        error: getErrorMessage(error),
      });
      return null;
    }

    if (!parsed || typeof parsed !== "object") {
      this.logger.warn("Invalid token structure, skipping", { tokenKey });
      return null;
    }

    const record = parsed as Record<string, unknown>;
    const userId = record.userId;
    const familyId = record.familyId;

    if (typeof userId !== "string" || typeof familyId !== "string") {
      this.logger.warn("Invalid token structure, skipping", { tokenKey });
      return null;
    }

    return { userId, familyId };
  }
}
