import { GraphQLError } from 'graphql';
import retry from 'async-retry';
import { AbortController } from "node-abort-controller";
import { SupergraphSdlUpdate } from '../../config';
import { SupergraphSdlQuery } from '../../__generated__/graphqlTypes';
import type {
  FetcherResponse,
  Fetcher,
  FetcherRequestInit,
} from '@apollo/utils.fetcher';
import type { Logger } from '@apollo/utils.logger';

// Magic /* GraphQL */ comment below is for codegen, do not remove
export const SUPERGRAPH_SDL_QUERY = /* GraphQL */`#graphql
  query SupergraphSdl($apiKey: String!, $ref: String!, $ifAfterId: ID) {
    routerConfig(ref: $ref, apiKey: $apiKey, ifAfterId: $ifAfterId) {
      __typename
      ... on RouterConfigResult {
        id
        supergraphSdl: supergraphSDL
        minDelaySeconds
      }
      ... on FetchError {
        code
        message
      }
    }
  }
`;


type SupergraphSdlQueryResult =
  | SupergraphSdlQuerySuccess
  | SupergraphSdlQueryFailure;

interface SupergraphSdlQuerySuccess {
  data: SupergraphSdlQuery;
}

interface SupergraphSdlQueryFailure {
  data?: SupergraphSdlQuery;
  errors: GraphQLError[];
}

const { name, version } = require('../../../package.json');

const fetchErrorMsg = "An error occurred while fetching your schema from Apollo: ";

export class UplinkFetcherError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'UplinkFetcherError';
  }
}

export async function loadSupergraphSdlFromUplinks({
  graphRef,
  apiKey,
  endpoints,
  fetcher,
  compositionId,
  maxRetries,
  requestTimeoutMs,
  roundRobinSeed,
  logger,
}: {
  graphRef: string;
  apiKey: string;
  endpoints: string[];
  fetcher: Fetcher;
  compositionId: string | null;
  maxRetries: number,
  requestTimeoutMs: number,
  roundRobinSeed: number,
  logger: Logger,
}) : Promise<SupergraphSdlUpdate | null> {
  // This Promise resolves with either an updated supergraph or null if no change.
  // This Promise can reject in the case that none of the retries are successful,
  // in which case it will reject with the most frequently encountered error.
  return retry(
    () =>
      loadSupergraphSdlFromStorage({
        graphRef,
        apiKey,
        endpoint: endpoints[roundRobinSeed++ % endpoints.length],
        fetcher,
        requestTimeoutMs,
        compositionId,
        logger,
      }),
    {
      retries: maxRetries,
      maxTimeout: 60_000,
      onRetry(e, attempt) {
        logger.debug(`Unable to fetch supergraph SDL (attempt ${attempt}), waiting before retry: ${e}`);
      },
    },
  );
}

export async function loadSupergraphSdlFromStorage({
  graphRef,
  apiKey,
  endpoint,
  fetcher,
  requestTimeoutMs,
  compositionId,
  logger,
}: {
  graphRef: string;
  apiKey: string;
  endpoint: string;
  fetcher: Fetcher;
  requestTimeoutMs: number;
  compositionId: string | null;
  logger: Logger;
}) : Promise<SupergraphSdlUpdate | null> {
  const requestBody = JSON.stringify({
    query: SUPERGRAPH_SDL_QUERY,
    variables: {
      ref: graphRef,
      apiKey,
      ifAfterId: compositionId,
    },
  })

  const controller = new AbortController();
  const signal = setTimeout(() => {
    logger.debug(`Aborting request due to timeout`);
    controller.abort();
  }, requestTimeoutMs);

  const requestDetails: FetcherRequestInit = {
    method: 'POST',
    body: requestBody,
    headers: {
      'apollographql-client-name': name,
      'apollographql-client-version': version,
      'user-agent': `${name}/${version}`,
      'content-type': 'application/json',
    },
    signal: controller.signal,
  };

  logger.debug(`🔧 Fetching ${graphRef} supergraph schema from ${endpoint} ifAfterId ${compositionId}`);

  let result: FetcherResponse;
  try {
    result = await fetcher(endpoint, requestDetails);
  } catch (e) {
    throw new UplinkFetcherError(fetchErrorMsg + (e.message ?? e));
  } finally {
    clearTimeout(signal);
  }

  let response: SupergraphSdlQueryResult;

  if (result.ok || result.status === 400) {
    try {
      response = await result.json();
    } catch (e) {
      // Bad response
      throw new UplinkFetcherError(fetchErrorMsg + result.status + ' ' + e.message ?? e);
    }

    if ('errors' in response) {
      throw new UplinkFetcherError(
        [fetchErrorMsg, ...response.errors.map((error) => error.message)].join(
          '\n',
        ),
      );
    }
  } else {
    throw new UplinkFetcherError(fetchErrorMsg + result.status + ' ' + result.statusText);
  }

  const { routerConfig } = response.data;
  if (routerConfig.__typename === 'RouterConfigResult') {
    const {
      id,
      supergraphSdl,
      minDelaySeconds,
      // messages,
    } = routerConfig;
    return { id, supergraphSdl, minDelaySeconds };
  } else if (routerConfig.__typename === 'FetchError') {
    // FetchError case
    const { code, message } = routerConfig;
    throw new UplinkFetcherError(`${code}: ${message}`);
  } else if (routerConfig.__typename === 'Unchanged') {
    return null;
  } else {
    throw new UplinkFetcherError('Programming error: unhandled response failure');
  }
}
