import {
  BadRequestException,
  Injectable,
  Inject,
  ServiceUnavailableException,
} from "@nestjs/common";
import { Redis } from "ioredis";
import { Logger } from "nestjs-pino";
import { randomBytes } from "crypto";
import type { AuthTokens } from "@customer-portal/domain/auth";
import type { UserRole } from "@customer-portal/domain/customer";
import { parseJwtExpiry, parseJwtExpiryMs } from "../../utils/jwt-expiry.util.js";
import { hashToken } from "../../utils/hash-token.util.js";
import type { RefreshTokenPayload, DeviceInfo } from "./token.types.js";
import { ACCESS_TOKEN_EXPIRY, REFRESH_TOKEN_EXPIRY } from "./token.constants.js";
import { JoseJwtService } from "./jose-jwt.service.js";
import { TokenStorageService } from "./token-storage.service.js";

const ERROR_SERVICE_UNAVAILABLE = "Authentication service temporarily unavailable";

/**
 * Token Generator Service
 *
 * Handles all token creation logic: access tokens, refresh tokens, and token pairs.
 */
@Injectable()
export class TokenGeneratorService {
  constructor(
    private readonly jwtService: JoseJwtService,
    private readonly storage: TokenStorageService,
    @Inject("REDIS_CLIENT") private readonly redis: Redis,
    @Inject(Logger) private readonly logger: Logger
  ) {}

  /**
   * Generate a new token pair with refresh token storage
   */
  async generateTokenPair(
    user: {
      id: string;
      email: string;
      role?: UserRole;
    },
    deviceInfo?: DeviceInfo
  ): Promise<AuthTokens> {
    if (!user.id || typeof user.id !== "string" || user.id.trim().length === 0) {
      this.logger.error("Invalid user ID provided for token generation", {
        userId: user.id,
      });
      throw new BadRequestException("Invalid user ID for token generation");
    }
    if (!user.email || typeof user.email !== "string" || user.email.trim().length === 0) {
      this.logger.error("Invalid user email provided for token generation", {
        userId: user.id,
      });
      throw new BadRequestException("Invalid user email for token generation");
    }

    const accessTokenId = this.generateTokenId();
    const refreshFamilyId = this.generateTokenId();
    const refreshTokenId = this.generateTokenId();

    const accessPayload = {
      sub: user.id,
      email: user.email,
      role: user.role || "USER",
      tokenId: accessTokenId,
      type: "access",
    };

    const refreshPayload: RefreshTokenPayload = {
      userId: user.id,
      familyId: refreshFamilyId,
      tokenId: refreshTokenId,
      deviceId: deviceInfo?.deviceId,
      userAgent: deviceInfo?.userAgent,
      type: "refresh",
    };

    const accessToken = await this.jwtService.sign(accessPayload, ACCESS_TOKEN_EXPIRY);

    const refreshExpirySeconds = parseJwtExpiry(REFRESH_TOKEN_EXPIRY);
    const refreshToken = await this.jwtService.sign(refreshPayload, refreshExpirySeconds);

    const refreshTokenHash = hashToken(refreshToken);
    const refreshAbsoluteExpiresAt = new Date(
      Date.now() + refreshExpirySeconds * 1000
    ).toISOString();

    if (this.redis.status !== "ready") {
      this.logger.error("Redis not ready for token issuance", {
        status: this.redis.status,
      });
      throw new ServiceUnavailableException(ERROR_SERVICE_UNAVAILABLE);
    }

    try {
      await this.storage.storeRefreshToken({
        userId: user.id,
        familyId: refreshFamilyId,
        refreshTokenHash,
        deviceInfo,
        refreshExpirySeconds,
        absoluteExpiresAt: refreshAbsoluteExpiresAt,
      });
    } catch (error) {
      this.logger.error("Failed to store refresh token in Redis", {
        error: error instanceof Error ? error.message : String(error),
        userId: user.id,
      });
      throw new ServiceUnavailableException(ERROR_SERVICE_UNAVAILABLE);
    }

    const accessExpiresAt = new Date(
      Date.now() + parseJwtExpiryMs(ACCESS_TOKEN_EXPIRY)
    ).toISOString();
    const refreshExpiresAt = refreshAbsoluteExpiresAt;

    this.logger.debug("Generated new token pair", {
      userId: user.id,
      accessTokenId,
      refreshFamilyId,
      refreshTokenId,
    });

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

  /**
   * Generate new access and refresh tokens for rotation
   */
  async generateRotationTokenPair(
    user: { id: string; email: string; role: string },
    familyId: string,
    remainingSeconds: number,
    deviceInfo?: DeviceInfo
  ): Promise<{ newAccessToken: string; newRefreshToken: string; newRefreshTokenHash: string }> {
    const accessTokenId = this.generateTokenId();
    const refreshTokenId = this.generateTokenId();

    const accessPayload = {
      sub: user.id,
      email: user.email,
      role: user.role || "USER",
      tokenId: accessTokenId,
      type: "access",
    };

    const newRefreshPayload: RefreshTokenPayload = {
      userId: user.id,
      familyId,
      tokenId: refreshTokenId,
      deviceId: deviceInfo?.deviceId,
      userAgent: deviceInfo?.userAgent,
      type: "refresh",
    };

    const newAccessToken = await this.jwtService.sign(accessPayload, ACCESS_TOKEN_EXPIRY);
    const newRefreshToken = await this.jwtService.sign(newRefreshPayload, remainingSeconds);
    const newRefreshTokenHash = hashToken(newRefreshToken);

    return { newAccessToken, newRefreshToken, newRefreshTokenHash };
  }

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