// IMPORTANT:
// this file is super important to load the __REACT_DEVTOOLS_GLOBAL_HOOK__ object
// without this, we can't stub the React DevTools global hook, we don't have a way to instrument the application
// make sure you import this file first before anything else (particularly React)

import type { ReactDevToolsGlobalHook, ReactRenderer } from "./types.js";
import { toUnsubscribe, type Unsubscribe } from "./unsubscribe.js";

export const version = process.env.VERSION;
export const BIPPY_INSTRUMENTATION_STRING = `bippy-${version}`;

const objectDefineProperty = Object.defineProperty;
// eslint-disable-next-line @typescript-eslint/unbound-method
const objectHasOwnProperty = Object.prototype.hasOwnProperty;

const NO_OP = () => {
  /**/
};

const checkDCE = (functionToCheck: unknown): void => {
  try {
    const code = Function.prototype.toString.call(functionToCheck);
    if (code.indexOf("^_^") > -1) {
      setTimeout(() => {
        throw new Error(
          "React is running in production mode, but dead code " +
            "elimination has not been applied. Read how to correctly " +
            "configure React for production: " +
            "https://reactjs.org/link/perf-use-production-build",
        );
      });
    }
  } catch {}
};

export const isRealReactDevtools = (
  rdtHook: ReactDevToolsGlobalHook | undefined | null = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__,
): boolean => {
  return Boolean(rdtHook && "getFiberRoots" in rdtHook);
};

let isReactRefreshOverride = false;
let injectFnStr: string | undefined = undefined;

export const isReactRefresh = (
  rdtHook: ReactDevToolsGlobalHook | undefined | null = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__,
): boolean => {
  if (isReactRefreshOverride) return true;
  if (rdtHook && typeof rdtHook.inject === "function") {
    injectFnStr = rdtHook.inject.toString();
  }
  // https://github.com/facebook/react/blob/8f8b336734d7c807f5aa11b0f31540e63302d789/packages/react-refresh/src/ReactFreshRuntime.js#L459
  return Boolean(injectFnStr?.includes("(injected)"));
};

export const _onActiveListeners = new Set<() => unknown>();

export const _renderers = new Set<ReactRenderer>();

const rendererInjectListeners = new Set<(renderer: ReactRenderer) => void>();
// re-wrapping inject (e.g. after the hook is replaced) leaves the old
// wrapper in the call chain, so notifications are deduped per renderer
const notifiedRenderers = new WeakSet<ReactRenderer>();
let notifyingInject: ReactDevToolsGlobalHook["inject"] | null = null;

const ensureInjectNotifiesListeners = (rdtHook: ReactDevToolsGlobalHook): void => {
  if (rdtHook.inject === notifyingInject) return;
  const prevInject = rdtHook.inject;
  const nextInject = (renderer: ReactRenderer) => {
    const rendererId = prevInject.call(rdtHook, renderer);
    if (!notifiedRenderers.has(renderer)) {
      notifiedRenderers.add(renderer);
      for (const listener of rendererInjectListeners) {
        listener(renderer);
      }
    }
    return rendererId;
  };
  rdtHook.inject = nextInject;
  notifyingInject = nextInject;
};

/**
 * Subscribes to future renderer injections into the DevTools hook. The
 * single shared inject wrapper lets multiple consumers (override methods,
 * react-refresh, user code) observe renderers without stacking patches
 * whose restore order matters. Returns an unsubscribe function.
 */
export const onRendererInject = (listener: (renderer: ReactRenderer) => void): Unsubscribe => {
  ensureInjectNotifiesListeners(getRDTHook());
  rendererInjectListeners.add(listener);
  return toUnsubscribe(() => {
    rendererInjectListeners.delete(listener);
  });
};

export const installRDTHook = (onActive?: () => unknown): ReactDevToolsGlobalHook => {
  if (onActive) {
    _onActiveListeners.add(onActive);
  }
  const renderers = new Map<number, ReactRenderer>();
  let rendererIdCounter = 0;
  let rdtHook: ReactDevToolsGlobalHook = {
    _instrumentationIsActive: false,
    _instrumentationSource: BIPPY_INSTRUMENTATION_STRING,
    checkDCE,
    hasUnsupportedRendererAttached: false,
    inject(renderer) {
      const nextRendererId = ++rendererIdCounter;
      renderers.set(nextRendererId, renderer);
      _renderers.add(renderer);
      if (!rdtHook._instrumentationIsActive) {
        rdtHook._instrumentationIsActive = true;
        _onActiveListeners.forEach((listener) => listener());
      }
      return nextRendererId;
    },
    on: NO_OP,
    onCommitFiberRoot: NO_OP,
    onCommitFiberUnmount: NO_OP,
    onPostCommitFiberRoot: NO_OP,
    renderers,
    supportsFiber: true,
    supportsFlight: true,
  };
  try {
    objectDefineProperty(globalThis, "__REACT_DEVTOOLS_GLOBAL_HOOK__", {
      configurable: true,
      enumerable: true,
      get() {
        return rdtHook;
      },
      set(newHook) {
        if (newHook && typeof newHook === "object") {
          const ourRenderers = rdtHook.renderers;
          rdtHook = newHook;
          if (ourRenderers.size > 0) {
            ourRenderers.forEach((renderer, id) => {
              _renderers.add(renderer);
              // eslint-disable-next-line @typescript-eslint/no-unsafe-call
              newHook.renderers.set(id, renderer);
            });
            patchRDTHook(onActive);
          }
        }
      },
    });
    // [!] this is a hack for chrome extensions - if we install before React DevTools, we could accidently prevent React DevTools from installing:
    // https://github.com/facebook/react/blob/18eaf51bd51fed8dfed661d64c306759101d0bfd/packages/react-devtools-extensions/src/contentScripts/installHook.js#L30C6-L30C27
    // eslint-disable-next-line @typescript-eslint/unbound-method
    const originalWindowHasOwnProperty = window.hasOwnProperty;
    let hasRanHack = false;
    objectDefineProperty(window, "hasOwnProperty", {
      configurable: true,
      value: function (this: unknown, ...args: [PropertyKey]) {
        try {
          if (!hasRanHack && args[0] === "__REACT_DEVTOOLS_GLOBAL_HOOK__") {
            globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ = undefined;
            // special falsy value to know that we've already installed before
            hasRanHack = true;
            return -0;
          }
        } catch {}
        return originalWindowHasOwnProperty.apply(this, args);
      },
      writable: true,
    });
  } catch {
    patchRDTHook(onActive);
  }
  return rdtHook;
};

export const patchRDTHook = (onActive?: () => unknown): void => {
  if (onActive) {
    _onActiveListeners.add(onActive);
  }
  try {
    const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
    if (!rdtHook) return;
    if (!rdtHook._instrumentationSource) {
      rdtHook.checkDCE = checkDCE;
      rdtHook.supportsFiber = true;
      rdtHook.supportsFlight = true;
      rdtHook.hasUnsupportedRendererAttached = false;
      rdtHook._instrumentationSource = BIPPY_INSTRUMENTATION_STRING;
      rdtHook._instrumentationIsActive = false;
      // we need to be careful here (needs to be below _instrumentationSource) else it causes excessive recursion
      const isReactDevtools = isRealReactDevtools(rdtHook);
      if (!isReactDevtools) {
        rdtHook.on = NO_OP;
      }
      if (rdtHook.renderers.size) {
        rdtHook._instrumentationIsActive = true;
        _onActiveListeners.forEach((listener) => listener());
        return;
      }
      const prevInject = rdtHook.inject;
      const isRefresh = isReactRefresh(rdtHook);
      if (isRefresh && !isReactDevtools) {
        isReactRefreshOverride = true;
        // but since the underlying implementation doens't care,
        // it's ok: https://github.com/facebook/react/blob/18eaf51bd51fed8dfed661d64c306759101d0bfd/packages/react-refresh/src/ReactFreshRuntime.js#L430
        const injectedRendererId = rdtHook.inject({
          scheduleRefresh() {},
        } as unknown as ReactRenderer);
        if (injectedRendererId) {
          rdtHook._instrumentationIsActive = true;
        }
      }
      rdtHook.inject = (renderer) => {
        const rendererId = prevInject(renderer);
        _renderers.add(renderer);
        if (isRefresh) {
          // react refresh doesn't inject this properly
          // https://github.com/facebook/react/blob/18eaf51bd51fed8dfed661d64c306759101d0bfd/packages/react-refresh/src/ReactFreshRuntime.js#L430
          rdtHook.renderers.set(rendererId, renderer);
        }
        rdtHook._instrumentationIsActive = true;
        _onActiveListeners.forEach((listener) => listener());
        return rendererId;
      };
    }
    if (
      rdtHook.renderers.size ||
      rdtHook._instrumentationIsActive ||
      // depending on this to inject is unsafe, since inject could occur before and we wouldn't know
      isReactRefresh()
    ) {
      onActive?.();
    }
  } catch {}
};

export const hasRDTHook = (): boolean => {
  return objectHasOwnProperty.call(globalThis, "__REACT_DEVTOOLS_GLOBAL_HOOK__");
};

/**
 * Returns the current React DevTools global hook.
 */
export const getRDTHook = (onActive?: () => unknown): ReactDevToolsGlobalHook => {
  if (!hasRDTHook()) {
    return installRDTHook(onActive);
  }

  patchRDTHook(onActive);
  // must exist at this point
  return globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ as ReactDevToolsGlobalHook;
};

export const isClientEnvironment = (): boolean => {
  return Boolean(
    typeof window !== "undefined" &&
    // eslint-disable-next-line @typescript-eslint/unbound-method
    (window.document?.createElement || window.navigator?.product === "ReactNative"),
  );
};

/**
 * Usually used purely for side effect
 */
export const safelyInstallRDTHook = () => {
  try {
    // __REACT_DEVTOOLS_GLOBAL_HOOK__ must exist before React is ever executed
    if (isClientEnvironment()) {
      getRDTHook();
    }
  } catch {}
};
