import { DisconnectReason, RequestResponse_Reason } from '@livekit/protocol';

/** Base error that all LiveKit specific custom errors inherit from. */
export class LivekitError extends Error {
  code: number;

  // More info: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
  cause?: unknown;

  constructor(code: number, message?: string, options?: { cause?: unknown }) {
    super(message || 'an error has occurred');
    this.name = 'LiveKitError';
    this.code = code;

    if (typeof options?.cause !== 'undefined') {
      this.cause = options?.cause;
    }
  }
}

/**
 * LiveKit specific error type representing an error with an associated set of reasons.
 * Use this to represent an error with multiple different but contextually related variants.
 * */
export abstract class LivekitReasonedError<Reason> extends LivekitError {
  abstract reason: Reason;

  abstract reasonName: string;
}

export class SimulatedError extends LivekitError {
  readonly name = 'simulated';

  constructor(message = 'Simulated failure') {
    super(-1, message);
  }
}

export enum ConnectionErrorReason {
  NotAllowed,
  ServerUnreachable,
  InternalError,
  Cancelled,
  LeaveRequest,
  Timeout,
  WebSocket,
  ServiceNotFound,
}

type NotAllowed = {
  reason: ConnectionErrorReason.NotAllowed;
  status: number;
  context?: unknown;
};

type InternalError = {
  reason: ConnectionErrorReason.InternalError;
  status: never;
  context?: { status?: number; statusText?: string };
};

type ConnectionTimeout = {
  reason: ConnectionErrorReason.Timeout;
  status: never;
  context: never;
};

type LeaveRequest = {
  reason: ConnectionErrorReason.LeaveRequest;
  status: never;
  context: DisconnectReason;
};

type Cancelled = {
  reason: ConnectionErrorReason.Cancelled;
  status: never;
  context: never;
};

type ServerUnreachable = {
  reason: ConnectionErrorReason.ServerUnreachable;
  status?: number;
  context?: never;
};

type WebSocket = {
  reason: ConnectionErrorReason.WebSocket;
  status?: number;
  context?: string;
};

type ServiceNotFound = {
  reason: ConnectionErrorReason.ServiceNotFound;
  status: never;
  context: string;
};

type ConnectionErrorVariants =
  | NotAllowed
  | ConnectionTimeout
  | LeaveRequest
  | InternalError
  | Cancelled
  | ServerUnreachable
  | WebSocket
  | ServiceNotFound;

export class ConnectionError<
  Variant extends ConnectionErrorVariants = ConnectionErrorVariants,
> extends LivekitReasonedError<Variant['reason']> {
  status?: Variant['status'];

  context: Variant['context'];

  reason: Variant['reason'];

  reasonName: string;

  readonly name = 'ConnectionError';

  protected constructor(
    message: string,
    reason: Variant['reason'],
    status?: Variant['status'],
    context?: Variant['context'],
  ) {
    super(1, message);
    this.status = status;
    this.reason = reason;
    this.context = context;
    this.reasonName = ConnectionErrorReason[reason];
  }

  static notAllowed(message: string, status: number, context?: unknown) {
    return new ConnectionError<NotAllowed>(
      message,
      ConnectionErrorReason.NotAllowed,
      status,
      context,
    );
  }

  static timeout(message: string) {
    return new ConnectionError<ConnectionTimeout>(message, ConnectionErrorReason.Timeout);
  }

  static leaveRequest(message: string, context: DisconnectReason) {
    return new ConnectionError<LeaveRequest>(
      message,
      ConnectionErrorReason.LeaveRequest,
      undefined,
      context,
    );
  }

  static internal(message: string, context?: { status?: number; statusText?: string }) {
    return new ConnectionError<InternalError>(
      message,
      ConnectionErrorReason.InternalError,
      undefined,
      context,
    );
  }

  static cancelled(message: string) {
    return new ConnectionError<Cancelled>(message, ConnectionErrorReason.Cancelled);
  }

  static serverUnreachable(message: string, status?: number) {
    return new ConnectionError<ServerUnreachable>(
      message,
      ConnectionErrorReason.ServerUnreachable,
      status,
    );
  }

  static websocket(message: string, status?: number, reason?: string) {
    return new ConnectionError<WebSocket>(message, ConnectionErrorReason.WebSocket, status, reason);
  }

  static serviceNotFound(message: string, serviceName: 'v0-rtc') {
    return new ConnectionError<ServiceNotFound>(
      message,
      ConnectionErrorReason.ServiceNotFound,
      undefined,
      serviceName,
    );
  }
}

export class DeviceUnsupportedError extends LivekitError {
  readonly name = 'DeviceUnsupportedError';

  constructor(message?: string) {
    super(21, message ?? 'device is unsupported');
  }
}

export class TrackInvalidError extends LivekitError {
  readonly name = 'TrackInvalidError';

  constructor(message?: string) {
    super(20, message ?? 'track is invalid');
  }
}

export class UnsupportedServer extends LivekitError {
  readonly name = 'UnsupportedServer';

  constructor(message?: string) {
    super(10, message ?? 'unsupported server');
  }
}

export class UnexpectedConnectionState extends LivekitError {
  readonly name = 'UnexpectedConnectionState';

  constructor(message?: string) {
    super(12, message ?? 'unexpected connection state');
  }
}

export class NegotiationError extends LivekitError {
  readonly name = 'NegotiationError';

  constructor(message?: string) {
    super(13, message ?? 'unable to negotiate');
  }
}

export class PublishDataError extends LivekitError {
  readonly name = 'PublishDataError';

  constructor(message?: string) {
    super(14, message ?? 'unable to publish data');
  }
}

export class PublishTrackError extends LivekitError {
  readonly name = 'PublishTrackError';

  status: number;

  constructor(message: string, status: number) {
    super(15, message);
    this.status = status;
  }
}

export type RequestErrorReason =
  | Exclude<RequestResponse_Reason, RequestResponse_Reason.OK>
  | 'TimeoutError';

export class SignalRequestError extends LivekitReasonedError<RequestErrorReason> {
  readonly name = 'SignalRequestError';

  reason: RequestErrorReason;

  reasonName: string;

  constructor(message: string, reason: RequestErrorReason) {
    super(15, message);
    this.reason = reason;
    this.reasonName = typeof reason === 'string' ? reason : RequestResponse_Reason[reason];
  }
}

// NOTE: matches with https://github.com/livekit/client-sdk-swift/blob/f37bbd260d61e165084962db822c79f995f1a113/Sources/LiveKit/DataStream/StreamError.swift#L17
export enum DataStreamErrorReason {
  // Unable to open a stream with the same ID more than once.
  AlreadyOpened = 0,

  // Stream closed abnormally by remote participant.
  AbnormalEnd = 1,

  // Incoming chunk data could not be decoded.
  DecodeFailed = 2,

  // Read length exceeded total length specified in stream header.
  LengthExceeded = 3,

  // Read length less than total length specified in stream header.
  Incomplete = 4,

  // Unable to register a stream handler more than once.
  HandlerAlreadyRegistered = 7,

  // Encryption type mismatch.
  EncryptionTypeMismatch = 8,
}

export class DataStreamError extends LivekitReasonedError<DataStreamErrorReason> {
  readonly name = 'DataStreamError';

  reason: DataStreamErrorReason;

  reasonName: string;

  constructor(message: string, reason: DataStreamErrorReason) {
    super(16, message);
    this.reason = reason;
    this.reasonName = DataStreamErrorReason[reason];
  }
}

export class SignalReconnectError extends LivekitError {
  readonly name = 'SignalReconnectError';

  constructor(message?: string) {
    super(18, message);
  }
}

export enum MediaDeviceFailure {
  // user rejected permissions
  PermissionDenied = 'PermissionDenied',
  // device is not available
  NotFound = 'NotFound',
  // device is in use. On Windows, only a single tab may get access to a device at a time.
  DeviceInUse = 'DeviceInUse',
  Other = 'Other',
}

export namespace MediaDeviceFailure {
  export function getFailure(error: any): MediaDeviceFailure | undefined {
    if (error && 'name' in error) {
      if (error.name === 'NotFoundError' || error.name === 'DevicesNotFoundError') {
        return MediaDeviceFailure.NotFound;
      }
      if (error.name === 'NotAllowedError' || error.name === 'PermissionDeniedError') {
        return MediaDeviceFailure.PermissionDenied;
      }
      if (error.name === 'NotReadableError' || error.name === 'TrackStartError') {
        return MediaDeviceFailure.DeviceInUse;
      }
      return MediaDeviceFailure.Other;
    }
  }
}
