// Polyfill for crypto.getRandomValues() required by ULID
import 'react-native-get-random-values';

import { MpDataLayerHelper } from './eedl/eedl';
import type {
  AppCustomerInfo,
  AppPageLoad,
  MapLike,
  MpDeviceInfo,
  SdkInitOptions,
} from './common/app-types';
import { DataStore } from './common/data-store';
import { Logger } from './common/logger';
import { Reporter } from './common/reporter';
import { DataElementProcessor } from './processors/data-element.processor';
import { TagProcessor } from './processors/tag.processor';
import { Utils } from './common/utils';
import { QcProcessor } from './processors/qc.processor';
import { NetworkService } from './common/network-service';
import { EventBus } from './common/event-bus';
import { VisitIdProcessor } from './processors/visit-id.processor';
import { GeoLocationProcessor } from './processors/geo-location.processor';
import { DeviceInfoHelper } from './common/device-info-helper';
import { DeepLinkHelper } from './common/deeplink-helper';
import { StorageHelper } from './common/storage-helper';

const DL_INIT_EVENT = 'page_load';

// Maximum number of events to buffer before SDK is ready
const MAX_BUFFERED_EVENTS = 500;

// Type for buffered events
interface BufferedEvent {
  type: 'pageLoad' | 'event';
  name?: string; // event name for recordEvent
  data: any;
}

class MagicPixelImpl {
  private static dl: MpDataLayerHelper = new MpDataLayerHelper(
    'mpDlEvent',
    'manual',
    DL_INIT_EVENT,
    '_mpRnDataLayer'
  );
  private static customerInfo: AppCustomerInfo;
  private static customerIdentifiers: MapLike = {};

  private static deepLinkUrl: string | undefined = undefined;
  private static orgId: string | undefined = undefined;

  private static firstAppLaunch = true;

  // SDK readiness state and event buffer
  private static isReady = false;
  private static isInitializing = false;
  private static eventBuffer: BufferedEvent[] = [];
  private static onInitFailureCallback: ((error: Error) => void) | undefined;

  /**
   * Initialize the MagicPixel SDK
   * This method is synchronous - it stores config immediately and runs async setup in background
   * Events called before async setup completes are buffered and processed once ready
   * @param options SDK initialization options
   */
  static init(options: SdkInitOptions): void {
    // Guard against multiple init() calls
    if (this.isReady) {
      Logger.logDbg(
        'MagicPixel SDK already initialized. Ignoring duplicate init() call.'
      );
      return;
    }

    if (this.isInitializing) {
      Logger.logDbg(
        'MagicPixel SDK initialization in progress. Ignoring duplicate init() call.'
      );
      return;
    }

    this.isInitializing = true;

    // Sync setup - happens immediately
    Logger.setLogLevel(options.logLevel ?? 'none');
    this.orgId = options.orgId;
    this.onInitFailureCallback = options.onInitFailure;

    Logger.logDbg('MagicPixel SDK init started');

    // Async setup - runs in background, events are buffered until complete
    this.initAsync(options).catch((err) => {
      Logger.logError('MagicPixel SDK initialization failed:', err);
      Reporter.reportError('sdk_init', err);

      // Reset initializing flag so developer can retry if needed
      this.isInitializing = false;

      // Call failure callback if provided
      if (this.onInitFailureCallback) {
        const error = err instanceof Error ? err : new Error(String(err));
        this.onInitFailureCallback(error);
      }
    });
  }

  /**
   * Shutdown the SDK and release all resources
   * Call this when the app is closing or when you need to reinitialize the SDK
   * After shutdown, you must call init() again to use the SDK
   */
  static shutdown(): void {
    Logger.logDbg('MagicPixel SDK shutting down...');

    // Clean up deeplink listener and callback
    DeepLinkHelper.cleanup();

    // Clear all EventBus listeners to prevent duplicates on reinit
    EventBus.clearAll();

    // Clear session-scoped data element values (in-memory store)
    StorageHelper.clearSessionStore();

    // Clear event buffer
    this.eventBuffer = [];

    // Reset state flags
    this.isReady = false;
    this.isInitializing = false;
    this.onInitFailureCallback = undefined;

    // Reset customer data
    this.customerInfo = undefined as any;
    this.customerIdentifiers = {};
    this.deepLinkUrl = undefined;
    this.firstAppLaunch = true;

    // Shutdown EEDL
    this.dl.shutdown();

    Logger.logDbg('MagicPixel SDK shutdown complete');
  }

  /**
   * Async initialization - runs in background
   */
  private static async initAsync(options: SdkInitOptions): Promise<void> {
    await NetworkService.refreshClientSdkJson(options);

    if (!DataStore.isDataStoreReady()) {
      throw new Error(
        'MagicPixel SDK: DataStore not ready after config fetch. Initialization failed.'
      );
    }

    // Initialize StorageHelper for data element storage duration
    // This loads visitor-scoped values from AsyncStorage and clears expired ones (>30 days)
    await StorageHelper.initialize();

    // Auto-detect device info - always use detected values to prevent hardcoded values
    const autoDetectedInfo = await DeviceInfoHelper.getAppInfo();
    Logger.logDbg('Auto-detected device info:', autoDetectedInfo);

    // Set device type from auto-detection (Expo/react-native-device-info)
    // This is more reliable than dimension-based detection which fails on high-res phones
    const detectedDeviceType = autoDetectedInfo.is_tablet ? 'tablet' : 'mobile';
    DataStore.overrideDeviceType(detectedDeviceType);
    Logger.logDbg('Device type set from auto-detection:', detectedDeviceType);

    // Always use auto-detected app_version (ignore options.app_version)
    this.setAppVersion(autoDetectedInfo.app_version);

    // Set device info with auto-detected values (cannot be overridden)
    await this.setDeviceInfo();

    await VisitIdProcessor.init(options?.orgId);

    const fbp = await VisitIdProcessor.getFacebookFBP();
    if (fbp) {
      Logger.logDbg('Setting facebook client id', fbp);
      this.setFacebookClientId(fbp);
    } else {
      Logger.logDbg('No facebook client id found. not setting');
    }

    // Auto-detect deeplink if app was opened via deeplink (e.g., ad click)
    // Supports: custom schemes (myapp://), universal links (https://), app links, HTTP links
    const initialDeepLink = await DeepLinkHelper.initialize((url, linkType) => {
      try {
        Logger.logDbg(`Deeplink detected (${linkType}):`, url);

        // Store deeplink URL - will be attached to next page_load event
        this.setDeepLinkUrl(url);

        // Fire deeplink opened event
        // URL params will be parsed and attached to page_load event automatically
        this.recordEvent('deeplink_opened', {
          deep_link_url: url,
          link_type: linkType,
        });
      } catch (err) {
        Logger.logError('Error processing deeplink:', err);
        Reporter.reportError('deeplink_callback', err);
      }
    });

    if (initialDeepLink) {
      Logger.logDbg('App opened with initial deeplink:', initialDeepLink);
    }

    // Initialize EEDL with any global event listeners
    await this.dl.init({});

    // Set event deduplication window if specified (default is 5000ms)
    if (typeof options.eventDeduplicationWindowMs !== 'undefined') {
      this.dl.setDeduplicationWindow(options.eventDeduplicationWindowMs);
    }

    // Make geo location API call after initialization
    try {
      await GeoLocationProcessor.makeGeoLocationApiCall();
    } catch (err) {
      Logger.logError('Error making geo location API call:', err);
    }

    MagicPixelEventBus.on('mpDlEvent', async (evtName, payload) => {
      Logger.logDbg('mpDlEvent:: ', evtName, JSON.stringify(payload));
      const eventName: string = payload.eventName;
      const eventDataModel: MapLike = payload.model ?? {};
      const eventId = eventDataModel['ev._id'];

      Logger.logDbg('Tracking Event:: ', eventName, 'with id:: ', eventId);

      if (DataStore.shouldExecuteTMForEvent(eventName)) {
        // Call runTM directly - no queuing, EEDL manages sequential processing
        await this.runTM(
          false,
          `custom_event_${eventName}`,
          eventName,
          eventId,
          eventDataModel
        );
      }
    });

    const isFirstOpen = await VisitIdProcessor.isFirstOpenAfterInstall();
    if (isFirstOpen) {
      Logger.logDbg('First open event fired');
      this.recordEvent('app_first_open', {});
    }

    this.ready();

    // Mark SDK as ready and flush any buffered events
    this.isReady = true;
    this.isInitializing = false;
    Logger.logDbg('SDK is ready, flushing event buffer');
    this.flushEventBuffer();
  }

  static recordEvent(eventName: string, payload: MapLike): void {
    // Buffer event if SDK is not ready yet
    if (!this.isReady) {
      Logger.logDbg(`SDK not ready, buffering event: ${eventName}`);
      // Evict oldest event if buffer at capacity
      if (this.eventBuffer.length >= MAX_BUFFERED_EVENTS) {
        const evicted = this.eventBuffer.shift();
        Logger.logDbg(
          `Event buffer at capacity (${MAX_BUFFERED_EVENTS}), evicted oldest:`,
          evicted?.type === 'event' ? evicted.name : 'pageLoad'
        );
      }
      this.eventBuffer.push({
        type: 'event',
        name: eventName,
        data: payload,
      });
      return;
    }

    this.processRecordEvent(eventName, payload);
  }

  private static processRecordEvent(eventName: string, payload: MapLike): void {
    if (this.customerInfo) {
      const newPayload: MapLike = {
        ...payload,
        ...this.customerInfo,
        ...(this.customerIdentifiers ?? {}),
      };
      this.dl.pushEvent(eventName, newPayload);
    } else {
      const newPayload: MapLike = {
        ...payload,
        ...(this.customerIdentifiers ?? {}),
      };
      this.dl.pushEvent(eventName, newPayload);
    }
  }

  static ready(): void {
    this.dl.ready();
  }

  /**
   * Flush buffered events after SDK is ready
   * Events are processed in the order they were received
   */
  private static flushEventBuffer(): void {
    if (this.eventBuffer.length === 0) {
      Logger.logDbg('No buffered events to flush');
      return;
    }

    Logger.logDbg(`Flushing ${this.eventBuffer.length} buffered event(s)`);

    // Process all buffered events in order
    while (this.eventBuffer.length > 0) {
      const event = this.eventBuffer.shift()!;

      if (event.type === 'pageLoad') {
        Logger.logDbg(`Processing buffered page load: ${event.data.page_name}`);
        this.processRecordPageLoad(event.data);
      } else if (event.type === 'event') {
        Logger.logDbg(`Processing buffered event: ${event.name}`);
        this.processRecordEvent(event.name!, event.data);
      }
    }

    Logger.logDbg('Event buffer flushed');
  }

  static getDebugId(): string {
    return DataStore.getDebugId();
  }

  private static async runTM(
    sseOnly: boolean,
    triggerName: string,
    evtName: string,
    evtId: string,
    eventData?: Record<string, any>
  ): Promise<void> {
    try {
      Logger.logDbg(
        'Running:: ',
        sseOnly,
        triggerName,
        evtName,
        evtId,
        eventData
      );

      // process all the data elements and cache them for this run
      await DataElementProcessor.processDataElements(
        DataStore.getSdkDataElements(),
        eventData
      );

      const sdk = DataStore.getClientSdk();

      const pcKey = DataStore.getSdkPcKey();
      if (pcKey) {
        const pcVal = DataStore.getDataElementValue(pcKey);
        if (typeof pcVal !== 'undefined' && pcVal === true) {
          Logger.logDbg('Set: PR Comp: ', true);
          DataStore.setPrivacyCompliance(true);
        } else {
          Logger.logDbg('Set: PR Comp: ', false);
          DataStore.setPrivacyCompliance(false);
        }
      } else {
        // No privacy manager configured - default to true (tracking allowed)
        Logger.logDbg(
          'Set: PR Comp: ',
          true,
          '(no privacy manager configured)'
        );
        DataStore.setPrivacyCompliance(true);
      }

      if (DataStore.getSdkPageLangKey()) {
        DataStore.setDataElement(
          'page_lang_key',
          DataStore.getSdkPageLangKey()
        );
        DataStore.setPageLang(
          DataStore.getDataElementValue('page_lang_key') as string
        );
      } else {
        DataStore.setDataElement('page_lang_key', null);
      }

      //set page_browser default data element with page_os because backend expects this
      DataStore.setDataElement(
        'page_browser',
        DataStore.getOperatingSystem().toLowerCase()
      );

      Reporter.initReporter(
        sdk.s.ev,
        sdk.s.ev_id,
        DataStore.getDeviceType(),
        DataStore.getPageLang(),
        DataStore.getDeviceOs(),
        DataStore.getPageName(),
        evtName,
        evtId,
        DataStore.getClientDownStream(),
        DataStore.getCoreVersion(),
        []
      );

      // Store developer payload (base64 encoded) for report
      if (eventData) {
        Reporter.reportDevPayload(eventData, evtId);
      }

      const validQCList = QcProcessor.processQc(
        DataStore.getSdkQC(),
        evtName,
        evtId
      );

      if (validQCList?.length > 0) {
        Logger.logDbg('Found Valid QCs to process', DataStore.getValidQcInfo());

        TagProcessor.processTags(evtName, evtId)
          .then(() => {
            this._fireTM(sdk.s.ev, sdk.s.ev_id, evtName, evtId, eventData);
          })
          .catch((err) => {
            Logger.logError('Error processing tag lists.', err);
            Reporter.reportError('i::processTags', err);
            this._fireTM(sdk.s.ev, sdk.s.ev_id, evtName, evtId, eventData);
          });
      } else {
        this._fireTM(sdk.s.ev, sdk.s.ev_id, evtName, evtId, eventData);
      }
    } catch (runTMErr) {
      Logger.logError('Error in runTM', runTMErr);
      Reporter.reportError('m::runTM', runTMErr);
    }
  }

  private static _fireTM(
    envName: string,
    envId: string,
    evtName: string,
    evtId: string,
    eventData?: Record<string, any>
  ): void {
    // increment visit_depth if event name is page_load because that's the only way we can track page views in an app for now
    if (evtName === DL_INIT_EVENT) {
      // Synchronous increment - no race condition possible
      VisitIdProcessor.incrementVisitDepth();
    }

    // Continue immediately after sync increment
    this._fireTMPrivate(envName, envId, evtName, evtId, eventData);
  }

  private static _fireTMPrivate(
    envName: string,
    envId: string,
    evtName: string,
    evtId: string,
    eventData?: Record<string, any>
  ): void {
    if (DataStore.hasOneSSTTag() && DataStore.shouldFireSstForEvent(evtName)) {
      Reporter.postSST(
        DataStore.getDataElements(),
        envName,
        envId,
        DataStore.getSSTDownStream(),
        evtName,
        evtId,
        eventData
      ).catch((err) => {
        Reporter.reportError('l::postSST', err);
      });
    }
    // Note: EEDL handles queue progression via its own .finally() block
    // No need to call processNext() here - it would be redundant
  }

  static setCustomerInfo(customerInfo: AppCustomerInfo): void {
    this.customerInfo = customerInfo;
    this.recordEvent('idl_attribution_link', customerInfo);
  }

  static setFirebaseAppInstanceId(instanceId: string): void {
    this.customerIdentifiers.firebase_instance_id = instanceId;
  }

  static setDeepLinkUrl(deepLinkUrl: string): void {
    this.deepLinkUrl = deepLinkUrl;
  }

  /**
   * Set device info with auto-detected values
   * Standard fields (os_version, device_model_name, package_name) are always auto-detected
   * Optional customFields parameter allows adding custom tracking fields only
   * @param customFields Optional custom fields to add (cannot override standard fields)
   */
  static async setDeviceInfo(
    customFields?: Record<string, any>
  ): Promise<void> {
    // Always auto-detect standard device info (cannot be overridden)
    const autoDetectedInfo = await DeviceInfoHelper.getAppInfo();

    // Standard fields are always auto-detected to prevent hardcoded values
    const deviceInfo: MpDeviceInfo = {
      os_version: autoDetectedInfo.os_version,
      device_model_name: autoDetectedInfo.device_model_name,
      package_name: autoDetectedInfo.package_name,
    };

    // Add any custom fields (if provided)
    if (customFields) {
      // Filter out attempts to override standard fields
      const allowedCustomFields = { ...customFields };
      delete allowedCustomFields.os_version;
      delete allowedCustomFields.device_model_name;
      delete allowedCustomFields.package_name;

      Object.assign(deviceInfo, allowedCustomFields);

      if (
        Object.keys(allowedCustomFields).length !==
        Object.keys(customFields).length
      ) {
        Logger.logDbg(
          'Warning: Attempted to override standard device fields. Using auto-detected values instead.'
        );
      }
    }

    Logger.logDbg('Setting device info:', deviceInfo);
    DataStore.setDeviceInfo(deviceInfo);
  }

  static setFacebookClientId(fbp: string): void {
    this.customerIdentifiers.fbp = fbp;
  }

  private static setAppVersion(appVersion?: string): void {
    if (appVersion && appVersion?.trim()?.length > 0) {
      this.customerIdentifiers.app_version = appVersion;
    }
  }

  static recordPageLoad(pageLoadInfo: AppPageLoad): void {
    // Buffer page load if SDK is not ready yet
    if (!this.isReady) {
      Logger.logDbg(
        `SDK not ready, buffering page load: ${pageLoadInfo.page_name}`
      );
      // Evict oldest event if buffer at capacity
      if (this.eventBuffer.length >= MAX_BUFFERED_EVENTS) {
        const evicted = this.eventBuffer.shift();
        Logger.logDbg(
          `Event buffer at capacity (${MAX_BUFFERED_EVENTS}), evicted oldest:`,
          evicted?.type === 'event' ? evicted.name : 'pageLoad'
        );
      }
      this.eventBuffer.push({
        type: 'pageLoad',
        data: { ...pageLoadInfo }, // Clone to preserve original data
      });
      return;
    }

    this.processRecordPageLoad(pageLoadInfo);
  }

  private static processRecordPageLoad(pageLoadInfo: AppPageLoad): void {
    // Retry visitor ID fetch if it failed during init (fire and forget)
    VisitIdProcessor.retryVisitorIdIfNeeded().catch((err) => {
      Logger.logError('Error retrying visitor ID fetch:', err);
    });

    pageLoadInfo.is_entry = this.firstAppLaunch ? 1 : 0;

    // Use stored deepLinkUrl if it exists AND either:
    // - deep_link_url wasn't provided in pageLoadInfo, OR
    // - deep_link_url is empty/falsy (defensive programming for developer mistakes)
    if (this.deepLinkUrl && !pageLoadInfo.deep_link_url) {
      pageLoadInfo.deep_link_url = this.deepLinkUrl;
    }

    // Set page_url for attribution framework
    // Priority: deep_link_url (if present) > constructed URL (v_<orgId>://<screen_name>)
    // Using deep_link_url allows urlp data elements to extract attribution params (gclid, fbclid, etc.)
    if (pageLoadInfo.deep_link_url) {
      pageLoadInfo.page_url = pageLoadInfo.deep_link_url;
      Logger.logDbg(
        'Setting page_url from deep_link_url:',
        pageLoadInfo.page_url
      );
    } else if (this.orgId && pageLoadInfo.page_name) {
      pageLoadInfo.page_url = `v_${this.orgId}://${pageLoadInfo.page_name}`;
      Logger.logDbg(
        'Setting page_url from orgId/page_name:',
        pageLoadInfo.page_url
      );
    } else {
      Logger.logDbg(
        'Not setting page_url. No deep_link_url and orgId or page_name is missing. Org Id is: ',
        this.orgId,
        'page_name is: ',
        pageLoadInfo.page_name
      );
    }

    const deviceInfo = DataStore.getDeviceInfo();

    if (this.customerInfo) {
      const newPayload: MapLike = {
        ...pageLoadInfo,
        ...this.customerInfo,
        ...(this.customerIdentifiers ?? {}),
        page_params: Utils.parseQueryParamsToObject(
          pageLoadInfo?.deep_link_url
        ),
      };

      if (deviceInfo) {
        newPayload.device_info = deviceInfo;
      }

      this.dl.pushEvent('page_load', newPayload);
    } else {
      const newPayload: MapLike = {
        ...pageLoadInfo,
        ...(this.customerIdentifiers ?? {}),
        page_params: Utils.parseQueryParamsToObject(
          pageLoadInfo?.deep_link_url
        ),
      };

      if (deviceInfo) {
        newPayload.device_info = deviceInfo;
      }

      this.dl.pushEvent('page_load', newPayload);
    }
    // only set for first page load event and remove after that
    this.deepLinkUrl = undefined;
    this.firstAppLaunch = false;
  }

  /**
   * Set user information for tracking
   * @param userInfo User information object
   */
  static setUserInfo(userInfo: {
    pid?: string;
    email?: string;
    phone?: string;
    country?: string;
    city?: string;
    state?: string;
    fName?: string;
    lName?: string;
    zip?: string;
  }): void {
    this.dl.pushEvent('user_info', userInfo);
  }

  /**
   * Persist custom data to storage
   * @param key Storage key
   * @param value Value to store
   */
  static persistData(key: string, value: any): void {
    this.dl.pushEvent('persist', { key, value });
  }

  /**
   * Clear specific persisted data
   * @param keys Array of keys to clear
   */
  static clearPersistedData(keys: string[]): void {
    this.dl.pushEvent('clear', keys);
  }

  /**
   * Set data layer variables
   * @param data Data to set in the data layer
   */
  static setDataLayer(data: MapLike): void {
    this.dl.pushEvent('set', data);
  }

  /**
   * Register a custom event processor
   * @param eventType Event type to listen for
   * @param processor Function to process the event
   */
  static registerEventProcessor(
    eventType: string,
    processor: (payload: any) => void
  ): void {
    this.dl.registerProcessor(eventType, processor);
  }

  /**
   * Get current data layer state
   * @returns Current data layer object
   */
  static getDataLayer(): MapLike {
    return this.dl.getDl();
  }

  /**
   * Get event state tracker
   * @returns Array of all tracked events
   */
  static getEventState(): any[] {
    return this.dl.getState();
  }

  /**
   * Check if a specific event has happened
   * @param eventName Event name to check
   * @returns True if event has occurred
   */
  static hasEventHappened(eventName: string): boolean {
    return this.dl.hasEventHappened(eventName);
  }
}

export const MagicPixelEventBus = EventBus;
export const MagicPixel = MagicPixelImpl;
export { DeepLinkHelper, DeepLinkType } from './common/deeplink-helper';
