import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { PinoLogger } from 'nestjs-pino';
import { BaseMeta, StandardResponse } from 'src/common/types/response.types';
import type { Request, Response } from 'express';
import type { IncomingHttpHeaders } from 'http';

@Injectable()
export class ResponseInterceptor implements NestInterceptor {
  constructor(private readonly logger: PinoLogger) {
    this.logger.setContext(ResponseInterceptor.name);
  }

  intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Observable<StandardResponse> {
    const startTime = Date.now();
    const request = context.switchToHttp().getRequest<Request>();
    const response = context.switchToHttp().getResponse<Response>();
    const route = this.resolveRoute(request);

    const baseMeta: BaseMeta = {
      requestId: request.requestId,
      timestamp: new Date().toISOString(),
      path: request.url,
      method: request.method,
      statusCode: response.statusCode,
    };

    return next.handle().pipe(
      tap(() => {
        baseMeta.duration = Date.now() - startTime;
        baseMeta.statusCode = response.statusCode;

        const responseSize = this.parseNumber(response.get('content-length'));
        const logData: Record<string, unknown> = {
          event: 'http.request',
          requestId: request.requestId,
          method: request.method,
          route,
          statusCode: response.statusCode,
          latencyMs: baseMeta.duration,
          clientIp: request.clientIp,
          userAgent: request.header('user-agent') ?? '',
          referer: request.header('referer') ?? '',
        };
        if (responseSize !== undefined) {
          logData.responseSize = responseSize;
        }

        this.logger.info(logData, 'http request');

        const query = this.sanitizeQuery(request.query);
        if (query) {
          this.logger.debug(
            {
              event: 'http.request.debug',
              requestId: request.requestId,
              route,
              query,
              headers: this.pickHeaders(request.headers),
            },
            'http request debug',
          );
        }
      }),
      map((result) => this.transformResponse(result, baseMeta)),
    );
  }

  private transformResponse(
    response: unknown,
    baseMeta: BaseMeta,
  ): StandardResponse {
    return {
      success: true,
      data: response ?? null,
      meta: baseMeta,
    };
  }

  private resolveRoute(request: {
    route?: { path?: string };
    baseUrl?: string;
    path?: string;
    url?: string;
  }): string {
    const routePath = request.route?.path;
    if (typeof routePath === 'string') {
      const baseUrl = request.baseUrl ?? '';
      const combined = `${baseUrl}${routePath}`.replace(/\/{2,}/g, '/');
      return combined.startsWith('/') ? combined : `/${combined}`;
    }

    const path = request.path ?? request.url;
    if (typeof path === 'string') {
      return path.split('?')[0] ?? path;
    }

    return '';
  }

  private parseNumber(value: unknown): number | undefined {
    if (typeof value === 'number' && Number.isFinite(value)) return value;
    if (typeof value === 'string') {
      const parsed = Number(value);
      return Number.isFinite(parsed) ? parsed : undefined;
    }
    return undefined;
  }

  private sanitizeQuery(query: unknown): Record<string, unknown> | undefined {
    if (!this.isRecord(query)) return undefined;
    const entries = Object.entries(query).filter(
      ([, value]) => value !== undefined,
    );
    if (entries.length === 0) return undefined;
    return Object.fromEntries(entries);
  }

  private isRecord(value: unknown): value is Record<string, unknown> {
    return typeof value === 'object' && value !== null && !Array.isArray(value);
  }

  private pickHeaders(headers: IncomingHttpHeaders): Record<string, string> {
    const whitelist = ['user-agent', 'referer', 'origin', 'content-type'];
    const picked: Record<string, string> = {};

    for (const key of whitelist) {
      const value = headers[key];
      if (typeof value === 'string' && value.trim().length > 0) {
        picked[key] = value;
      }
    }

    return picked;
  }
}
