import * as React from 'react';
import {
  useState,
  useEffect,
  useMemo,
  useCallback,
  useLayoutEffect,
  useRef,
  memo,
  StrictMode,
  FC,
} from 'react';
import { createRoot, Root } from 'react-dom/client';
import Api from './services/api';
import Widget from './components/Widget';
import {
  IData,
  ISettings,
  Sources,
  Config,
  IDebug,
  MinContentEvent,
  EventMapKeys,
  EventMap,
  AdalongWidgetInstance,
} from './types';
import { Debug } from './debug';
import { Helpers } from './services/helpers';
import {
  DEFAULT_CONFIG, initial, StateSetter, StoreProvider, StoreState,
} from './services/store';
import createWidgetEvents from './services/events';
import KeepAlive from './services/keepAlive';
import { AnalyticsConsent } from './services/analyticsConsent';

declare const __WIDGET_VERSION__: string;

interface AdalongWidgetProps {
  token?: string;
  config?: Config;
}

// Separate interface for widget root props
interface WidgetRootProps {
  token: string;
  config?: Config;
  onReady: (widget: ReturnType<typeof useAdalongWidget>) => void;
}

// Wrapper component for hooks compliance, memory and state management
const WidgetRoot: FC<WidgetRootProps> = memo(({ token, config, onReady }) => {
  const widget = useAdalongWidget({ token, config });

  // Use layout effect to ensure synchronous execution
  useLayoutEffect(() => {
    onReady(widget);
  }, [widget, onReady]);

  return null;
});

const initializeAdalongWidget = ({
  token,
  config,
}: {
  token: string;
  config?: Config;
}): Promise<AdalongWidgetInstance> => new Promise((resolve) => {
  Api.initialize(config);

  if (typeof config?.hasAnalyticsConsent === 'boolean') {
    AnalyticsConsent.set(config.hasAnalyticsConsent);
  }

  const container = document.createElement('div');
  document.body.appendChild(container);
  const root = createRoot(container);

  const handleReady = (widget: ReturnType<typeof useAdalongWidget>) => {
    resolve(widget);
    // Ensure cleanup happens after resolution
    queueMicrotask(() => {
      root.unmount();
      container.remove();
    });
  };

  root.render(
      <StrictMode>
        <StoreProvider
          state={{
            ...initial,
            config: config || DEFAULT_CONFIG,
          }}
        >
          <WidgetRoot token={token} config={config} onReady={handleReady} />
        </StoreProvider>
      </StrictMode>,
  );
});

const useAdalongWidget = ({ token, config: initialConfig }: AdalongWidgetProps) => {
  const [loaded, setLoaded] = useState(false);
  const [layout, setLayout] = useState<ISettings['type']>();
  const [destroyed, setDestroyed] = useState(false);
  // Keep reference of last used element selector and settings for recovery
  const lastElementRef = useRef<string | null>(null);
  const lastSettingsRef = useRef<Partial<ISettings> | undefined>(undefined);
  const keepAliveRef = useRef<KeepAlive | null>(null);

  const id = useMemo(() => initialConfig?.id || Helpers.randomId(), [initialConfig?.id]);
  const startDate = useMemo(() => new Date(), []);
  const events = useMemo(() => createWidgetEvents(), []);
  const config = useMemo(
    () => ({
      ...DEFAULT_CONFIG,
      ...initialConfig,
    }),
    [initialConfig],
  );

  const portalId = useMemo(() => `adalong-postViewer-${id}`, [id]);
  const portalRef = useRef<HTMLDivElement | null>(null);
  // Keep a single React root per target element to avoid duplicate roots on re-load.
  const reactRootRef = useRef<{ element: HTMLElement; root: Root } | null>(null);
  const cleanupObserverRef = useRef<MutationObserver | null>(null);

  const widgetVersion = 7;
  const storeStateRef = useRef<{ getState(): StoreState; stateSetter: StateSetter }>({
    getState: () => initial,
    stateSetter: () => {},
  });

  const triggerEvent = useCallback(
    <K extends EventMapKeys>(eventName: K, event: EventMap[K], widgetId?: string) => {
      events.triggerEvent(eventName, event, widgetId);
    },
    [events],
  );

  const getSlideState = useCallback(
    () => ({
      canSlideLeft: storeStateRef.current.getState().canSlideLeft,
      canSlideRight: storeStateRef.current.getState().canSlideRight,
    }),
    [],
  );

  const changePost = useCallback((dir: 'left' | 'right') => {
    dispatchEvent(new CustomEvent('adalongWidget_changePost', { detail: dir }));
  }, []);

  const setSlider = useCallback((dir: 'left' | 'right') => {
    dispatchEvent(new CustomEvent('adalongWidget_slide', { detail: dir }));
  }, []);

  const setDebug = useCallback((debug: IDebug) => {
    if (!debug) return;

    Debug.try(() => {
      const { level } = debug;
      if (level) {
        Debug.setLevel(level);
      }
    }).catch(console.error);
  }, []);

  const updateHasAnalyticsConsent = useCallback((granted: boolean) => {
    AnalyticsConsent.set(granted);
  }, []);

  const loadWidgetContent = useCallback(
    async (
      widgetToken: string,
      sources?: Sources,
      settings?: Partial<ISettings>,
    ): Promise<IData | null> => {
      if (!widgetToken) {
        throw new Error('No settings or token provided');
      }

      const defaultSettings: ISettings & { id: string } = await Api.getSettings(
        widgetToken,
        widgetVersion,
        settings?.localization,
      );
      const { id: settingsId, ...restDefaultSettings } = defaultSettings;
      const finalSettings: ISettings = {
        ...restDefaultSettings,
        ...settings,
      };

      const defaultProductList = finalSettings.default_sources?.product_list;
      const dataProductsMissing = Array.isArray(defaultProductList)
        && defaultProductList.length === 0
        && (!sources?.productIds || sources.productIds.length === 0);

      if (dataProductsMissing) {
        console.warn('Adalong Widget: data-products is missing');
        return null;
      }

      const content = await Api.getContent(widgetToken, widgetVersion, sources, finalSettings);
      if (!content) {
        throw new Error('No content returned');
      }

      content.medias = content.medias.map((media, i) => ({ ...media, postIndex: i }));

      const minContent: MinContentEvent = {
        required: finalSettings.min_media,
        current: content.medias.length,
      };

      if (minContent.required && minContent.current < minContent.required) {
        if (storeStateRef.current.getState().hasSubscribed?.('minContentNotReached')) {
          storeStateRef.current
            .getState()
            .triggerEvent('minContentNotReached', minContent, settingsId);
        }
        return null;
      }

      storeStateRef.current.getState().triggerEvent('minContentReached', minContent, settingsId);

      return { settings: finalSettings, content, id: settingsId };
    },
    [widgetVersion],
  );

  const load = useCallback(
    async (element: string | HTMLElement, settings?: Partial<ISettings>) => {
      if (!token || destroyed) {
        return;
      }

      // Store the element for potential recovery
      if (typeof element === 'string') {
        lastElementRef.current = element;
      } else if (element.id) {
        lastElementRef.current = `#${element.id}`;
      } else {
        lastElementRef.current = null;
      }
      lastSettingsRef.current = settings;

      await Debug.try(async () => {
        const rootElement = typeof element === 'string'
          ? document.querySelector<HTMLElement>(element)
          : element;

        if (!rootElement) {
          throw new Error('Root element not found');
        }

        const parseDataset = (data: string | undefined): string[] | undefined => {
          if (!data) return undefined;
          return Helpers.isValidJson(data) ? (JSON.parse(data) as string[]) : data.split(',');
        };

        const sources: Sources = {
          productIds: parseDataset(rootElement.dataset.products),
          collectionIds: parseDataset(rootElement.dataset.collections),
        };

        const data = await loadWidgetContent(token, sources, settings);
        if (!data) return;

        setLayout(data.settings.type);

        // Clean up any existing keep-alive before creating a new one
        if (keepAliveRef.current) {
          keepAliveRef.current.stop();
        }

        // Re-use the React root if load() is called again on the same element.
        if (reactRootRef.current && reactRootRef.current.element !== rootElement) {
          try {
            reactRootRef.current.root.unmount();
          } catch (e) {
            Debug.log('Failed to unmount previous root', e);
          }
          reactRootRef.current = null;
        }
        if (!reactRootRef.current) {
          reactRootRef.current = { element: rootElement, root: createRoot(rootElement) };
        }
        reactRootRef.current.root.render(
          <StoreProvider
            state={{
              exportState: (getState: () => StoreState, setter: StateSetter) => {
                storeStateRef.current = { getState, stateSetter: setter };
              },
              data,
              root: rootElement,
              forceMobile: rootElement.getAttribute('data-forcemobile') === 'true',
              isMobile: Helpers.isMobileDevice(),
              triggerEvent,
              config,
            }}
          >
            <Widget root={rootElement} startDate={startDate} postViewerPortalId={portalId} />
          </StoreProvider>,
        );

        setLoaded(true);

        // Set up keep-alive monitoring for auto-recovery.
        // NOTE: KeepAlive is intentionally conservative — see KeepAlive class for details.
        // We only enable it when an explicit selector is available.
        const elementSelector = typeof element === 'string'
          ? element
          : (element.id ? `#${element.id}` : null);

        if (elementSelector) {
          keepAliveRef.current = new KeepAlive({
            targetSelector: elementSelector,
            onDisappear: () => {
              Debug.log('Widget disappeared, attempting to recover');
              if (lastElementRef.current) {
                load(lastElementRef.current, lastSettingsRef.current);
              }
            },
            maxAttempts: 3,
            checkInterval: 2000,
          });
          keepAliveRef.current.start();
        }

        // Setup mutation observer for cleanup. Scope it to the parent (or html) instead of
        // the entire document.body subtree to avoid firing on every unrelated DOM change.
        if (cleanupObserverRef.current) {
          cleanupObserverRef.current.disconnect();
        }
        const cleanupTarget = rootElement.parentNode || document.documentElement;
        const observer = new MutationObserver(() => {
          if (!document.body.contains(rootElement)) {
            observer.disconnect();
            cleanupObserverRef.current = null;
            setDestroyed(true);
          }
        });
        observer.observe(cleanupTarget, { childList: true, subtree: false });
        cleanupObserverRef.current = observer;
      });
    },
    [token, destroyed, config, portalId, startDate, loadWidgetContent],
  );

  useEffect(() => {
    // Create portal div if it doesn't exist
    if (!document.getElementById(portalId)) {
      const portalDiv = document.createElement('div');
      portalDiv.id = portalId;
      document.body.appendChild(portalDiv);
      portalRef.current = portalDiv;
    }

    return () => {
      const node = portalRef.current;
      if (node && node.parentNode) {
        node.parentNode.removeChild(node);
      }
      portalRef.current = null;
    };
  }, [portalId]);

  useEffect(() => {
    // Add fonts
    const uid = 'adalong-fonts';
    const fonts = 'Fustat:400,700|Dangrek'
    if (!document.getElementById(uid)) {
      const link = document.createElement('link');
      link.id = uid;
      link.rel = 'stylesheet';
      link.href = `https://fonts.googleapis.com/css?family=${fonts}&display=swap`;
      document.head.appendChild(link);
    }
      
    // Register widget in global API
    window.adalongWidgetAPI ??= { widgets: [] };
    window.adalongWidgetAPI.version = __WIDGET_VERSION__;
    window.adalongWidgetAPI.widgets.push({
      id,
      layout,
      loaded,
      load,
      getSlideState,
      changePost,
      setSlider,
      setDebug,
      setHasAnalyticsConsent: updateHasAnalyticsConsent,
      ...events,
    });

    // Cleanup
    return () => {
      // Stop the keep-alive monitoring
      if (keepAliveRef.current) {
        keepAliveRef.current.stop();
        keepAliveRef.current = null;
      }

      if (cleanupObserverRef.current) {
        cleanupObserverRef.current.disconnect();
        cleanupObserverRef.current = null;
      }

      // Unmount the React root we created for the host element.
      if (reactRootRef.current) {
        const { root } = reactRootRef.current;
        reactRootRef.current = null;
        queueMicrotask(() => {
          try { root.unmount(); } catch (e) { Debug.log('Root unmount failed', e); }
        });
      }

      const index = window.adalongWidgetAPI.widgets.findIndex((w) => w.id === id);
      if (index > -1) {
        window.adalongWidgetAPI.widgets.splice(index, 1);
      }
    };
  }, [id, portalId, load, getSlideState, changePost, setSlider, events]);

  return {
    id,
    layout,
    loaded,
    load,
    getSlideState,
    changePost,
    setSlider,
    setDebug,
    setHasAnalyticsConsent: updateHasAnalyticsConsent,
    ...events,
  };
};

const setHasAnalyticsConsent = (granted: boolean): void => {
  AnalyticsConsent.set(granted);
};

export { initializeAdalongWidget, useAdalongWidget, setHasAnalyticsConsent };
export type { AdalongWidgetInstance };
