// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only

import type { LibSignalErrorBase } from '@signalapp/libsignal-client';

import { parseRetryAfter } from '../util/parseRetryAfter.std.ts';
import type { ServiceIdString } from '../types/ServiceId.std.ts';
import type { HTTPError } from '../types/HTTPError.std.ts';
import type { HeaderListType } from '../types/WebAPI.d.ts';

import type { CallbackResultType } from './Types.d.ts';

function appendStack(newError: Error, originalError: Error) {
  // oxlint-disable-next-line no-param-reassign
  newError.stack += `\nOriginal stack:\n${originalError.stack}`;
}

class ReplayableError extends Error {
  functionCode?: number;

  constructor(options: {
    name?: string;
    message: string;
    functionCode?: number;
    cause?: unknown;
  }) {
    super(options.message, { cause: options.cause });

    this.name = options.name || 'ReplayableError';
    this.message = options.message;

    // Maintains proper stack trace, where our error was thrown (only available on V8)
    //   via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this);
    }

    this.functionCode = options.functionCode;
  }
}

// oxlint-disable-next-line max-classes-per-file
export class OutgoingIdentityKeyError extends ReplayableError {
  public readonly identifier: string;

  // Note: Data to resend message is no longer captured
  constructor(incomingIdentifier: string, cause?: LibSignalErrorBase) {
    const [identifier] = incomingIdentifier.split('.');

    super({
      name: 'OutgoingIdentityKeyError',
      message: `The identity of ${identifier} has changed.`,
      cause,
    });

    this.identifier = identifier;
  }
}

export class OutgoingMessageError extends ReplayableError {
  readonly identifier: string;

  readonly httpError?: HTTPError;

  // Note: Data to resend message is no longer captured
  constructor(
    incomingIdentifier: string,
    _m: unknown,
    _t: unknown,
    httpError?: HTTPError
  ) {
    const [identifier] = incomingIdentifier.split('.', 1);

    super({
      name: 'OutgoingMessageError',
      message: httpError ? httpError.message : 'no http error',
    });

    this.identifier = identifier;

    if (httpError) {
      this.httpError = httpError;
      appendStack(this, httpError);
    }
  }

  get code(): undefined | number {
    return this.httpError?.code;
  }
}

export class SendMessageNetworkError extends ReplayableError {
  readonly identifier: string;

  readonly httpError: HTTPError;

  constructor(identifier: string, _m: unknown, httpError: HTTPError) {
    super({
      name: 'SendMessageNetworkError',
      message: httpError.message,
    });

    const [id] = identifier.split('.', 1);
    this.identifier = id;
    this.httpError = httpError;

    appendStack(this, httpError);
  }

  get code(): number {
    return this.httpError.code;
  }

  get responseHeaders(): undefined | HeaderListType {
    return this.httpError.responseHeaders;
  }
}

export type SendMessageChallengeData = Readonly<{
  token?: string;
  options?: ReadonlyArray<string>;
}>;

export class SendMessageChallengeError extends ReplayableError {
  public identifier: string;

  public readonly httpError: HTTPError;

  public readonly data: SendMessageChallengeData | undefined;

  public readonly retryAt?: number;

  constructor(identifier: string, httpError: HTTPError) {
    super({
      name: 'SendMessageChallengeError',
      message: httpError.message,
      cause: httpError,
    });

    const [id] = identifier.split('.', 1);
    this.identifier = id;
    this.httpError = httpError;

    this.data = httpError.response as SendMessageChallengeData;

    const headers = httpError.responseHeaders || {};

    const retryAfter = parseRetryAfter(headers['retry-after']);
    if (retryAfter) {
      this.retryAt = Date.now() + retryAfter;
    }

    appendStack(this, httpError);
  }

  get code(): number {
    return this.httpError.code;
  }
}

export class SendMessageProtoError extends Error implements CallbackResultType {
  public readonly successfulServiceIds?: Array<ServiceIdString>;

  public readonly failoverServiceIds?: Array<ServiceIdString>;

  public readonly errors?: CallbackResultType['errors'];

  public readonly unidentifiedDeliveries?: Array<ServiceIdString>;

  public readonly dataMessage: Uint8Array<ArrayBuffer> | undefined;

  public readonly editMessage: Uint8Array<ArrayBuffer> | undefined;

  // Fields necessary for send log save
  public readonly contentHint?: number;

  public readonly contentProto?: Uint8Array<ArrayBuffer>;

  public readonly timestamp?: number;

  public readonly recipients?: Record<ServiceIdString, Array<number>>;

  public readonly sendIsNotFinal?: boolean;

  constructor({
    successfulServiceIds,
    failoverServiceIds,
    errors,
    unidentifiedDeliveries,
    dataMessage,
    editMessage,
    contentHint,
    contentProto,
    timestamp,
    recipients,
    sendIsNotFinal,
  }: CallbackResultType) {
    super(`SendMessageProtoError: ${SendMessageProtoError.getMessage(errors)}`);

    this.successfulServiceIds = successfulServiceIds;
    this.failoverServiceIds = failoverServiceIds;
    this.errors = errors;
    this.unidentifiedDeliveries = unidentifiedDeliveries;
    this.dataMessage = dataMessage;
    this.editMessage = editMessage;
    this.contentHint = contentHint;
    this.contentProto = contentProto;
    this.timestamp = timestamp;
    this.recipients = recipients;
    this.sendIsNotFinal = sendIsNotFinal;
  }

  protected static getMessage(errors: CallbackResultType['errors']): string {
    if (!errors) {
      return 'No errors';
    }

    return errors.map(error => error.toString()).join(', ');
  }
}

/** @testexport */
export class MessageError extends ReplayableError {
  readonly httpError: HTTPError;

  constructor(_m: unknown, httpError: HTTPError) {
    super({
      name: 'MessageError',
      message: httpError.message,
    });

    this.httpError = httpError;

    appendStack(this, httpError);
  }

  get code(): number {
    return this.httpError.code;
  }
}

export class UnregisteredUserError extends Error {
  readonly serviceId: string;

  readonly httpError: HTTPError;

  constructor(serviceId: ServiceIdString, httpError: HTTPError) {
    const { message } = httpError;

    super(message);

    this.message = message;
    this.name = 'UnregisteredUserError';

    // Maintains proper stack trace, where our error was thrown (only available on V8)
    //   via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this);
    }

    this.serviceId = serviceId;
    this.httpError = httpError;

    appendStack(this, httpError);
  }

  get code(): number {
    return this.httpError.code;
  }
}

export class ConnectTimeoutError extends Error {}

export class UnknownRecipientError extends Error {}

export class IncorrectSenderKeyAuthError extends Error {}

export class WarnOnlyError extends Error {}

export class NoSenderKeyError extends Error {}
