import {
  ApolloError,
  ApolloLink,
  FetchResult,
  NextLink,
  Operation,
  ServerError,
} from '@apollo/client/core';
import { Severity } from '@sentry/browser';
import Observable from 'zen-observable';

import { GraphQLBreadcrumb, makeBreadcrumb } from './breadcrumb';
import { FullOptions, SentryLinkOptions, withDefaults } from './options';
import {
  attachBreadcrumbToSentry,
  setFingerprint,
  setTransaction,
} from './sentry';

export class SentryLink extends ApolloLink {
  private readonly options: FullOptions;

  constructor(options: SentryLinkOptions = {}) {
    super();
    this.options = withDefaults(options);
  }

  request(
    operation: Operation,
    forward: NextLink,
  ): Observable<FetchResult> | null {
    const options = this.options;

    if (!(options.shouldHandleOperation?.(operation) ?? true)) {
      return forward(operation);
    }

    if (options.setTransaction) {
      setTransaction(operation);
    }

    if (options.setFingerprint) {
      setFingerprint(operation);
    }

    const attachBreadcrumbs = options.attachBreadcrumbs;
    const breadcrumb = attachBreadcrumbs
      ? makeBreadcrumb(operation, options)
      : undefined;

    // While this could be done more simplistically by simply subscribing,
    // wrapping the observer in our own observer ensures we get the results
    // before they are passed along to other observers. This guarantees we
    // get to run our instrumentation before others observers potentially
    // throw and thus flush the results to Sentry.
    return new Observable<FetchResult>((originalObserver) => {
      const subscription = forward(operation).subscribe({
        next: (result) => {
          if (attachBreadcrumbs) {
            // We must have a breadcrumb if attachBreadcrumbs was set
            (breadcrumb as GraphQLBreadcrumb).level = severityForResult(result);

            if (attachBreadcrumbs.includeFetchResult) {
              // We must have a breadcrumb if attachBreadcrumbs was set
              (breadcrumb as GraphQLBreadcrumb).data.fetchResult = result;
            }

            if (
              attachBreadcrumbs.includeError &&
              result.errors &&
              result.errors.length > 0
            ) {
              // We must have a breadcrumb if attachBreadcrumbs was set
              (breadcrumb as GraphQLBreadcrumb).data.error = new ApolloError({
                graphQLErrors: result.errors,
              });
            }
          }

          originalObserver.next(result);
        },
        complete: () => {
          if (attachBreadcrumbs) {
            attachBreadcrumbToSentry(
              operation,
              // We must have a breadcrumb if attachBreadcrumbs was set
              breadcrumb as GraphQLBreadcrumb,
              options,
            );
          }

          originalObserver.complete();
        },
        error: (error) => {
          if (attachBreadcrumbs) {
            // We must have a breadcrumb if attachBreadcrumbs was set
            (breadcrumb as GraphQLBreadcrumb).level = Severity.Error;

            let scrubbedError;
            if (isServerError(error)) {
              const { result, response, ...rest } = error;
              scrubbedError = rest;

              if (attachBreadcrumbs.includeFetchResult) {
                // We must have a breadcrumb if attachBreadcrumbs was set
                (breadcrumb as GraphQLBreadcrumb).data.fetchResult = result;
              }
            } else {
              scrubbedError = error;
            }

            if (attachBreadcrumbs.includeError) {
              // We must have a breadcrumb if attachBreadcrumbs was set
              (breadcrumb as GraphQLBreadcrumb).data.error = scrubbedError;
            }

            attachBreadcrumbToSentry(
              operation,
              // We must have a breadcrumb if attachBreadcrumbs was set
              breadcrumb as GraphQLBreadcrumb,
              options,
            );
          }

          originalObserver.error(error);
        },
      });

      return () => {
        subscription.unsubscribe();
      };
    });
  }
}

function isServerError(error: unknown): error is ServerError {
  return (
    typeof error === 'object' &&
    error !== null &&
    'response' in error &&
    'result' in error &&
    'statusCode' in error
  );
}

function severityForResult(result: FetchResult): Severity {
  return result.errors && result.errors.length > 0
    ? Severity.Error
    : Severity.Info;
}