import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
  RequestTimeoutException,
  ServiceUnavailableException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Reflector } from '@nestjs/core';
import { Observable, TimeoutError, catchError, finalize, throwError, timeout } from 'rxjs';
import { REQUEST_TIMEOUT_METADATA } from '../../common/decorators/request-timeout.decorator';
import { ShutdownCoordinatorService } from './shutdown-coordinator.service';

type HttpRequest = {
  originalUrl?: string;
  url?: string;
};

@Injectable()
export class RequestTimeoutInterceptor implements NestInterceptor {
  constructor(
    private readonly configService: ConfigService,
    private readonly reflector: Reflector,
    private readonly shutdownCoordinator: ShutdownCoordinatorService,
  ) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
    if (context.getType() !== 'http') {
      return next.handle();
    }

    const request = context.switchToHttp().getRequest<HttpRequest>();
    const healthRequest = this.isHealthRequest(request.originalUrl ?? request.url ?? '');

    if (this.shutdownCoordinator.isDraining()) {
      if (healthRequest) {
        return next.handle();
      }
      return throwError(
        () =>
          new ServiceUnavailableException({
            statusCode: 503,
            error: 'Service Unavailable',
            message: 'Server is shutting down; retry the request on another instance',
          }),
      );
    }

    if (!this.shutdownCoordinator.beginRequest()) {
      return throwError(() => new ServiceUnavailableException('Server is shutting down'));
    }

    const timeoutOverride = this.reflector.getAllAndOverride<number>(REQUEST_TIMEOUT_METADATA, [
      context.getHandler(),
      context.getClass(),
    ]);
    const timeoutMs =
      timeoutOverride ??
      this.configService.get<number>('reliability.requestTimeoutMs', { infer: true }) ??
      30_000;
    const response$ = next.handle();

    if (timeoutMs <= 0) {
      return response$.pipe(finalize(() => this.shutdownCoordinator.endRequest()));
    }

    return response$.pipe(
      timeout({ first: timeoutMs }),
      catchError((error: unknown) => {
        if (error instanceof TimeoutError) {
          return throwError(
            () =>
              new RequestTimeoutException({
                statusCode: 408,
                error: 'Request Timeout',
                message: `Request did not complete within ${timeoutMs}ms`,
              }),
          );
        }
        return throwError(() => error);
      }),
      finalize(() => this.shutdownCoordinator.endRequest()),
    );
  }

  private isHealthRequest(url: string): boolean {
    const path = url.split('?', 1)[0].replace(/\/+$/, '');
    return /\/health(?:\/ready)?$/.test(path);
  }
}
