import { Exchange, Client, Operation, OperationResult } from '@urql/core';
import { parse } from 'graphql';
import { pipe, tap, take, toPromise } from 'wonka';
import { DevtoolsExecuteQueryMessage } from './types';
import {
  getDisplayName,
  hash,
  createDebugMessage,
  createNativeMessenger,
  createBrowserMessenger,
  Messenger,
} from './utils';

interface HandlerArgs {
  sendMessage: Messenger['sendMessage'];
}

const curriedDevtoolsExchange: (a: Messenger) => Exchange = ({
  sendMessage,
  addMessageListener,
}) => ({ client, forward }) => {
  // Listen for messages from devtools
  addMessageListener((message) => {
    if (message.source !== 'devtools' || !(message.type in messageHandlers)) {
      return;
    }

    messageHandlers[message.type]({ client, sendMessage })(message as any);
  });

  // Forward debug events to content script
  client.subscribeToDebugTarget &&
    client.subscribeToDebugTarget((event) =>
      sendMessage({
        type: 'debug-event',
        source: 'exchange',
        data: event,
      })
    );

  return (ops$) =>
    pipe(
      ops$,
      tap(handleOperation({ client, sendMessage })),
      forward,
      tap(handleResult({ client, sendMessage }))
    );
};

interface HandlerArgs {
  client: Client;
  sendMessage: Messenger['sendMessage'];
}

/** Handle outgoing operations */
const handleOperation = ({ sendMessage }: HandlerArgs) => (
  operation: Operation
) => {
  if (operation.kind === 'teardown') {
    const msg = createDebugMessage({
      type: 'teardown',
      message: 'The operation has been torn down',
      operation,
      data: undefined,
    });

    return sendMessage(msg);
  }

  const msg = createDebugMessage({
    type: 'execution',
    message: 'The client has received an execute command.',
    operation,
    data: {
      sourceComponent: getDisplayName(),
    },
  });
  return sendMessage(msg);
};

/** Handle new value or error */
const handleResult = ({ sendMessage }: HandlerArgs) => ({
  operation,
  data,
  error,
}: OperationResult) => {
  if (error) {
    const msg = createDebugMessage({
      type: 'error',
      message: 'The operation has returned a new error.',
      operation,
      data: {
        value: error,
      },
    });
    return sendMessage(msg);
  }

  const msg = createDebugMessage({
    type: 'update',
    message: 'The operation has returned a new response.',
    operation,
    data: {
      value: data,
    },
  });
  sendMessage(msg);
};

/** Handle execute request message. */
const handleExecuteQueryMessage = ({ client }: HandlerArgs) => (
  message: DevtoolsExecuteQueryMessage
) => {
  const isMutation = /(^|\W)+mutation\W/.test(message.query);
  const requestType = isMutation ? 'mutation' : 'query';
  const op = client.createRequestOperation(
    requestType,
    {
      key: hash(JSON.stringify(message.query)),
      query: parse(message.query),
    },
    {
      meta: {
        source: 'Devtools',
      },
    }
  );

  pipe(client.executeRequestOperation(op), take(1), toPromise);
};

/** Handle connection initiated by devtools. */
const handleConnectionInitMessage = ({ sendMessage }: HandlerArgs) => () =>
  sendMessage({
    type: 'connection-acknowledge',
    source: 'exchange',
    version: __pkg_version__,
  });

/** Map of handlers for incoming messages. */
const messageHandlers = {
  'execute-query': handleExecuteQueryMessage,
  'connection-init': handleConnectionInitMessage,
} as const;

export const devtoolsExchange = ((): Exchange => {
  const isNative =
    typeof navigator !== 'undefined' && navigator?.product === 'ReactNative';
  const isSSR = !isNative && typeof window === 'undefined';

  // Prod or SSR
  if (process.env.NODE_ENV === 'production' || isSSR) {
    return ({ forward }) => (ops$) => pipe(ops$, forward);
  }

  if (isNative) {
    return curriedDevtoolsExchange(createNativeMessenger());
  }

  return curriedDevtoolsExchange(createBrowserMessenger());
})();
