import { sessionKey } from "./constants";
import {
  PulseXConfig,
  EventPayload,
  SectionEngagement,
  EngagementTrackingTask,
  ClickEvent,
  HoverEvent,
  FormSubmissionEvent,
} from "./types";
import {
  generateSessionId,
  saveToLocalStorage,
  loadQueueFromLocalStorage,
  deepCopy,
} from "./utils";

export default class PulseX {
  private config: PulseXConfig;
  private sessionId: string;
  private queue: EventPayload[] = [];

  private engagementTrackingTasks: EngagementTrackingTask[] = [];

  constructor(config: PulseXConfig) {
    this.config = {
      maxQueueSize: 100,
      ...config,
    };
    const storedSession = localStorage.getItem(sessionKey);

    if (storedSession) {
      this.sessionId = storedSession;
    } else {
      this.sessionId = generateSessionId();
      localStorage.setItem(sessionKey, this.sessionId);
    }

    this.loadQueue();
  }

  public start(): void {
    this.startEngagementTracking();
    this.setupVisibilityListener();
  }

  /**
   * Tracks user engagement for a specific section of the webpage.
   *
   * @param {string} sectionId - The ID of the HTML element to track.
   * @param {number} [threshold] - The minimum time (in milliseconds) the user must view the section for it to be considered engaged.
   *
   * @remarks
   * - If the element with the given `sectionId` is not found, a warning will be logged.
   * - The section will be added to the tracking list, and engagement will be recorded only if the user views it for at least `threshold` milliseconds.
   * - If `threshold` is not provided, it may default to a predefined value in the tracking system.
   *
   * @example
   * ```ts
   * tracker.trackSectionEngagement("homepage", 3000); // Track "homepage" section with a 3-second threshold
   * ```
   */
  public trackSectionEngagement(sectionId: string, threshold?: number): void {
    const element = document.getElementById(sectionId);
    if (!element) {
      console.warn(`PulseX: Element with ID ${sectionId} not found`);
      return;
    }

    const task: EngagementTrackingTask = {
      element,
      threshold,
    };
    // Add this section to the list of sections to track
    this.engagementTrackingTasks.push(task);
  }

  private startEngagementTracking(): void {
    // Track user activity on those sections
    // like time of section on viewport, clicks on the section, hover on the section, etc.
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            const sectionId = entry.target.id;
            const data: SectionEngagement = {
              sectionId,
              startTime: Date.now(),
              endTime: 0,
              totalDuration: 0,
            };
            const payload = this.createBasePayload("section-engagement", data);
            this.queue.push(payload);
          }
          // stored the exiting time of the section after the user exits the section
          else {
            const sectionId = entry.target.id;
            const startedEvent = this.queue.find(
              (event) =>
                event.data.endTime === 0 && event.data.sectionId === sectionId
            );
            if (startedEvent) {
              startedEvent.data.endTime = Date.now();
              startedEvent.data.totalDuration =
                startedEvent.data.endTime - startedEvent.data.startTime;
            }

            // get the task for the section to calculate the threshold
            const taskIndex = this.engagementTrackingTasks.findIndex(
              (task) => task.element.id === sectionId
            );
            const task = this.engagementTrackingTasks[taskIndex];

            if (
              task &&
              startedEvent &&
              startedEvent?.data.totalDuration >= (task.threshold || 0)
            ) {
              // if the user has viewed the section for the minimum threshold time
              // then we can consider the user has engaged with the section
              this.saveQueueToLocalStorage();
            } else if (startedEvent) {
              // if the user has not viewed the section for the minimum threshold time
              // then we can remove the event from the queue
              // this.queue.splice(this.queue.indexOf(startedEvent), 1);
              this.queue.splice(taskIndex, 1);
            }
          }
        });
      },
      {
        threshold: 0.5,
      }
    );

    this.engagementTrackingTasks.forEach((task) => {
      if (task) {
        observer.observe(task.element);
      }
    });
  }

  private createBasePayload(
    type: string,
    data: SectionEngagement | ClickEvent | HoverEvent | FormSubmissionEvent
  ): EventPayload {
    return {
      _id: Math.random().toString(16).substring(2, 18),
      sessionId: this.sessionId,
      type,
      data,
      pageUrl: window.location.href,
      referrer: document.referrer,
      createdAt: new Date().toISOString(),
    };
  }

  private getQueue(): EventPayload[] {
    const queue = localStorage.getItem("pulsex_events");
    return queue ? JSON.parse(queue) : [];
  }

  private async sendData(): Promise<void> {
    const queue = this.getQueue();
    if (queue.length === 0) return;

    const eventsToSend = deepCopy(queue);
    this.clearQueue();

    console.log("PulseX: Sending data...", eventsToSend);

    try {
      const response = await fetch(this.config.apiEndpoint, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(eventsToSend),
      });

      if (!response.ok) {
        throw new Error("Failed to send data");
      }
    } catch (error) {
      console.error("PulseX: Error sending data:", error);
      this.queue.unshift(...eventsToSend);
      this.saveQueueToLocalStorage();
    }
  }

  private setupVisibilityListener(): void {
    // empty the queue when the tab is closed
    window.addEventListener("visibilitychange", () => {
      if (document.visibilityState === "hidden") {
        this.sendData();
      }
    });
  }

  private saveQueueToLocalStorage(): void {
    saveToLocalStorage(this.queue);
  }

  private clearQueue(): void {
    this.queue = [];
    saveToLocalStorage([]);
  }

  private loadQueue(): void {
    this.queue = loadQueueFromLocalStorage();
  }

  // New Methods for Additional Event Tracking

  /**
   * Tracks click events on a specified element.
   *
   * @param {string} elementId - The ID of the element to track clicks on.
   *
   * @example
   * ```ts
   * tracker.trackClick("loginBtn"); // Tracks click events on element with ID 'loginBtn'
   * ```
   */
  public trackClick(elementId: string): void {
    const element = document.getElementById(elementId);
    if (!element) {
      console.warn(`PulseX: Element with ID ${elementId} not found`);
      return;
    }
    element.addEventListener("click", (event: MouseEvent) => {
      const target = event.target as HTMLElement;
      const clickData: ClickEvent = {
        elementId: target.id,
        textContent: target.textContent || "",
        timestamp: Date.now(),
        x: event.clientX,
        y: event.clientY,
        button: event.button,
      };
      const payload = this.createBasePayload("click", clickData);
      this.savePayloadToLocalStorage(payload);
    });
  }

  /**
   * Tracks hover events on a specified element.
   * Records hover duration and, if a click occurs during the hover, includes the click data.
   *
   * @param {string} elementId - The ID of the element to track hover events on.
   *
   * @example
   * ```ts
   * tracker.trackHover("product-card"); // Tracks hover events on element with ID 'product-card'
   * ```
   */
  public trackHover(elementId: string): void {
    const element = document.getElementById(elementId);
    if (!element) {
      console.warn(`PulseX: Element with ID ${elementId} not found`);
      return;
    }

    let hoverStartTime: number = 0;
    let clickOccurred: boolean = false;
    let clickData: any = null;

    element.addEventListener("mouseover", () => {
      hoverStartTime = Date.now();
      clickOccurred = false;
      clickData = null;
    });

    element.addEventListener("click", (event: MouseEvent) => {
      clickOccurred = true;
      const target = event.target as HTMLElement;
      clickData = {
        elementId: target.id,
        textContent: target.textContent || "",
        timestamp: Date.now(),
        x: event.clientX,
        y: event.clientY,
        button: event.button,
      };
    });

    element.addEventListener("mouseout", () => {
      const hoverEndTime = Date.now();
      const hoverDuration = hoverEndTime - hoverStartTime;
      const hoverData: HoverEvent = {
        elementId,
        startTime: hoverStartTime,
        endTime: hoverEndTime,
        hoverDuration,
        clicked: clickOccurred,
        clickData: clickOccurred ? clickData : null,
      };
      const payload = this.createBasePayload("hover", hoverData);

      this.savePayloadToLocalStorage(payload);
    });
  }

  /**
   * Tracks form submission events on a specified form.
   *
   * @param {string} formId - The ID of the form to track submissions for.
   *
   * @example
   * ```ts
   * tracker.trackFormSubmission("login-form"); // Tracks form submissions on form with ID 'login-form'
   * ```
   */
  public trackFormSubmission(formId: string): void {
    const form = document.getElementById(formId) as HTMLFormElement;
    if (!form) {
      console.warn(`PulseX: Form with ID ${formId} not found`);
      return;
    }
    form.addEventListener("submit", (event: Event) => {
      event.preventDefault();
      const inputValues: Record<string, string | boolean | number> = {};
      new FormData(form).forEach((value, key) => {
        inputValues[key] = value.toString();
      });
      const formData = {
        formId,
        timestamp: Date.now(),
        inputValues,
      };
      const payload = this.createBasePayload("form-submission", formData);

      this.savePayloadToLocalStorage(payload);
    });
  }

  savePayloadToLocalStorage(payload: EventPayload) {
    const currDataInLocalStorage = this.getQueue();
    currDataInLocalStorage.push(payload);
    saveToLocalStorage(currDataInLocalStorage);
  }
}
