//@ts-ignore
import { trigger, on, off } from "utils/events";
import {
  getFormStyles,
  getCalculatorStyles,
  getWidgetData,
  getWidgetDataDirect,
  startForm,
  startFormDirect
} 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, updateDirectModeState, setDirectFormId, type AccountState} from "./store/slices/account/accountSlice";
import {updateWidgetOfferData} from "./store/slices/widget/widgetSlice";
import {markSDKInitialized} from "./utils/sdkInitialization";

// Known form steps (mirrors the front's StepType). Used to validate a URL-provided
// ?intentionStep= before forwarding it to form/start — we only pass it through when
// it names a step we actually recognise, otherwise it's ignored.
const KNOWN_FORM_STEPS = new Set<string>([
  'WAITING_FOR_DECISION', 'APPLICANT_DATA', 'NONE', 'IDENTITY_VERIFICATION',
  'POSITIVE_DECISION', 'PICK_OFFER', 'INFO_STEP', 'SIGNING', 'ACTIVATION_CONDITIONS',
  'ACTIVATION_CONDITION_BANK_STATEMENT', 'ACTIVATION_CONDITION_PICK_CARD_VERIFICATION',
  'ACTIVATION_CONDITION_VIDEO_VERIFICATION', 'CANCEL', 'NEGATIVE_DECISION',
  'CHANGE_CONDITIONS', 'CHANGE_BANK_ACCOUNT', 'BANK_ACCOUNT_VERIFICATION',
  'WAITING_FOR_BANK_ACCOUNT_CHANGE', 'CHANGE_BANK_ACCOUNT_SUCCESS', 'CHANGE_BANK_ACCOUNT_FAILURE',
  'CALCULATOR', 'SIGN_UP_FORM', 'CANCELED', 'OTHER_USER_FILLS_FORM', 'WAITING_FOR_DECISION_SUCCESS',
  'SMS_IDENTITY_VERIFICATION', 'OTP', 'SUCCESS', 'CREDIT_SCORE_CALCULATOR', 'CREDIT_SCORE_CALCULATOR_RESULT',
]);

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);

      // Keep the throttle cache in sync with the widget's push, not just the Redux store.
      // Otherwise a later getWidgetData(true) inside the throttle window returns the stale
      // init snapshot from _lastSuccessfulData and can overwrite the widget's newer step.
      // (Deliberately NOT touching _lastFetchAt — the fetch cadence stays unchanged.)
      this._lastSuccessfulData = 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. Accepts two call styles:
   *
   * Partner mode (backward compatible):
   *   initializePragmaCash(partnerKey, partnerCustomerId, customerData, onDataUpdate?, test?)
   *
   * Direct/standalone mode (no partner):
   *   initializePragmaCash({ country: 'PL', formId?, recommendationCode?, isReflink?, returnUrl?, onDataUpdate?, test? })
   */
  async initializePragmaCash(
    partnerKeyOrOptions: string | {
      country: string;
      formId?: string;
      recommendationCode?: string;
      isReflink?: boolean;
      returnUrl?: string;
      calculatedAmount?: number;
      intentionStep?: string;
      onDataUpdate?: DataUpdateCallback;
      test?: boolean;
    },
    partnerCustomerId?: string,
    customerData?: ICustomerData,
    onDataUpdate?: DataUpdateCallback,
    test = false,
  ): Promise<InitialDataResponse | undefined> {
    try {
      this.ensureEventListenersSetup();

      // Detect call style: object = direct mode, string = partner mode
      if (typeof partnerKeyOrOptions === 'object') {
        const opts = partnerKeyOrOptions;
        const urlParams = new URLSearchParams(window.location.search);

        // SDK auto-reads from URL, explicit options take priority
        const country = opts.country || urlParams.get('country') || 'PL';
        const formId = opts.formId || urlParams.get('formId') || undefined;
        // Email links use snake_case (recommendation_code); camelCase/lowercase kept as fallbacks.
        const recommendationCode = opts.recommendationCode || urlParams.get('recommendation_code') || urlParams.get('recommendationCode') || urlParams.get('recommendationcode') || undefined;
        const isReflink = opts.isReflink ?? (urlParams.get('isReflink') === 'true' ? true : undefined);
        const returnUrl = opts.returnUrl || (window.location.origin + window.location.pathname);
        const rawCalculatedAmount = opts.calculatedAmount ?? parseFloat((urlParams.get('calculatedAmount') ?? '').replace(',', '.'));
        const calculatedAmount = !isNaN(rawCalculatedAmount) ? rawCalculatedAmount : undefined;
        // Optional entry intention from the link (e.g. ?intentionStep=CREDIT_SCORE_CALCULATOR).
        // Only forwarded to form/start when it names a step we recognise.
        const rawIntentionStep = opts.intentionStep || urlParams.get('intentionStep') || undefined;
        const intentionStep = rawIntentionStep && KNOWN_FORM_STEPS.has(rawIntentionStep) ? rawIntentionStep : undefined;
        const directCallback = opts.onDataUpdate;
        const directTest = opts.test;

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

        // Helper: create new form and update store
        const createNewForm = async () => {
          const newFormId = await startFormDirect({
            country,
            recommendationCode,
            isReflink,
            returnUrl,
            rawUrl: window.location.href,
            intentionStep,
          });
          store.dispatch(setDirectFormId(newFormId));
          // Reset throttle so next getWidgetDataProvider uses the new formId
          this._lastFetchAt = 0;
          this._lastSuccessfulData = undefined;
          this._lastInitKey = `direct::${country}::${newFormId}`;
          return newFormId;
        };

        let resolvedFormId = formId;

        if (resolvedFormId) {
          // We have a formId (from URL or options) — set it and try to fetch
          store.dispatch(updateDirectModeState({ country, formId: resolvedFormId, recommendationCode, isReflink, returnUrl }));
          this._lastFetchAt = 0;
          this._lastSuccessfulData = undefined;
          this._lastInitKey = `direct::${country}::${resolvedFormId}`;
        } else {
          // No formId — create a new form first
          resolvedFormId = await createNewForm();
          store.dispatch(updateDirectModeState({ country, formId: resolvedFormId, recommendationCode, isReflink, returnUrl }));
        }

        let widgetData = await this.getWidgetDataProvider(directTest ?? false);

        // formId was stale (404) — create a fresh form and retry once
        if (!widgetData && formId) {
          console.warn('Direct mode: formId', formId, 'not found (404), creating new form');
          resolvedFormId = await createNewForm();
          store.dispatch(updateDirectModeState({ country, formId: resolvedFormId, recommendationCode, isReflink, returnUrl }));
          widgetData = await this.getWidgetDataProvider(directTest ?? false);
        }

        if (widgetData?.style) {
          this.setRootVariables();
        }

        this.transferData("transferDataToWidget", widgetData);
        this.transferData("transferAccountState", {
          partnerKey: '',
          partnerCustomerId: '',
          customerData: undefined,
          isDirectMode: true,
          country,
          formId: resolvedFormId,
          recommendationCode,
          isReflink,
          returnUrl,
          calculatedAmount,
        });

        markSDKInitialized();
        return widgetData;
      }

      // Partner mode
      const partnerKey = partnerKeyOrOptions;

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

      store.dispatch(updateAccountState({
        partnerKey,
        partnerCustomerId: partnerCustomerId!,
        customerData,
        isDirectMode: false,
      } as AccountState));

      const initKey = `${partnerKey}::${partnerCustomerId}`;
      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,
        isDirectMode: false,
      });

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

  async startFormProvider(test = false): Promise<boolean> {
    try {
      const state = accountState(store.getState());

      // Direct mode: use startFormDirect
      if (state.isDirectMode) {
        const newFormId = await startFormDirect({
          country: state.country || 'PL',
          recommendationCode: state.recommendationCode,
          isReflink: state.isReflink,
          returnUrl: state.returnUrl,
          rawUrl: window.location.href,
        });
        store.dispatch(setDirectFormId(newFormId));
        return true;
      }

      // Partner mode
      const { partnerKey, partnerCustomerId, customerData } = state;
      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, isDirectMode: false });
        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 state = accountState(store.getState());
        let result: InitialDataResponse;

        if (state.isDirectMode) {
          if (!state.country) return undefined;
          result = await getWidgetDataDirect({
            country: state.country,
            formId: state.formId,
            recommendationCode: state.recommendationCode,
            isReflink: state.isReflink,
            test,
          });
        } else {
          const { partnerKey, partnerCustomerId, customerData } = state;
          if (!partnerKey || !partnerCustomerId || !customerData) {
            return undefined;
          }
          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 === 404) {
          // Form not found — caller should handle (e.g. create new form)
          console.warn("Widget data provider: form not found (404)");
          return undefined;
        }
        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 };
