import { ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
  HttpStatus } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { CustomLogger } from '../logger/custom-logger';
import { StatusException } from './status-exception';
import { CoffeeError } from '../domain/error.interface';

const UNKNOWN_ERROR_MESSAGE = 'Internal server error';
const UNKNOWN_ERROR_TYPE = 'unknown_error_type';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {

  private readonly logger = new CustomLogger(AllExceptionsFilter.name);
  private static statusCodeRegex = /^[1-5][0-9][0-9]$/;

  private static getStatusFromMessage(message: string): number {
    const status = message.replace(/(^\d+)(.+$)/i, '$1');
    const defaultStatus = HttpStatus.INTERNAL_SERVER_ERROR;
    if (!this.isStatusCode(status)) {
      return defaultStatus;
    }
    return parseInt(status, 10) || defaultStatus;
  }

  private static isStatusCode(status: string): boolean {
    return this.statusCodeRegex.test(status);
  }

  private static removeStatusFromMessage(
    statusCode: number,
    message: string
  ): string {
    const statusCodeMessage = statusCode.toString();
    const index = message.indexOf(statusCodeMessage);
    if (index < 0) {
      return message;
    }
    return message.substring(index + statusCodeMessage.length).trim();
  }

  public catch(exception: any, host: ArgumentsHost): void | Observable<any> {
    if (typeof exception === 'object') {
      this.logger.error(
        `Caught exception ${JSON.stringify(exception, null, 2)}`
      );
    } else {
      this.logger.error(`Caught exception ${exception}`);
    }

    const ctx = host.switchToHttp();
    const type = host.getType();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

    let requestInfo;
    const date = new Date();
    if (type === 'http') {
      requestInfo = `Type: HTTPS, IP: ${request.ip}. User Agent: ${request.headers['user-agent']}`;
    } else if (type === 'rpc') {
      requestInfo = 'Type: RPC';
    }
    console.log(date.toISOString(), ' - UnhandledError caught in coffee-core:', exception, 'Request info:', requestInfo);

    if (exception instanceof HttpException) {
      const exceptionStatusCode = exception.getStatus();
      let resultMessage = exception.message;
      // keep string array notation to overcome 'response' being a private field
      let errorType = exception['response'].error || UNKNOWN_ERROR_TYPE;

      const objectResponse = exception.getResponse() as Record<string, unknown>;
      const objectResponseMessage = objectResponse?.message;
      if (objectResponseMessage) {
        if (Array.isArray(objectResponseMessage)) {
          resultMessage = objectResponseMessage[0];
        } else {
          resultMessage = objectResponseMessage as string;
        }
      }

      return this.handleExceptionResponse(response, {
        code: exceptionStatusCode,
        message: resultMessage || '',
        type: errorType
      });
    }
    if (exception instanceof StatusException) {
      return this.handleExceptionResponse(response, {
        code: exception.status || HttpStatus.INTERNAL_SERVER_ERROR,
        message: exception.message || UNKNOWN_ERROR_MESSAGE,
        type: exception.type || UNKNOWN_ERROR_TYPE
      });
    }

    return this.handleExceptionResponse(response, this.handleUnknownException(exception));
  }

  public handleUnknownException(exception: any): CoffeeError {
    const message = typeof exception === 'string' ? exception : (exception as any).message;
    if (message) {
      const statusCode = AllExceptionsFilter.getStatusFromMessage(message) || HttpStatus.INTERNAL_SERVER_ERROR;
      const newMessage = AllExceptionsFilter.removeStatusFromMessage(statusCode, message);
      return {
        code: statusCode,
        message: newMessage,
        type: UNKNOWN_ERROR_TYPE
      };
    }
    return {
      code: HttpStatus.INTERNAL_SERVER_ERROR,
      message: UNKNOWN_ERROR_MESSAGE,
      type: UNKNOWN_ERROR_TYPE
    };
  }

  private handleExceptionResponse(response: any, error: CoffeeError): void | Observable<any> {
    if (!response.status) {
      return throwError(() => `${error.code} ${error.message}`);
    }

    response.status(error.code).json({
      ...error,
      type: this.formatErrorType(error.type)
    });
  }

  private formatErrorType(type: string): string {
    return type.trim().toLowerCase().replaceAll(' ', '_');
  }
}
