import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { type Request } from 'express';
import { ExtractJwt, Strategy } from 'passport-jwt';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- NestJS DI requires value imports for emitDecoratorMetadata
import { PrismaService, RedisService } from '@modules/shared';
import { type JwtPayload } from '../services/token.service';

function extractJwtFromCookieOrHeader(req: Request): string | null {
  const cookieToken = req.cookies?.['access_token'] as string | undefined;
  if (cookieToken) return cookieToken;
  return ExtractJwt.fromAuthHeaderAsBearerToken()(req);
}

/** Cached user status — JSON encoded in Redis. */
interface CachedUserStatus {
  isActive: boolean;
  deletedAt: string | null;
}

/**
 * Redis key prefix for user status cache. Versioned so that a schema
 * change can invalidate all stale entries by bumping the version.
 */
export const USER_STATUS_CACHE_PREFIX = 'auth:user_status:v1';
/** TTL for cached user status (seconds). */
export const USER_STATUS_CACHE_TTL_SECONDS = 60;

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    private readonly prisma: PrismaService,
    private readonly redis: RedisService,
  ) {
    const jwtSecret = process.env['JWT_SECRET'];
    if (!jwtSecret) {
      throw new Error('JWT_SECRET environment variable is required');
    }

    super({
      jwtFromRequest: extractJwtFromCookieOrHeader,
      ignoreExpiration: false,
      secretOrKey: jwtSecret,
      audience: 'goodgo-api',
      issuer: 'goodgo-platform',
    });
  }

  async validate(payload: JwtPayload): Promise<JwtPayload> {
    const status = await this.loadUserStatus(payload.sub);
    if (!status || !status.isActive || status.deletedAt !== null) {
      throw new UnauthorizedException('User account is inactive or deleted');
    }
    return { sub: payload.sub, phone: payload.phone, role: payload.role };
  }

  /**
   * Loads user status from Redis cache if present, otherwise from DB and
   * populates the cache with a 60 s TTL. Redis failures are non-fatal:
   * we fall back to DB so a Redis outage cannot lock out all users.
   *
   * Returns null only when the user does not exist in the DB.
   */
  private async loadUserStatus(userId: string): Promise<CachedUserStatus | null> {
    const cacheKey = `${USER_STATUS_CACHE_PREFIX}:${userId}`;

    if (this.redis.isAvailable()) {
      try {
        const cached = await this.redis.get(cacheKey);
        if (cached !== null) {
          return JSON.parse(cached) as CachedUserStatus;
        }
      } catch {
        // Swallow: degrade to DB on Redis read error.
      }
    }

    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      select: { isActive: true, deletedAt: true },
    });
    if (!user) return null;

    const status: CachedUserStatus = {
      isActive: user.isActive,
      deletedAt: user.deletedAt ? user.deletedAt.toISOString() : null,
    };

    if (this.redis.isAvailable()) {
      try {
        await this.redis.set(cacheKey, JSON.stringify(status), USER_STATUS_CACHE_TTL_SECONDS);
      } catch {
        // Swallow: cache population is best-effort.
      }
    }

    return status;
  }
}
