import { AbstractLogger } from 'tachyon-logger/abstractLogger';
import { consoleLogger } from 'tachyon-logger/consoleLogger';
import { LogLevel, LogMessage } from 'tachyon-logger/logger';
import { LoggerMiddleware } from 'tachyon-logger/loggerMiddleware';

const MAX_ATTEMPTS = 3;
const CLIENT_ERROR = 'client_error';
export const SENTRY_PLATFORM = 'mobile_web';
export const SENTRY_PRODUCT = 'mobile_web';
export const SENTINEL_ENDPOINT = 'https://sentinel.twitchmw.net/error-report';

/**
 * Error objects stored inside each Sentinel log entry.
 */
interface SentinelLogEntryError {
  readonly message: string;
  readonly name?: string;
  readonly stack?: string;
  readonly type?: number;
}

/**
 * Log entries stored inside each Sentinel report.
 */
interface SentinelLogEntry {
  readonly category: string;
  readonly errors: SentinelLogEntryError[];
  readonly message: string;
  readonly level: number;
  readonly time: number;
  readonly args?: any[];
}

/**
 * Interface for reporting errors to Sentinel.
 */
interface SentinelErrorReport {
  readonly buildID: string;
  readonly clientTime: number;
  readonly deviceID: string;
  readonly location?: string;
  readonly logEntries: SentinelLogEntry[];
  readonly pageComponentName: string;
  readonly pageSessionID: string;
  readonly platform: string;
  readonly product: string;
  readonly userAgent: string;
  readonly userID: number;
  readonly username: string;
}

/**
 * Attributes that will be added to the sentinel report.
 */
export interface SentinelReportOptions {
  readonly buildID: string;
  readonly pageSessionID?: string;
  readonly pageComponentName?: string;
  readonly deviceID: string;
  readonly minimumLevel?: LogLevel;
}

/**
 * Options that can be used to configure the SentinelLogger. This includes data
 * to be added to each report sent to Sentinel.
 */
export interface SentinelOpts extends SentinelReportOptions {
  readonly middleware?: LoggerMiddleware[];
}

/**
 * A logger that outputs all logs to the console.
 */
export class SentinelLogger extends AbstractLogger {
  private sentinelReportOptions: SentinelReportOptions;

  /**
   * @param opts Options to be given to the SentinelLogger that will be used
   *             when creating reports for Sentinel.
   */
  constructor(opts: SentinelOpts) {
    const { middleware, ...sentinelReportOptions } = opts;
    super(middleware || []);
    this.sentinelReportOptions = sentinelReportOptions;
  }

  public debug(message: LogMessage): void {
    this.report(message, LogLevel.DEBUG);
  }

  public log(message: LogMessage): void {
    this.report(message, LogLevel.DEBUG);
  }

  public info(message: LogMessage): void {
    this.report(message, LogLevel.INFO);
  }

  public warn(message: LogMessage): void {
    this.report(message, LogLevel.WARNING);
  }

  public error(message: LogMessage): void {
    this.report(message, LogLevel.ERROR);
  }

  /**
   * Create a report object to be sent to Sentinel.
   *
   * @param message The LogMessage to be reported.
   * @param level The log level to use in creating a Sentinel log entry.
   */
  private buildReport(
    message: LogMessage,
    level: LogLevel,
  ): SentinelErrorReport {
    return {
      buildID: this.sentinelReportOptions.buildID,
      clientTime: Date.now(),
      deviceID: this.sentinelReportOptions.deviceID,
      location:
        typeof window !== 'undefined' ? window.location.href : undefined,
      logEntries: [this.getSentinelLogEntry(message, level)],
      pageComponentName: this.sentinelReportOptions.pageComponentName || '',
      pageSessionID: this.sentinelReportOptions.pageSessionID || '',
      platform: SENTRY_PLATFORM,
      product: SENTRY_PRODUCT,
      userAgent: navigator.userAgent,
      userID: 0, // Explicitly set to 0 to maintain security level.
      username: '', // Explicitly blank to maintain security level.
    };
  }

  /**
   * Create a Sentinel log entry for the report.
   *
   * @param message The LogMessage to be reported.
   * @param level The log level to use in creating a Sentinel log entry.
   */
  private getSentinelLogEntry(
    message: LogMessage,
    level: LogLevel,
  ): SentinelLogEntry {
    const logObject = this.getLogObject(level, message);
    const messageStr = JSON.stringify(logObject.message || '');
    return {
      args: [logObject],
      category: CLIENT_ERROR,
      errors: [
        {
          message: messageStr,
        },
      ],
      level: Object.keys(LogLevel).indexOf(level),
      message: messageStr,
      time: logObject.time || Date.now(),
    };
  }

  /**
   * Send an error report to Sentinel.
   *
   * @param message The LogMessage to be reported.
   * @param level The log level to use in creating a Sentinel log entry.
   */
  private async report(message: LogMessage, level: LogLevel): Promise<void> {
    const logLevels = Object.keys(LogLevel);
    if (
      logLevels.indexOf(level) <
      logLevels.indexOf(
        this.sentinelReportOptions.minimumLevel || LogLevel.ERROR,
      )
    ) {
      return;
    }
    const report = this.buildReport(message, level);
    consoleLogger.debug({ message: 'Reporting error.', report });
    for (let attempt = 1; attempt <= MAX_ATTEMPTS; ++attempt) {
      try {
        await fetch(SENTINEL_ENDPOINT, {
          body: JSON.stringify(report),
          headers: {
            Accept: 'application/json; charset=UTF-8',
            'Content-Type': 'application/json; charset=UTF-8',
          },
          method: 'POST',
        });
        consoleLogger.debug('Report sent.');
        return;
      } catch (err) {
        consoleLogger.warn({
          attempt,
          err,
          maxAttempts: MAX_ATTEMPTS,
          message: 'Failed to submit error report.',
        });
      }
    }
  }
}
