import {
  Injectable,
  Inject,
  UnauthorizedException,
  ServiceUnavailableException,
} from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { ConfigService } from "@nestjs/config";
import { Redis } from "ioredis";
import { Logger } from "nestjs-pino";
import { randomBytes, createHash } from "crypto";
import type { AuthTokens } from "@customer-portal/domain/auth";
import type { User } from "@customer-portal/domain/customer";
import { UsersFacade } from "@bff/modules/users/application/users.facade";
import { mapPrismaUserToDomain } from "@bff/infra/mappers";

export interface RefreshTokenPayload {
  userId: string;
  tokenId: string;
  deviceId?: string;
  userAgent?: string;
  type: "refresh";
}

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

interface StoredRefreshTokenFamily {
  userId: string;
  tokenHash: string;
  deviceId?: string;
  userAgent?: string;
  createdAt?: string;
}

@Injectable()
export class AuthTokenService {
  private readonly ACCESS_TOKEN_EXPIRY = "15m"; // Short-lived access tokens
  private readonly REFRESH_TOKEN_EXPIRY = "7d"; // Longer-lived refresh tokens
  private readonly REFRESH_TOKEN_FAMILY_PREFIX = "refresh_family:";
  private readonly REFRESH_TOKEN_PREFIX = "refresh_token:";
  private readonly REFRESH_USER_SET_PREFIX = "refresh_user:";
  private readonly allowRedisFailOpen: boolean;
  private readonly requireRedisForTokens: boolean;
  private readonly maintenanceMode: boolean;
  private readonly maintenanceMessage: string;

  constructor(
    private readonly jwtService: JwtService,
    private readonly configService: ConfigService,
    @Inject("REDIS_CLIENT") private readonly redis: Redis,
    @Inject(Logger) private readonly logger: Logger,
    private readonly usersFacade: UsersFacade
  ) {
    this.allowRedisFailOpen =
      this.configService.get("AUTH_ALLOW_REDIS_TOKEN_FAILOPEN", "false") === "true";
    this.requireRedisForTokens =
      this.configService.get("AUTH_REQUIRE_REDIS_FOR_TOKENS", "false") === "true";
    this.maintenanceMode = this.configService.get("AUTH_MAINTENANCE_MODE", "false") === "true";
    this.maintenanceMessage = this.configService.get(
      "AUTH_MAINTENANCE_MESSAGE",
      "Authentication service is temporarily unavailable for maintenance. Please try again later."
    );
  }

  /**
   * Check if authentication service is available
   */
  private checkServiceAvailability(): void {
    if (this.maintenanceMode) {
      this.logger.warn("Authentication service in maintenance mode", {
        maintenanceMessage: this.maintenanceMessage,
      });
      throw new ServiceUnavailableException(this.maintenanceMessage);
    }

    if (this.requireRedisForTokens && this.redis.status !== "ready") {
      this.logger.error("Redis required for token operations but not available", {
        redisStatus: this.redis.status,
        requireRedisForTokens: this.requireRedisForTokens,
      });
      throw new ServiceUnavailableException("Authentication service temporarily unavailable");
    }
  }

  /**
   * Generate a new token pair with refresh token rotation
   */
  async generateTokenPair(
    user: {
      id: string;
      email: string;
      role?: string;
    },
    deviceInfo?: {
      deviceId?: string;
      userAgent?: string;
    }
  ): Promise<AuthTokens> {
    this.checkServiceAvailability();

    const tokenId = this.generateTokenId();
    const familyId = this.generateTokenId();

    // Create access token payload
    const accessPayload = {
      sub: user.id,
      email: user.email,
      role: user.role || "user",
      tokenId,
      type: "access",
    };

    // Create refresh token payload
    const refreshPayload: RefreshTokenPayload = {
      userId: user.id,
      tokenId: familyId,
      deviceId: deviceInfo?.deviceId,
      userAgent: deviceInfo?.userAgent,
      type: "refresh",
    };

    // Generate tokens
    const accessToken = this.jwtService.sign(accessPayload, {
      expiresIn: this.ACCESS_TOKEN_EXPIRY,
    });

    const refreshToken = this.jwtService.sign(refreshPayload, {
      expiresIn: this.REFRESH_TOKEN_EXPIRY,
    });

    // Store refresh token family in Redis
    const refreshTokenHash = this.hashToken(refreshToken);
    const refreshExpirySeconds = this.parseExpiryToSeconds(this.REFRESH_TOKEN_EXPIRY);

    if (this.redis.status === "ready") {
      try {
        await this.storeRefreshTokenInRedis(
          user.id,
          familyId,
          refreshTokenHash,
          deviceInfo,
          refreshExpirySeconds
        );
      } catch (error) {
        this.logger.error("Failed to store refresh token in Redis", {
          error: error instanceof Error ? error.message : String(error),
          userId: user.id,
        });

        // If Redis is required, fail the operation
        if (this.requireRedisForTokens) {
          throw new ServiceUnavailableException("Authentication service temporarily unavailable");
        }
      }
    } else {
      if (this.requireRedisForTokens) {
        this.logger.error("Redis required but not ready for token issuance", {
          status: this.redis.status,
        });
        throw new ServiceUnavailableException("Authentication service temporarily unavailable");
      }

      this.logger.warn("Redis not ready for token issuance; issuing non-rotating tokens", {
        status: this.redis.status,
      });
    }

    const accessExpiresAt = new Date(
      Date.now() + this.parseExpiryToMs(this.ACCESS_TOKEN_EXPIRY)
    ).toISOString();
    const refreshExpiresAt = new Date(
      Date.now() + this.parseExpiryToMs(this.REFRESH_TOKEN_EXPIRY)
    ).toISOString();

    this.logger.debug("Generated new token pair", { userId: user.id, tokenId, familyId });

    return {
      accessToken,
      refreshToken,
      expiresAt: accessExpiresAt,
      refreshExpiresAt,
      tokenType: "Bearer",
    };
  }

  /**
   * Refresh access token using refresh token rotation
   */
  async refreshTokens(
    refreshToken: string,
    deviceInfo?: {
      deviceId?: string;
      userAgent?: string;
    }
  ): Promise<{ tokens: AuthTokens; user: User }> {
    if (!refreshToken) {
      throw new UnauthorizedException("Invalid refresh token");
    }

    this.checkServiceAvailability();

    if (!this.allowRedisFailOpen && this.redis.status !== "ready") {
      this.logger.error("Redis unavailable for token refresh", {
        redisStatus: this.redis.status,
      });
      throw new ServiceUnavailableException("Token refresh temporarily unavailable");
    }
    try {
      // Verify refresh token
      const payload = this.jwtService.verify<RefreshTokenPayload>(refreshToken);

      if (payload.type !== "refresh") {
        this.logger.warn("Token presented to refresh endpoint is not a refresh token", {
          tokenId: payload.tokenId,
        });
        throw new UnauthorizedException("Invalid refresh token");
      }

      const refreshTokenHash = this.hashToken(refreshToken);

      // Check if refresh token exists and is valid
      let storedToken: string | null;
      try {
        storedToken = await this.redis.get(`${this.REFRESH_TOKEN_PREFIX}${refreshTokenHash}`);
      } catch (error) {
        this.logger.error("Redis error during token refresh", {
          error: error instanceof Error ? error.message : String(error),
        });
        throw new UnauthorizedException("Token validation temporarily unavailable");
      }

      if (!storedToken) {
        this.logger.warn("Refresh token not found or expired", {
          tokenHash: refreshTokenHash.slice(0, 8),
        });
        throw new UnauthorizedException("Invalid refresh token");
      }

      const tokenRecord = this.parseRefreshTokenRecord(storedToken);
      if (!tokenRecord) {
        this.logger.warn("Stored refresh token payload was invalid JSON", {
          tokenHash: refreshTokenHash.slice(0, 8),
        });
        await this.redis.del(`${this.REFRESH_TOKEN_PREFIX}${refreshTokenHash}`);
        throw new UnauthorizedException("Invalid refresh token");
      }

      if (!tokenRecord.valid) {
        this.logger.warn("Refresh token marked as invalid", {
          tokenHash: refreshTokenHash.slice(0, 8),
        });
        // Invalidate entire token family on reuse attempt
        await this.invalidateTokenFamily(tokenRecord.familyId);
        throw new UnauthorizedException("Invalid refresh token");
      }

      // Get user info from database (using internal method to get role)
      const user = await this.usersFacade.findByIdInternal(payload.userId);
      if (!user) {
        this.logger.warn("User not found during token refresh", { userId: payload.userId });
        throw new UnauthorizedException("User not found");
      }

      // Convert to the format expected by generateTokenPair
      const userProfile = mapPrismaUserToDomain(user);

      // Invalidate current refresh token
      await this.redis.del(`${this.REFRESH_TOKEN_PREFIX}${refreshTokenHash}`);

      // Generate new token pair
      const newTokenPair = await this.generateTokenPair(user, deviceInfo);

      this.logger.debug("Refreshed token pair", { userId: payload.userId });

      return {
        tokens: newTokenPair,
        user: userProfile,
      };
    } catch (error) {
      this.logger.error("Token refresh failed", {
        error: error instanceof Error ? error.message : String(error),
      });

      // Always fail closed when Redis is not available for token operations
      // This prevents refresh token replay attacks and maintains security
      if (this.redis.status !== "ready") {
        this.logger.error("Redis unavailable for token refresh - failing closed for security", {
          redisStatus: this.redis.status,
          allowRedisFailOpen: this.allowRedisFailOpen,
          securityReason: "refresh_token_rotation_requires_redis",
        });
        throw new ServiceUnavailableException("Token refresh temporarily unavailable");
      }

      throw new UnauthorizedException("Invalid refresh token");
    }
  }

  /**
   * Revoke a specific refresh token
   */
  async revokeRefreshToken(refreshToken: string): Promise<void> {
    try {
      const refreshTokenHash = this.hashToken(refreshToken);
      const storedToken = await this.redis.get(`${this.REFRESH_TOKEN_PREFIX}${refreshTokenHash}`);

      if (storedToken) {
        const tokenRecord = this.parseRefreshTokenRecord(storedToken);
        await this.redis.del(`${this.REFRESH_TOKEN_PREFIX}${refreshTokenHash}`);

        if (tokenRecord) {
          await this.redis.del(`${this.REFRESH_TOKEN_FAMILY_PREFIX}${tokenRecord.familyId}`);
        }

        this.logger.debug("Revoked refresh token", { tokenHash: refreshTokenHash.slice(0, 8) });
      }
    } catch (error) {
      this.logger.error("Failed to revoke refresh token", {
        error: error instanceof Error ? error.message : String(error),
      });
    }
  }

  /**
   * Store refresh token in Redis with per-user token set management
   */
  private async storeRefreshTokenInRedis(
    userId: string,
    familyId: string,
    refreshTokenHash: string,
    deviceInfo?: { deviceId?: string; userAgent?: string },
    refreshExpirySeconds?: number
  ): Promise<void> {
    const expiry = refreshExpirySeconds || this.parseExpiryToSeconds(this.REFRESH_TOKEN_EXPIRY);
    const userFamilySetKey = `${this.REFRESH_USER_SET_PREFIX}${userId}`;

    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(),
      })
    );

    // 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);

    // Enforce maximum tokens per user (optional limit)
    const maxTokensPerUser = 10; // Configurable limit
    pipeline.scard(userFamilySetKey);

    const results = await pipeline.exec();

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

  /**
   * Clean up excess tokens for a user, keeping only the most recent ones
   */
  private async cleanupExcessUserTokens(userId: string, maxTokens: number): Promise<void> {
    try {
      const userFamilySetKey = `${this.REFRESH_USER_SET_PREFIX}${userId}`;
      const familyIds = await this.redis.smembers(userFamilySetKey);

      if (familyIds.length <= maxTokens) {
        return;
      }

      // Get creation times for all families
      const familiesWithTimes: Array<{ familyId: string; createdAt: Date }> = [];

      for (const familyId of familyIds) {
        const familyData = await this.redis.get(`${this.REFRESH_TOKEN_FAMILY_PREFIX}${familyId}`);
        if (familyData) {
          const family = this.parseRefreshTokenFamilyRecord(familyData);
          if (family?.createdAt) {
            familiesWithTimes.push({
              familyId,
              createdAt: new Date(family.createdAt),
            });
          }
        }
      }

      // Sort by creation time (oldest first) and remove excess
      familiesWithTimes.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
      const tokensToRemove = familiesWithTimes.slice(0, familiesWithTimes.length - maxTokens);

      for (const { familyId } of tokensToRemove) {
        await this.invalidateTokenFamily(familyId);
        await this.redis.srem(userFamilySetKey, 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,
      });
    }
  }

  /**
   * Get all active refresh token families for a user
   */
  async getUserRefreshTokenFamilies(
    userId: string
  ): Promise<
    Array<{ familyId: string; deviceId?: string; userAgent?: string; createdAt?: string }>
  > {
    try {
      const userFamilySetKey = `${this.REFRESH_USER_SET_PREFIX}${userId}`;
      const familyIds = await this.redis.smembers(userFamilySetKey);

      const families: Array<{
        familyId: string;
        deviceId?: string;
        userAgent?: string;
        createdAt?: string;
      }> = [];

      for (const familyId of familyIds) {
        const familyData = await this.redis.get(`${this.REFRESH_TOKEN_FAMILY_PREFIX}${familyId}`);
        if (familyData) {
          const family = this.parseRefreshTokenFamilyRecord(familyData);
          if (family) {
            families.push({
              familyId,
              deviceId: family.deviceId,
              userAgent: family.userAgent,
              createdAt: family.createdAt,
            });
          }
        }
      }

      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; // Most recent first
      });
    } catch (error) {
      this.logger.error("Failed to get user refresh token families", {
        error: error instanceof Error ? error.message : String(error),
        userId,
      });
      return [];
    }
  }

  /**
   * Revoke all refresh tokens for a user (optimized with per-user sets)
   */
  async revokeAllUserTokens(userId: string): Promise<void> {
    try {
      const userFamilySetKey = `${this.REFRESH_USER_SET_PREFIX}${userId}`;
      const familyIds = await this.redis.smembers(userFamilySetKey);

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

      const pipeline = this.redis.pipeline();

      // Get all family data first to find token hashes
      const familyDataPromises = familyIds.map(familyId =>
        this.redis.get(`${this.REFRESH_TOKEN_FAMILY_PREFIX}${familyId}`)
      );

      const familyDataResults = await Promise.all(familyDataPromises);

      // Delete all tokens and families
      for (let i = 0; i < familyIds.length; i++) {
        const familyId = familyIds[i];
        const familyData = familyDataResults[i];

        // Delete family record
        pipeline.del(`${this.REFRESH_TOKEN_FAMILY_PREFIX}${familyId}`);

        // Delete token record if we can parse the family data
        if (familyData) {
          const family = this.parseRefreshTokenFamilyRecord(familyData);
          if (family?.tokenHash) {
            pipeline.del(`${this.REFRESH_TOKEN_PREFIX}${family.tokenHash}`);
          }
        }
      }

      // Delete the user's token set
      pipeline.del(userFamilySetKey);

      await pipeline.exec();

      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 the old scan method if the optimized approach fails
      await this.revokeAllUserTokensFallback(userId);
    }
  }

  /**
   * Fallback method for revoking all user tokens using scan
   */
  private async revokeAllUserTokensFallback(userId: string): Promise<void> {
    try {
      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;

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

            const family = this.parseRefreshTokenFamilyRecord(data);
            if (family && family.userId === userId) {
              await this.redis.del(key);
              await this.redis.del(`${this.REFRESH_TOKEN_PREFIX}${family.tokenHash}`);
            }
          }
        }
      } while (cursor !== "0");

      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,
      });
    }
  }

  private async invalidateTokenFamily(familyId: string): Promise<void> {
    try {
      const familyData = await this.redis.get(`${this.REFRESH_TOKEN_FAMILY_PREFIX}${familyId}`);
      if (familyData) {
        const family = this.parseRefreshTokenFamilyRecord(familyData);

        const pipeline = this.redis.pipeline();
        pipeline.del(`${this.REFRESH_TOKEN_FAMILY_PREFIX}${familyId}`);

        if (family) {
          pipeline.del(`${this.REFRESH_TOKEN_PREFIX}${family.tokenHash}`);

          // Remove from user's token set
          const userFamilySetKey = `${this.REFRESH_USER_SET_PREFIX}${family.userId}`;
          pipeline.srem(userFamilySetKey, familyId);

          await pipeline.exec();

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

  private generateTokenId(): string {
    return randomBytes(32).toString("hex");
  }

  private hashToken(token: string): string {
    return createHash("sha256").update(token).digest("hex");
  }

  private 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;
  }

  private 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,
        };
      }
    } catch (error) {
      this.logger.warn("Failed to parse refresh token family record", {
        error: error instanceof Error ? error.message : String(error),
      });
    }
    return null;
  }

  private parseExpiryToMs(expiry: string): number {
    const unit = expiry.slice(-1);
    const value = parseInt(expiry.slice(0, -1));

    switch (unit) {
      case "s":
        return value * 1000;
      case "m":
        return value * 60 * 1000;
      case "h":
        return value * 60 * 60 * 1000;
      case "d":
        return value * 24 * 60 * 60 * 1000;
      default:
        return 15 * 60 * 1000; // Default 15 minutes
    }
  }

  private parseExpiryToSeconds(expiry: string): number {
    return Math.floor(this.parseExpiryToMs(expiry) / 1000);
  }

  private calculateExpiryDate(expiresIn: string | number): string {
    const now = new Date();
    if (typeof expiresIn === "number") {
      return new Date(now.getTime() + expiresIn * 1000).toISOString();
    }
    return new Date(now.getTime() + this.parseExpiryToMs(expiresIn)).toISOString();
  }
}
