//@ts-ignore
import { trigger, on, off } from "utils/events";
import {
  getFormStyles,
  getCalculatorStyles,
  getWidgetData,
  startForm
} from "./services/styles";
import {
  ICustomerData,
  InitialDataResponse,
  StartFormResponse
} from "./types/dataTypes";
import { store } from "./store";
import { accountState } from "./store/selectors/account";
import {
  widgetConfig,
  widgetIsAvailable,
  widgetShowReason,
  widgetStyle
} from "./store/selectors/widget";
import {updateAccountState} from "./store/slices/account/accountSlice";
import {updateWidgetOfferData} from "./store/slices/widget/widgetSlice";
import {markSDKInitialized} from "./utils/sdkInitialization";

declare global {
  interface Window {
    PragmaGoSDK: {
      PragmaGo: typeof PragmaGo;
    };
    onPragmaGoLoad?: () => void;
    pragmaGoReady?: Promise<typeof PragmaGo>;
  }
}

interface QueueItem {
  eventName: string;
  data: unknown;
}

interface WidgetDataResponse {
  success: boolean;
  data?: InitialDataResponse;
  error?: string;
  source?: 'fetch' | 'store';
}

interface DataUpdateCallback {
  (data: InitialDataResponse): void;
}

class PragmaGo {
  private static instance: PragmaGo;
  private eventQueue: QueueItem[];
  private onDataUpdateCallback?: DataUpdateCallback;
  private eventListenersSetup: boolean = false;
  private _widgetDataInflight: Promise<InitialDataResponse | undefined> | null = null;
  private _rateLimitedUntil = 0;
  private _lastFetchAt = 0;
  private _lastInitKey = "";
  private _lastSuccessfulData: InitialDataResponse | undefined = undefined;
  private readonly _sessionId = crypto.randomUUID();
  private static readonly MIN_FETCH_INTERVAL_MS = 30_000;
  private static readonly MODAL_FETCH_INTERVAL_MS = 3_000;

  public static getInstance(): PragmaGo {
    if (!PragmaGo.instance) {
      PragmaGo.instance = new PragmaGo();
    }

    // Ensure event listeners are set up every time getInstance is called
    PragmaGo.instance.ensureEventListenersSetup();

    return PragmaGo.instance;
  }
  private widgetReady: boolean = false;

  constructor() {
    this.widgetReady = false;
    this.eventQueue = [];

    this.PGO_showOfferWidget = this.PGO_showOfferWidget.bind(this);
    this.initializePragmaCash = this.initializePragmaCash.bind(this);
    this.shouldShowWidget = this.shouldShowWidget.bind(this);
    this.getThemeStyles = this.getThemeStyles.bind(this);
    this.getWidgetDataProvider = this.getWidgetDataProvider.bind(this);
    this.setRootVariables = this.setRootVariables.bind(this);
    this.openWidgetModal = this.openWidgetModal.bind(this);
    this.getWidgetData = this.getWidgetData.bind(this);
    this.transferData = this.transferData.bind(this);
    this.setWidgetReady = this.setWidgetReady.bind(this);
    this.getUserData = this.getUserData.bind(this);
    this.handleWidgetDataUpdate = this.handleWidgetDataUpdate.bind(this);
    this.setDataUpdateCallback = this.setDataUpdateCallback.bind(this);

    // Listening for data from widget
    this.setupWidgetEventListeners();
  }

  setWidgetReady(): void {
    this.widgetReady = true;
    this.checkEventQueue();
  }

  /**
   * Sets callback that will be called on every widget data update
   */
  setDataUpdateCallback(callback: DataUpdateCallback): void {
    this.onDataUpdateCallback = callback;
  }

  /**
   * Ensures event listeners are set up (called automatically)
   */
  private ensureEventListenersSetup(): void {
    if (!this.eventListenersSetup) {
      this.setupWidgetEventListeners();
      this.eventListenersSetup = true;
    }
  }

  /**
   * Sets up event listeners for widget events
   */
  private setupWidgetEventListeners(): void {
    on("widgetDataUpdate", this.handleWidgetDataUpdate);
    document.addEventListener('widgetDataUpdate', (event) => {
      this.handleWidgetDataUpdate(event as CustomEvent);
    });
  }

  /**
   * Handles data updates from widget
   */
  private handleWidgetDataUpdate(event: CustomEvent): void {
    const updatedData = event?.detail;

    if (updatedData) {
      // Update SDK store
      this.updateSDKStore(updatedData);

      // Call integrator callback
      if (this.onDataUpdateCallback) {
        this.onDataUpdateCallback(updatedData);
      }
    }
  }

  /**
   * Updates SDK store based on widget data (widget data only)
   */
  private updateSDKStore(widgetData: any): void {
    // Update only widget data in SDK store
    // Account state is immutable and comes from SDK
    store.dispatch(updateWidgetOfferData(widgetData));
  }

  private checkEventQueue(): void {
    if (this.widgetReady && this.eventQueue.length > 0) {
      this.eventQueue.forEach(({ eventName, data }) => {
        trigger(eventName, data);
      });
      this.eventQueue = [];
    }
  }

  getUserData() {
    return accountState(store.getState());
  }

  transferData(eventName: string, data: unknown): void {
    if (this.widgetReady) {
      trigger(eventName, data);
    } else {
      this.eventQueue.push({ eventName, data });
    }
  }

  async getWidgetData(fetchNew = false, test = false, minInterval?: number): Promise<WidgetDataResponse> {
    try {
      const data = fetchNew
        ? await this.getWidgetDataProvider(test, minInterval)
        : widgetConfig(store.getState());

      if (data) {
        // If fetching new data, update SDK store and call callback
        if (fetchNew) {
          this.updateSDKStore(data);

          if (this.onDataUpdateCallback) {
            this.onDataUpdateCallback(data);
          }
        }

        this.transferData("transferDataToWidget", data);
        return { success: true, data, source: fetchNew ? "fetch" : "store" };
      }
      return { success: false, error: "No widget data available" };
    } catch (error) {
      console.error("Error in getWidgetData:", error);
      return {
        success: false,
        error: error instanceof Error ? error.message : "Unknown error"
      };
    }
  }

  async shouldShowWidget(initialized = false, test = false): Promise<boolean | string> {
    try {
      if (initialized && widgetConfig(store.getState())) {
        const isAvailable = widgetIsAvailable(store.getState());
        const reason = widgetShowReason(store.getState());
        this.transferData("transferDataToWidget", widgetConfig(store.getState()));
        // @ts-ignore
        return !isAvailable ? reason : isAvailable;
      } else {
        const widgetData = await this.getWidgetDataProvider(test);
        if (!widgetData) {
          return false;
        }

        // Update SDK store and call callback with fresh data
        this.updateSDKStore(widgetData);
        if (this.onDataUpdateCallback) {
          this.onDataUpdateCallback(widgetData);
        }

        this.transferData("transferDataToWidget", widgetData);
        return widgetData.noOfferReason || widgetData.available;
      }
    } catch (error) {
      console.error("Error in shouldShowWidget:", error);
      return false;
    }
  }

  /**
   * Initializes PragmaCash SDK with partner and customer data
   * @param partnerKey - Partner key
   * @param partnerCustomerId - Customer ID at partner
   * @param customerData - Customer data
   * @param onDataUpdate - Optional callback function called when widget data is updated
   * @param test - Optional test mode (default false)
   */
  async initializePragmaCash(
    partnerKey: string,
    partnerCustomerId: string,
    customerData: ICustomerData,
    onDataUpdate?: DataUpdateCallback,
    test = false
  ): Promise<InitialDataResponse | undefined> {
    try {
      // Ensure event listeners are set up first
      this.ensureEventListenersSetup();

      if (onDataUpdate) {
        this.setDataUpdateCallback(onDataUpdate);
      }

      store.dispatch(updateAccountState({ partnerKey, partnerCustomerId, customerData }));
      // Reset throttle only when partner or customer actually changes
      const initKey = (partnerKey && partnerCustomerId)
        ? `${partnerKey}::${partnerCustomerId}`
        : this._sessionId;
      if (initKey !== this._lastInitKey) {
        this._lastFetchAt = 0;
        this._lastSuccessfulData = undefined;
        this._lastInitKey = initKey;
      }
      const widgetData = await this.getWidgetDataProvider(test);

      //@ts-ignore
      if (widgetData?.style) {
        this.setRootVariables();
      }

      this.transferData("transferDataToWidget", widgetData);
      this.transferData("transferAccountState", {
        partnerKey,
        partnerCustomerId,
        customerData
      });

      markSDKInitialized();
      return widgetData;
    } catch (error) {
      console.error("Error in initializePragmaCash:", error);
      return undefined;
    }
  }

  async startFormProvider(test = false): Promise<boolean> {
    try {
      const { partnerKey, partnerCustomerId, customerData } = accountState(store.getState());
      if (!partnerKey || !partnerCustomerId || !customerData) {
        return false;
      }
      return await startForm({
        partnerKey,
        partnerCustomerId,
        userId: customerData.userId,
        returnUrl: customerData.returnUrl,
        test
      });
    } catch (error) {
      console.error("Start form provider error:", error);
      return false;
    }
  }

  async openWidgetModal(landingPage=false, test = false): Promise<boolean | string> {
    try {
      // Ensure event listeners are set up
      this.ensureEventListenersSetup();

      const widgetData = widgetConfig(store.getState());
      if (!widgetData) {
        return 'No widget data available';
      }

      if (widgetData?.available === true ) {

        if (!landingPage) {
          const formStarted = await this.startFormProvider(test);
          if (!formStarted) {
            return 'Failed to start form';
          }
        }

        const updatedWidgetData = await this.getWidgetDataProvider(test, PragmaGo.MODAL_FETCH_INTERVAL_MS);
        if (updatedWidgetData) {
          this.updateSDKStore(updatedWidgetData);

          // Call integrator callback with fresh data
          if (this.onDataUpdateCallback) {
            this.onDataUpdateCallback(updatedWidgetData);
          }
        }

        trigger(landingPage ? "SDK_openLandingPage" : "openWidgetForm", {
          sendInfo: true,
          widgetData: updatedWidgetData || widgetData,
          timestamp: new Date().toISOString()
        });
        return 'Data sent to widget';
      }

      return widgetData.noOfferReason || 'Widget not available';
    } catch (error) {
      console.error("Error in openWidgetModal:", error);
      return false;
    }
  }

  /**
   * Shows offer widget (main SDK function)
   * @param partnerKey - Partner key
   * @param partnerCustomerId - Customer ID at partner
   * @param customerData - Customer data
   * @param onDataUpdate - Optional callback function called when widget data is updated
   * @param test - Optional test mode (default false)
   */
  async PGO_showOfferWidget(
    partnerKey: string,
    partnerCustomerId: string,
    customerData: ICustomerData,
    onDataUpdate?: DataUpdateCallback,
    test = false
  ): Promise<boolean | string | void> {
    try {
      const widgetData = await this.initializePragmaCash(partnerKey, partnerCustomerId, customerData, onDataUpdate, test);
      if (!widgetData) {
        return 'Failed to initialize widget';
      }

      const shouldShow = await this.shouldShowWidget(true, test);
      if (shouldShow === true) {
        this.transferData("transferDataToWidget", widgetData);
        this.transferData("transferAccountState", { partnerKey, partnerCustomerId, customerData });
        return true;
      }

      return widgetData.noOfferReason || shouldShow;
    } catch (error) {
      console.error("Error in PGO_showOfferWidget:", error);
      return false;
    }
  }

  async getWidgetDataProvider(test = false, minInterval = PragmaGo.MIN_FETCH_INTERVAL_MS): Promise<InitialDataResponse | undefined> {
    if (Date.now() < this._rateLimitedUntil) {
      console.warn("Widget data provider: rate limited, returning cached data");
      return this._lastSuccessfulData ?? widgetConfig(store.getState()) ?? undefined;
    }

    // Throttle sequential calls — return last known data without new HTTP
    if (this._lastFetchAt > 0 && Date.now() - this._lastFetchAt < minInterval) {
      return this._lastSuccessfulData ?? widgetConfig(store.getState()) ?? undefined;
    }

    if (this._widgetDataInflight) {
      return this._widgetDataInflight;
    }

    this._widgetDataInflight = (async () => {
      try {
        const { partnerKey, partnerCustomerId, customerData } = accountState(store.getState());
        if (!partnerKey || !partnerCustomerId || !customerData) {
          return undefined;
        }
        const result = await getWidgetData({ partnerKey, partnerCustomerId, customerData, test });
        this._lastFetchAt = Date.now();
        this._lastSuccessfulData = result;
        return result;
      } catch (error: any) {
        const status = error?.response?.status;
        if (status === 429 || status === 461) {
          this._rateLimitedUntil = Date.now() + 60_000;
          console.warn(`Widget data provider: received ${status}, backing off for 60s`);
        }
        // Throttle even on error — prevents immediate retry on bad data (400 etc.)
        this._lastFetchAt = Date.now();
        console.error("Widget data provider error:", error);
        return this._lastSuccessfulData;
      } finally {
        this._widgetDataInflight = null;
      }
    })();

    return this._widgetDataInflight;
  }

  async getThemeStyles({
                         styleId,
                         paymentId,
                       }: {
    styleId: string;
    paymentId?: string;
  }) {
    try {
      return styleId && paymentId
        ? await getFormStyles({ styleId, paymentId })
        : await getCalculatorStyles({ styleId });
    } catch (error) {
      this.handleError(error instanceof Error ? error : new Error('Unknown error'));
    }
  }

  async createGlobalStyleVariables({
                                     styleId,
                                     paymentId,
                                   }: {
    styleId: string;
    paymentId?: string;
  }): Promise<void> {
    try {
      const data = await this.getThemeStyles({ styleId, paymentId });
      if (data) {
        this.setRootVariables(false);
      }
    } catch (error) {
      this.handleError(error instanceof Error ? error : new Error('Unknown error'));
    }
  }

  setRootVariables(theme = false): void {
    const style = widgetStyle(store.getState());
    const root = document.documentElement.style;

    if (style) {
      if (style.primaryColor) root.setProperty("--primaryColor", style.primaryColor);
      if (!theme && style.secondaryColor) root.setProperty("--secondaryColor", style.secondaryColor);
      if (!theme && style.logoFileId) root.setProperty("--logoFileName", style.logoFileId);
      if (theme && style.logoFileName) root.setProperty("--logoFileName", style.logoFileName);
      if (style.borderRadiusBody) root.setProperty("--borderRadiusBody", style.borderRadiusBody);
      if (style.borderRadiusButton) root.setProperty("--borderRadiusButton", style.borderRadiusButton);
    }
  }

  private handleError(error: Error): never {
    throw new Error("Error: " + error.message);
  }
}

if (typeof window !== 'undefined') {
  if (!window.pragmaGoReady) {
    window.pragmaGoReady = new Promise((resolve) => {
      window.onPragmaGoLoad = () => resolve(PragmaGo);
    });
  }

  window.PragmaGoSDK = {
    PragmaGo
  };

  if (window.onPragmaGoLoad) {
    window.onPragmaGoLoad();
  }
}

export { PragmaGo };
