/* eslint-disable @typescript-eslint/no-explicit-any */

import {service} from '@loopback/core';
import {isArray, isBoolean, isNumber, isObject, isString} from 'lodash';
import {EntityName, ObjectNode} from '../../models';
import {
  ApplicationService,
  CurrentContext,
  ObjectNodeService,
} from '../../services';
import {TransientEntityService} from '../../services/transient-entity/transient-entity.service';
import {ContentEncryptedObjectService} from '../content-encrypted-object/content-encrypted-object.definition';
import {ServerConfigService} from '../server-config/server-config.service';
import {LOGS_TYPE, LOG_PROVIDER} from './log.const';

const DEFAULT_NB_LOGS = 1000;
const LOG_LEVEL: string[] = ['ERROR', 'WARNING', 'INFO', 'DEBUG'];

export class LogService {
  constructor(
    @service(ApplicationService) private appCtx: ApplicationService,
    @service(TransientEntityService)
    private transientEntityService: TransientEntityService,
    @service(ServerConfigService)
    private serverConfigService: ServerConfigService,
    @service(ObjectNodeService) private objectNodeService: ObjectNodeService,
    @service(ContentEncryptedObjectService)
    private contentEncryptedObjectService: ContentEncryptedObjectService,
  ) {
    this.transientEntityService.registerTransientEntityTypeFunction(
      LOG_PROVIDER,
      LogService.name,
      'Add logs definition',
      EntityName.objectNode,
      LOGS_TYPE.name,
      this.completeLogsNode.bind(this),
    );

    this.serverConfigService.registerNewConfigurationKey('debug', {
      type: 'boolean',
      title: 'Active debug mode',
    });

    this.serverConfigService.registerNewConfigurationKey('nbLogs', {
      type: 'number',
      title: 'Number of kept logs',
      default: DEFAULT_NB_LOGS,
    });

    this.serverConfigService.registerNewConfigurationKey('logLevel', {
      title: 'Log level',
      type: 'string',
      oneOf: LOG_LEVEL.map((level) => ({
        enum: [level],
        title: level,
      })),
    });
  }

  private async completeLogsNode(objectNode: ObjectNode, ctx: CurrentContext) {
    if (
      objectNode.entityCtx?.jsonSchema?.properties?.contentEncryptedObject
        ?.properties?.value
    ) {
      // objectNode.entityCtx.jsonSchema.properties.contentEncryptedObject.properties.value.properties = this.properties;
    }
  }

  public async isDebug(
    level: 'WARNING' | 'ERROR' | 'DEBUG' | 'INFO',
  ): Promise<boolean> {
    const debug = !!(await this.serverConfigService.getConfig()).debug;
    if (!debug) return false;
    let logLevel = (await this.serverConfigService.getConfig()).logLevel;
    if (!logLevel) {
      logLevel = 'INFO';
    }
    return LOG_LEVEL.indexOf(level) <= LOG_LEVEL.indexOf(logLevel);
  }

  public addLogAsync(
    level: 'WARNING' | 'ERROR' | 'DEBUG' | 'INFO',
    source: string,
    action: string,
    message: string,
    ctx?: CurrentContext,
    object?: any,
  ): void {
    this.addLog(level, source, action, message, ctx, object)
      .then(() => {})
      .catch(() => {});
  }

  public async addLog(
    level: 'WARNING' | 'ERROR' | 'DEBUG' | 'INFO',
    source: string,
    action: string,
    message: string,
    ctx?: CurrentContext,
    object?: any,
  ) {
    if (!(await this.isDebug(level))) {
      return;
    }
    const logs = this.appCtx.getExtensionContext(LOG_PROVIDER).nodes.logs.value;
    if (!logs) {
      return;
    }
    const logsCtx = CurrentContext.get(ctx);
    const logsObj: ObjectNode = await this.objectNodeService.getNode(
      logs.id as string,
      logsCtx,
    );
    await this.contentEncryptedObjectService.addTransientContent(logsObj);
    if (!logsObj?.contentEncryptedObject) {
      logsObj.contentEncryptedObject = {};
    }
    if (!logsObj.contentEncryptedObject.value) {
      logsObj.contentEncryptedObject.value = {};
    }
    if (!logsObj.contentEncryptedObject.value.logs) {
      logsObj.contentEncryptedObject.value.logs = [];
    }
    const time = new Date().toISOString();
    logsObj.contentEncryptedObject.value.logs.unshift(
      `${time}  |  ${level}  |  ${source}  |  ${action}  |  ${message} ${
        object ? this.stringify(object) : ''
      }  |  memoryUsage: ${this.stringify(process.memoryUsage())}`,
    );

    let nbLogs = (await this.serverConfigService.getConfig()).nbLogs;
    if (!nbLogs) {
      nbLogs = DEFAULT_NB_LOGS;
    }
    while (logsObj.contentEncryptedObject.value.logs.length > nbLogs) {
      logsObj.contentEncryptedObject.value.logs.pop();
    }

    await this.objectNodeService.modifyById(
      logsObj.id as string,
      logsObj,
      logsCtx,
      true,
    );

    return;
  }

  private stringify(obj: any): string {
    if (isString(obj)) {
      return obj;
    }
    if (isBoolean(obj) || isNumber(obj)) {
      return JSON.stringify(obj);
    }
    if (isArray(obj)) {
      return (
        '[' +
        obj
          .map((val) => {
            return this.stringify(val);
          })
          .join(',') +
        ']'
      );
    }
    if (isObject(obj)) {
      return (
        '(' +
        Object.keys(obj)
          .map((key) => {
            return `${key} = ${this.stringify(
              (obj as {[k: string]: any})[key],
            )}`;
          })
          .join(', ') +
        ')'
      );
    }
    return JSON.stringify(obj);
  }
}
