import React from 'react';
import { Platform } from 'react-native';
import { Logger } from '../logger';
import {
  CHECK_INTERVAL,
  MAX_CHECK_TIMEOUT,
  SOCKET_WAIT_TIMEOUT,
} from './constants';
import { initWebSocket } from './websocket';
import type { SOCKET_CLIENT_RESPONSE, SOCKET_TEST_REQUEST } from '../websocketTypes';

import { add, get, type TrackedElementData } from './trackedElements';
import { handleAction } from './handleAction';

// NOTE: On new arch the module isn't autoloaded
import '../NativeOwl';

const logger = new Logger(true);

let isReactUpdating = true;

let owlClient: WebSocket;

export const initClient = () => {
  logger.info('[OWL - Client] Initialising OWL client');

  patchReact();
  waitForWebSocket();
};

/**
 * Based on an elements props, store element tracking data and return updated props
 */
export const applyElementTracking = (
  props: any,
  isJsx: boolean = false
): {
  [key: string]: any;
  ref?: React.RefObject<unknown>;
  showsHorizontalScrollIndicator: false;
  showsVerticalScrollIndicator: false;
} => {
  if (isJsx) {
    applyJsxChildrenElementTracking(props);
  }

  const testID = props?.testID;

  const returnProps = {
    ...props,
    showsHorizontalScrollIndicator: false,
    showsVerticalScrollIndicator: false,
  };

  if (!testID) {
    return returnProps;
  }

  const existingTrackedElement = get(testID);

  const ref =
    (props?.ref as React.RefObject<unknown> | undefined) ||
    existingTrackedElement?.ref ||
    React.createRef();

  const trackData: TrackedElementData = {
    ref: existingTrackedElement?.ref || ref,
    onPress: existingTrackedElement?.onPress || props?.onPress,
    onLongPress: existingTrackedElement?.onLongPress || props?.onLongPress,
    onChangeText: existingTrackedElement?.onChangeText || props?.onChangeText,
  };

  add(logger, testID, trackData);

  return {
    ...returnProps,
    ref,
  };
};

/**
 * To get access to the prop callbacks when the element is created, we need to check the children
 */
export const applyJsxChildrenElementTracking = (props: any): void => {
  if (props.children && Array.isArray(props.children)) {
    props.children.forEach((child: any) => {
      const testID = child?.props?.testID;

      if (!testID) {
        return;
      }

      const existingTrackedElement = get(testID);

      const ref =
        (child?.props?.ref as React.RefObject<unknown> | undefined) ||
        existingTrackedElement?.ref ||
        React.createRef();

      const trackData: TrackedElementData = {
        ref: existingTrackedElement?.ref || ref,
        onPress: existingTrackedElement?.onPress || child?.props?.onPress,
        onLongPress:
          existingTrackedElement?.onLongPress || child?.props?.onLongPress,
        onChangeText:
          existingTrackedElement?.onChangeText || child?.props?.onChangeText,
      };

      add(logger, testID, trackData);
    });
  }
};

/**
 * We patch react so that we can maintain a list of elements that have testID's
 * We can then use this list to find the element when we receive an action
 */
export const patchReact = () => {
  const originalReactCreateElement: typeof React.createElement =
    React.createElement;
  let automateTimeout: NodeJS.Timeout;

  if (parseInt(React.version.split('.')?.[0] || '0', 10) >= 18) {
    const jsxRuntime = require('react/jsx-runtime');
    const origJsx = jsxRuntime.jsx;

    // @ts-ignore
    jsxRuntime.jsx = (type: any, config: Object, maybeKey?: string) => {
      const newProps = applyElementTracking(config, true);

      clearTimeout(automateTimeout);

      automateTimeout = setTimeout(() => {
        isReactUpdating = false;
      }, CHECK_INTERVAL);

      isReactUpdating = true;

      return origJsx(type, newProps, maybeKey);
    };
  }

  // @ts-ignore
  React.createElement = (type, props, ...children) => {
    const newProps = applyElementTracking(props);

    clearTimeout(automateTimeout);

    automateTimeout = setTimeout(() => {
      isReactUpdating = false;
    }, CHECK_INTERVAL);

    isReactUpdating = true;

    return originalReactCreateElement(type, newProps, ...children);
  };
};

/**
 * The app might launch before the OWL server starts, so we need to keep trying...
 */
export const waitForWebSocket = async () => {
  try {
    owlClient = await initWebSocket(
      logger,
      Platform.OS === 'android' ? 'android' : 'ios',
      handleMessage
    );

    logger.info('[OWL - Websocket] Connection established');
  } catch {
    setTimeout(waitForWebSocket, SOCKET_WAIT_TIMEOUT);
  }
};

/**
 * When we receive a message, we need to find the element that corresponds to the testID,
 * then attempt to handle the requested action on it.
 */
export const handleMessage = async (message: string) => {
  const socketEvent = JSON.parse(message) as SOCKET_TEST_REQUEST;
  const testID = socketEvent.testID;

  let element;

  try {
    element = await getElementByTestId(testID);
  } catch (error) {
    sendNotFound(testID);
  }

  if (element) {
    try {
      if (socketEvent.type === 'ACTION') {
        handleAction(
          logger,
          testID,
          element,
          socketEvent.action,
          socketEvent.value
        );

        setTimeout(sendDone, 1000);
      } else {
        sendDone();
      }
    } catch (error) {
      let message = 'Unknown error';
      if (error instanceof Error) {
        message = error.message;
      }

      sendError(testID, message);
    }
  }
};

const sendEvent = (event: SOCKET_CLIENT_RESPONSE) =>
  owlClient.send(JSON.stringify(event));

const sendNotFound = (testID: string) =>
  sendEvent({ type: 'NOT_FOUND', testID });

const sendDone = () => sendEvent({ type: 'DONE' });

const sendError = (testID: string, message: string) =>
  sendEvent({ type: 'ERROR', testID, message });

/**
 * This function resolves the tracked element by its testID, so that we can handle events on it.
 * If the element is not immedietly available, we wait for it to be available for some time.
 */
const getElementByTestId = async (testID: string) =>
  new Promise<TrackedElementData>((resolve, reject) => {
    logger.info(`[OWL - Client] Looking for Element with testID ${testID}`);

    const rejectTimeout = setTimeout(() => {
      logger.error(`[OWL - Client] ❌ not found`);

      clearInterval(checkInterval);
      reject(new Error(`Element with testID ${testID} not found`));
    }, MAX_CHECK_TIMEOUT);

    const checkInterval = setInterval(() => {
      const element = get(testID);
      if (isReactUpdating || !element) {
        return;
      }

      logger.info(`[OWL - Client] ✓ found`);

      clearInterval(checkInterval);
      clearTimeout(rejectTimeout);
      resolve(element);
    }, CHECK_INTERVAL);
  });
