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

export interface StoredRefreshToken {
  familyId: string;
  userId: string;
  valid: boolean;
}

export interface StoredRefreshTokenFamily {
  userId: string;
  tokenHash: string;
  deviceId?: string | undefined;
  userAgent?: string | undefined;
  createdAt?: string | undefined;
  absoluteExpiresAt?: string | undefined;
}

/**
 * Token Storage Service
 *
 * Handles Redis CRUD operations for refresh tokens and token families.
 * Manages per-user token sets for efficient token management.
 */
@Injectable()
export class TokenStorageService {
  readonly REFRESH_TOKEN_FAMILY_PREFIX = "refresh_family:";
  readonly REFRESH_TOKEN_PREFIX = "refresh_token:";
  readonly REFRESH_USER_SET_PREFIX = "refresh_user:";

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

  /**
   * Store refresh token in Redis with per-user token set management
   */
  async storeRefreshToken(
    userId: string,
    familyId: string,
    refreshTokenHash: string,
    deviceInfo?: { deviceId?: string | undefined; userAgent?: string | undefined },
    refreshExpirySeconds?: number,
    absoluteExpiresAt?: string
  ): Promise<void> {
    const expiry = refreshExpirySeconds || 7 * 24 * 60 * 60; // Default 7 days
    const userFamilySetKey = `${this.REFRESH_USER_SET_PREFIX}${userId}`;
    const absolute = absoluteExpiresAt ?? new Date(Date.now() + expiry * 1000).toISOString();

    const pipeline = this.redis.pipeline();

    // Store token family metadata
    pipeline.setex(
      `${this.REFRESH_TOKEN_FAMILY_PREFIX}${familyId}`,
      expiry,
      JSON.stringify({
        userId,
        tokenHash: refreshTokenHash,
        deviceId: deviceInfo?.deviceId,
        userAgent: deviceInfo?.userAgent,
        createdAt: new Date().toISOString(),
        absoluteExpiresAt: absolute,
      })
    );

    // Store token validation data
    pipeline.setex(
      `${this.REFRESH_TOKEN_PREFIX}${refreshTokenHash}`,
      expiry,
      JSON.stringify({
        familyId,
        userId,
        valid: true,
      })
    );

    // Add to user's token set for per-user management
    pipeline.sadd(userFamilySetKey, familyId);
    pipeline.expire(userFamilySetKey, expiry);

    // Get count for cleanup check
    pipeline.scard(userFamilySetKey);

    const results = await pipeline.exec();

    // Check if user has too many tokens
    const cardResult = results?.at(-1);
    if (
      cardResult &&
      Array.isArray(cardResult) &&
      cardResult[1] &&
      typeof cardResult[1] === "number"
    ) {
      const maxTokensPerUser = 10;
      const tokenCount = cardResult[1];
      if (tokenCount > maxTokensPerUser) {
        await this.cleanupExcessUserTokens(userId, maxTokensPerUser);
      }
    }
  }

  /**
   * Get a refresh token record by hash
   */
  async getTokenByHash(tokenHash: string): Promise<StoredRefreshToken | null> {
    const key = `${this.REFRESH_TOKEN_PREFIX}${tokenHash}`;
    const data = await this.redis.get(key);
    return data ? this.parseRefreshTokenRecord(data) : null;
  }

  /**
   * Get a token family by ID
   */
  async getFamily(familyId: string): Promise<StoredRefreshTokenFamily | null> {
    const key = `${this.REFRESH_TOKEN_FAMILY_PREFIX}${familyId}`;
    const data = await this.redis.get(key);
    return data ? this.parseRefreshTokenFamilyRecord(data) : null;
  }

  /**
   * Get token and family data together
   */
  async getTokenAndFamily(
    tokenHash: string,
    familyId: string
  ): Promise<{ token: string | null; family: string | null }> {
    const tokenKey = `${this.REFRESH_TOKEN_PREFIX}${tokenHash}`;
    const familyKey = `${this.REFRESH_TOKEN_FAMILY_PREFIX}${familyId}`;
    const [token, family] = await Promise.all([
      this.redis.get(tokenKey),
      this.redis.get(familyKey),
    ]);
    return { token, family };
  }

  /**
   * Mark a token as invalid (used during rotation)
   */
  async markTokenInvalid(
    tokenHash: string,
    familyId: string,
    userId: string,
    ttlSeconds: number
  ): Promise<void> {
    const key = `${this.REFRESH_TOKEN_PREFIX}${tokenHash}`;
    await this.redis.setex(key, ttlSeconds, JSON.stringify({ familyId, userId, valid: false }));
  }

  /**
   * Atomically validate and rotate a refresh token.
   * Uses Redis Lua script to prevent race conditions during concurrent refresh requests.
   *
   * @returns Object with success flag and error reason if failed
   */
  async atomicTokenRotation(params: {
    oldTokenHash: string;
    newTokenHash: string;
    familyId: string;
    userId: string;
    deviceInfo?: { deviceId?: string | undefined; userAgent?: string | undefined } | undefined;
    createdAt: string;
    absoluteExpiresAt: string;
    ttlSeconds: number;
  }): Promise<{ success: boolean; error?: string | undefined }> {
    const {
      oldTokenHash,
      newTokenHash,
      familyId,
      userId,
      deviceInfo,
      createdAt,
      absoluteExpiresAt,
      ttlSeconds,
    } = params;

    const tokenKey = `${this.REFRESH_TOKEN_PREFIX}${oldTokenHash}`;
    const familyKey = `${this.REFRESH_TOKEN_FAMILY_PREFIX}${familyId}`;
    const newTokenKey = `${this.REFRESH_TOKEN_PREFIX}${newTokenHash}`;
    const userSetKey = `${this.REFRESH_USER_SET_PREFIX}${userId}`;

    // Lua script for atomic token rotation
    // This script:
    // 1. Validates the old token exists and is valid
    // 2. Validates the family exists and references the old token
    // 3. Marks the old token as invalid
    // 4. Updates the family with the new token
    // 5. Creates the new token record
    const luaScript = `
      local tokenKey = KEYS[1]
      local familyKey = KEYS[2]
      local newTokenKey = KEYS[3]
      local userSetKey = KEYS[4]

      local oldTokenHash = ARGV[1]
      local newTokenHash = ARGV[2]
      local familyId = ARGV[3]
      local userId = ARGV[4]
      local ttl = tonumber(ARGV[5])
      local invalidTokenData = ARGV[6]
      local newFamilyData = ARGV[7]
      local newTokenData = ARGV[8]

      -- Get current token data
      local tokenData = redis.call('GET', tokenKey)
      if not tokenData then
        return {0, 'TOKEN_NOT_FOUND'}
      end

      -- Parse and validate token
      local token = cjson.decode(tokenData)
      if not token.valid then
        return {0, 'TOKEN_ALREADY_INVALID'}
      end
      if token.familyId ~= familyId then
        return {0, 'FAMILY_MISMATCH'}
      end
      if token.userId ~= userId then
        return {0, 'USER_MISMATCH'}
      end

      -- Get family data
      local familyData = redis.call('GET', familyKey)
      if familyData then
        local family = cjson.decode(familyData)
        if family.tokenHash ~= oldTokenHash then
          return {0, 'TOKEN_NOT_CURRENT'}
        end
      end

      -- All validations passed - perform atomic rotation
      -- Mark old token as invalid
      redis.call('SETEX', tokenKey, ttl, invalidTokenData)

      -- Update family with new token
      redis.call('SETEX', familyKey, ttl, newFamilyData)

      -- Create new token record
      redis.call('SETEX', newTokenKey, ttl, newTokenData)

      -- Update user set
      redis.call('SADD', userSetKey, familyId)
      redis.call('EXPIRE', userSetKey, ttl)

      return {1, 'SUCCESS'}
    `;

    try {
      const invalidTokenData = JSON.stringify({ familyId, userId, valid: false });
      const newFamilyData = JSON.stringify({
        userId,
        tokenHash: newTokenHash,
        deviceId: deviceInfo?.deviceId,
        userAgent: deviceInfo?.userAgent,
        createdAt,
        absoluteExpiresAt,
      });
      const newTokenData = JSON.stringify({ familyId, userId, valid: true });

      const result = (await this.redis.eval(
        luaScript,
        4, // number of keys
        tokenKey,
        familyKey,
        newTokenKey,
        userSetKey,
        oldTokenHash,
        newTokenHash,
        familyId,
        userId,
        ttlSeconds.toString(),
        invalidTokenData,
        newFamilyData,
        newTokenData
      )) as [number, string];

      if (result[0] === 1) {
        return { success: true };
      }

      this.logger.warn("Atomic token rotation failed", {
        reason: result[1],
        familyId: familyId.slice(0, 8),
        tokenHash: oldTokenHash.slice(0, 8),
      });

      return { success: false, error: result[1] };
    } catch (error) {
      this.logger.error("Atomic token rotation error", {
        error: error instanceof Error ? error.message : String(error),
        familyId: familyId.slice(0, 8),
      });
      return { success: false, error: "REDIS_ERROR" };
    }
  }

  /**
   * Update token family with new token
   */
  async updateFamily(
    familyId: string,
    userId: string,
    newTokenHash: string,
    deviceInfo: { deviceId?: string | undefined; userAgent?: string | undefined } | undefined,
    createdAt: string,
    absoluteExpiresAt: string,
    ttlSeconds: number
  ): Promise<void> {
    const familyKey = `${this.REFRESH_TOKEN_FAMILY_PREFIX}${familyId}`;
    const tokenKey = `${this.REFRESH_TOKEN_PREFIX}${newTokenHash}`;
    const userFamilySetKey = `${this.REFRESH_USER_SET_PREFIX}${userId}`;

    const pipeline = this.redis.pipeline();
    pipeline.setex(
      familyKey,
      ttlSeconds,
      JSON.stringify({
        userId,
        tokenHash: newTokenHash,
        deviceId: deviceInfo?.deviceId,
        userAgent: deviceInfo?.userAgent,
        createdAt,
        absoluteExpiresAt,
      })
    );
    pipeline.setex(tokenKey, ttlSeconds, JSON.stringify({ familyId, userId, valid: true }));
    pipeline.sadd(userFamilySetKey, familyId);
    pipeline.expire(userFamilySetKey, ttlSeconds);
    await pipeline.exec();
  }

  /**
   * Delete a token by hash
   */
  async deleteToken(tokenHash: string): Promise<void> {
    await this.redis.del(`${this.REFRESH_TOKEN_PREFIX}${tokenHash}`);
  }

  /**
   * Delete a token family
   */
  async deleteFamily(familyId: string): Promise<void> {
    await this.redis.del(`${this.REFRESH_TOKEN_FAMILY_PREFIX}${familyId}`);
  }

  /**
   * Get TTL for a key
   */
  async getTtl(key: string): Promise<number> {
    return await this.redis.ttl(key);
  }

  /**
   * Get all family IDs for a user
   */
  async getUserFamilyIds(userId: string): Promise<string[]> {
    const key = `${this.REFRESH_USER_SET_PREFIX}${userId}`;
    return await this.redis.smembers(key);
  }

  /**
   * Remove a family from user's set
   */
  async removeFromUserSet(userId: string, familyId: string): Promise<void> {
    const key = `${this.REFRESH_USER_SET_PREFIX}${userId}`;
    await this.redis.srem(key, familyId);
  }

  /**
   * Delete user's family set
   */
  async deleteUserSet(userId: string): Promise<void> {
    await this.redis.del(`${this.REFRESH_USER_SET_PREFIX}${userId}`);
  }

  /**
   * Clean up excess tokens for a user
   */
  private async cleanupExcessUserTokens(userId: string, maxTokens: number): Promise<void> {
    try {
      const familyIds = await this.getUserFamilyIds(userId);
      if (familyIds.length <= maxTokens) return;

      const familiesWithTimes: Array<{ familyId: string; createdAt: Date }> = [];

      for (const familyId of familyIds) {
        const family = await this.getFamily(familyId);
        if (family?.createdAt) {
          familiesWithTimes.push({
            familyId,
            createdAt: new Date(family.createdAt),
          });
        }
      }

      familiesWithTimes.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
      const tokensToRemove = familiesWithTimes.slice(0, familiesWithTimes.length - maxTokens);

      for (const { familyId } of tokensToRemove) {
        const family = await this.getFamily(familyId);
        if (family) {
          await this.deleteFamily(familyId);
          await this.deleteToken(family.tokenHash);
        }
        await this.removeFromUserSet(userId, familyId);
      }

      this.logger.debug("Cleaned up excess user tokens", {
        userId,
        removedCount: tokensToRemove.length,
        remainingCount: maxTokens,
      });
    } catch (error) {
      this.logger.error("Failed to cleanup excess user tokens", {
        error: error instanceof Error ? error.message : String(error),
        userId,
      });
    }
  }

  parseRefreshTokenRecord(value: string): StoredRefreshToken | null {
    try {
      const parsed = JSON.parse(value) as Partial<StoredRefreshToken>;
      if (
        parsed &&
        typeof parsed === "object" &&
        typeof parsed.familyId === "string" &&
        typeof parsed.userId === "string" &&
        typeof parsed.valid === "boolean"
      ) {
        return {
          familyId: parsed.familyId,
          userId: parsed.userId,
          valid: parsed.valid,
        };
      }
    } catch (error) {
      this.logger.warn("Failed to parse refresh token record", {
        error: error instanceof Error ? error.message : String(error),
      });
    }
    return null;
  }

  parseRefreshTokenFamilyRecord(value: string): StoredRefreshTokenFamily | null {
    try {
      const parsed = JSON.parse(value) as Partial<StoredRefreshTokenFamily>;
      if (
        parsed &&
        typeof parsed === "object" &&
        typeof parsed.userId === "string" &&
        typeof parsed.tokenHash === "string"
      ) {
        return {
          userId: parsed.userId,
          tokenHash: parsed.tokenHash,
          deviceId: typeof parsed.deviceId === "string" ? parsed.deviceId : undefined,
          userAgent: typeof parsed.userAgent === "string" ? parsed.userAgent : undefined,
          createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : undefined,
          absoluteExpiresAt:
            typeof parsed.absoluteExpiresAt === "string" ? parsed.absoluteExpiresAt : undefined,
        };
      }
    } catch (error) {
      this.logger.warn("Failed to parse refresh token family record", {
        error: error instanceof Error ? error.message : String(error),
      });
    }
    return null;
  }
}
