import { Parse } from 'parse/node';
import AdaptableController from './AdaptableController';
import { LoggerAdapter } from '../Adapters/Logger/LoggerAdapter';

const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;
const LOG_STRING_TRUNCATE_LENGTH = 1000;
const truncationMarker = '... (truncated)';

export const LogLevel = {
  INFO: 'info',
  ERROR: 'error',
};

export const LogOrder = {
  DESCENDING: 'desc',
  ASCENDING: 'asc',
};

export const logLevels = ['error', 'warn', 'info', 'debug', 'verbose', 'silly', 'silent'];

export class LoggerController extends AdaptableController {
  constructor(adapter, appId, options = { logLevel: 'info' }) {
    super(adapter, appId, options);
    let level = 'info';
    if (options.verbose) {
      level = 'verbose';
    }
    if (options.logLevel) {
      level = options.logLevel;
    }
    const index = logLevels.indexOf(level); // info by default
    logLevels.forEach((level, levelIndex) => {
      if (levelIndex > index) {
        // silence the levels that are > maxIndex
        this[level] = () => {};
      }
    });
  }

  maskSensitiveUrl(path) {
    const urlString = 'http://localhost' + path; // prepend dummy string to make a real URL
    const urlObj = new URL(urlString);
    const query = urlObj.searchParams;
    let sanitizedQuery = '?';

    for (const [key, value] of query) {
      if (key !== 'password') {
        // normal value
        sanitizedQuery += key + '=' + value + '&';
      } else {
        // password value, redact it
        sanitizedQuery += key + '=' + '********' + '&';
      }
    }

    // trim last character, ? or &
    sanitizedQuery = sanitizedQuery.slice(0, -1);

    // return original path name with sanitized params attached
    return urlObj.pathname + sanitizedQuery;
  }

  maskSensitive(argArray) {
    return argArray.map(e => {
      if (!e) {
        return e;
      }

      if (typeof e === 'string') {
        return e.replace(/(password".?:.?")[^"]*"/g, '$1********"');
      }
      // else it is an object...

      // check the url
      if (e.url) {
        // for strings
        if (typeof e.url === 'string') {
          e.url = this.maskSensitiveUrl(e.url);
        } else if (Array.isArray(e.url)) {
          // for strings in array
          e.url = e.url.map(item => {
            if (typeof item === 'string') {
              return this.maskSensitiveUrl(item);
            }

            return item;
          });
        }
      }

      if (e.body) {
        for (const key of Object.keys(e.body)) {
          if (key === 'password') {
            e.body[key] = '********';
            break;
          }
        }
      }

      if (e.params) {
        for (const key of Object.keys(e.params)) {
          if (key === 'password') {
            e.params[key] = '********';
            break;
          }
        }
      }

      return e;
    });
  }

  log(level, args) {
    // make the passed in arguments object an array with the spread operator
    args = this.maskSensitive([...args]);
    args = [].concat(
      level,
      args.map(arg => {
        if (typeof arg === 'function') {
          return arg();
        }
        return arg;
      })
    );
    this.adapter.log.apply(this.adapter, args);
  }

  info() {
    return this.log('info', arguments);
  }

  error() {
    return this.log('error', arguments);
  }

  warn() {
    return this.log('warn', arguments);
  }

  verbose() {
    return this.log('verbose', arguments);
  }

  debug() {
    return this.log('debug', arguments);
  }

  silly() {
    return this.log('silly', arguments);
  }

  logRequest({ method, url, headers, body }) {
    this.verbose(
      () => {
        const stringifiedBody = JSON.stringify(body, null, 2);
        return `REQUEST for [${method}] ${url}: ${stringifiedBody}`;
      },
      {
        method,
        url,
        headers,
        body,
      }
    );
  }

  logResponse({ method, url, result }) {
    this.verbose(
      () => {
        const stringifiedResponse = JSON.stringify(result, null, 2);
        return `RESPONSE from [${method}] ${url}: ${stringifiedResponse}`;
      },
      { result: result }
    );
  }
  // check that date input is valid
  static validDateTime(date) {
    if (!date) {
      return null;
    }
    date = new Date(date);

    if (!isNaN(date.getTime())) {
      return date;
    }

    return null;
  }

  truncateLogMessage(string) {
    if (string && string.length > LOG_STRING_TRUNCATE_LENGTH) {
      const truncated = string.substring(0, LOG_STRING_TRUNCATE_LENGTH) + truncationMarker;
      return truncated;
    }

    return string;
  }

  static parseOptions(options = {}) {
    const from =
      LoggerController.validDateTime(options.from) ||
      new Date(Date.now() - 7 * MILLISECONDS_IN_A_DAY);
    const until = LoggerController.validDateTime(options.until) || new Date();
    const size = Number(options.size) || 10;
    const order = options.order || LogOrder.DESCENDING;
    const level = options.level || LogLevel.INFO;

    return {
      from,
      until,
      size,
      order,
      level,
    };
  }

  // Returns a promise for a {response} object.
  // query params:
  // level (optional) Level of logging you want to query for (info || error)
  // from (optional) Start time for the search. Defaults to 1 week ago.
  // until (optional) End time for the search. Defaults to current time.
  // order (optional) Direction of results returned, either “asc” or “desc”. Defaults to “desc”.
  // size (optional) Number of rows returned by search. Defaults to 10
  getLogs(options = {}) {
    if (!this.adapter) {
      throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'Logger adapter is not available');
    }
    if (typeof this.adapter.query !== 'function') {
      throw new Parse.Error(
        Parse.Error.PUSH_MISCONFIGURED,
        'Querying logs is not supported with this adapter'
      );
    }
    options = LoggerController.parseOptions(options);
    return this.adapter.query(options);
  }

  expectedAdapterType() {
    return LoggerAdapter;
  }
}

export default LoggerController;
