"use client"
import isEqual from "fast-deep-equal"
import { usePathname } from "next/navigation"
import { startTransition, useEffect, useRef, useState } from "react"
import {
  type Control,
  type FieldErrors,
  type FieldValues,
  useFormState,
  type UseFromSubscribe,
} from "react-hook-form"

import { trpc } from "@/lib/trpc/client"
import useRouterTransitionStore from "@/stores/router-transition"
import useTrackingStore from "@/stores/tracking"

import useLang from "@/hooks/useLang"
import { useSessionId } from "@/hooks/useSessionId"
import { promiseWithTimeout } from "@/utils/promiseWithTimeout"
import { createSDKPageObject, trackPageView } from "@/utils/tracking"
import {
  type FormType,
  trackFormAbandonment,
  trackFormInputStarted,
  trackFormValidationError,
} from "@/utils/tracking/form"

import type {
  TrackingSDKProps,
  TrackingSDKUserData,
} from "@/types/components/tracking"

enum TransitionStatusEnum {
  NotRun = "NotRun",
  Running = "Running",
  Done = "Done",
}

type TransitionStatus = keyof typeof TransitionStatusEnum

let hasTrackedHardNavigation = false
export const useTrackHardNavigation = ({
  pageData,
  hotelInfo,
  paymentInfo,
  ancillaries,
}: TrackingSDKProps) => {
  const lang = useLang()
  const {
    data: userTrackingData,
    isPending,
    isError,
  } = trpc.user.userTrackingInfo.useQuery({ lang })

  const sessionId = useSessionId()
  const pathName = usePathname()

  useEffect(() => {
    if (isPending) {
      return
    }

    const userData: TrackingSDKUserData = isError
      ? { loginStatus: "Error" }
      : userTrackingData

    if (hasTrackedHardNavigation) {
      return
    }

    hasTrackedHardNavigation = true

    const track = () => {
      trackPerformance({
        pathName,
        sessionId,
        paymentInfo,
        hotelInfo,
        userData,
        pageData,
        ancillaries,
      })
    }

    if (document.readyState === "complete") {
      track()
      return
    }

    window.addEventListener("load", track)
    return () => window.removeEventListener("load", track)
  }, [
    isError,
    pathName,
    hotelInfo,
    userTrackingData,
    pageData,
    sessionId,
    paymentInfo,
    isPending,
    ancillaries,
  ])
}

export const useTrackSoftNavigation = ({
  pageData,
  hotelInfo,
  paymentInfo,
  ancillaries,
}: TrackingSDKProps) => {
  const lang = useLang()
  const {
    data: userTrackingData,
    isPending,
    isError,
  } = trpc.user.userTrackingInfo.useQuery({ lang })

  const [status, setStatus] = useState<TransitionStatus>(
    TransitionStatusEnum.NotRun
  )
  const { getPageLoadTime } = useTrackingStore()

  const sessionId = useSessionId()
  const pathName = usePathname()
  const { isTransitioning, stopRouterTransition } = useRouterTransitionStore()

  const previousPathname = useRef<string | null>(null)

  useEffect(() => {
    if (isPending) {
      return
    }

    if (isTransitioning && status === TransitionStatusEnum.NotRun) {
      startTransition(() => {
        setStatus(TransitionStatusEnum.Running)
      })
      return
    }

    if (isTransitioning && status === TransitionStatusEnum.Running) {
      setStatus(TransitionStatusEnum.Done)
      stopRouterTransition()
      return
    }

    if (!isTransitioning && status === TransitionStatusEnum.Done) {
      const pageLoadTime = getPageLoadTime()
      const trackingData = {
        ...pageData,
        sessionId,
        pathName,
        pageLoadTime: pageLoadTime,
      }
      const pageObject = createSDKPageObject(trackingData)
      const userData: TrackingSDKUserData = isError
        ? { loginStatus: "Error" }
        : userTrackingData

      trackPageView({
        event: "pageView",
        pageInfo: pageObject,
        userInfo: userData,
        hotelInfo: hotelInfo,
        paymentInfo,
        ancillaries,
      })

      setStatus(TransitionStatusEnum.NotRun) // Reset status
      previousPathname.current = pathName // Update for next render
    }
  }, [
    isError,
    isPending,
    isTransitioning,
    status,
    stopRouterTransition,
    pageData,
    pathName,
    hotelInfo,
    getPageLoadTime,
    sessionId,
    paymentInfo,
    userTrackingData,
    ancillaries,
  ])
}

const trackPerformance = async ({
  pathName,
  sessionId,
  paymentInfo,
  hotelInfo,
  userData,
  pageData,
  ancillaries,
}: {
  pathName: string
  sessionId: string | null
  paymentInfo: TrackingSDKProps["paymentInfo"]
  hotelInfo: TrackingSDKProps["hotelInfo"]
  userData: TrackingSDKUserData
  pageData: TrackingSDKProps["pageData"]
  ancillaries: TrackingSDKProps["ancillaries"]
}) => {
  let pageLoadTime: number | undefined = undefined
  let lcpTime: number | undefined = undefined

  try {
    pageLoadTime = await promiseWithTimeout(getPageLoadTimeEntry(), 3000)
  } catch (error) {
    console.error("Error obtaining pageLoadTime:", error)
  }

  try {
    lcpTime = await promiseWithTimeout(getLCPTimeEntry(), 3000)
  } catch (error) {
    console.error("Error obtaining lcpTime:", error)
  }

  const trackingData = {
    ...pageData,
    sessionId,
    pathName,
    pageLoadTime,
    lcpTime,
  }
  const pageObject = createSDKPageObject(trackingData)

  trackPageView({
    event: "pageView",
    pageInfo: pageObject,
    userInfo: userData,
    hotelInfo,
    paymentInfo,
    ancillaries,
  })
}

const getLCPTimeEntry = () => {
  return new Promise<number | undefined>((resolve) => {
    const observer = new PerformanceObserver((entries) => {
      const lastEntry = entries.getEntries().at(-1)
      if (lastEntry) {
        observer.disconnect()
        resolve(lastEntry.startTime / 1000)
      }
    })

    const lcpSupported = PerformanceObserver.supportedEntryTypes?.includes(
      "largest-contentful-paint"
    )

    if (lcpSupported) {
      observer.observe({
        type: "largest-contentful-paint",
        buffered: true,
      })
    } else {
      resolve(undefined)
    }
  })
}

const getPageLoadTimeEntry = () => {
  return new Promise<number>((resolve) => {
    const observer = new PerformanceObserver((entries) => {
      const navEntry = entries.getEntriesByType("navigation")[0]
      if (navEntry) {
        observer.disconnect()
        resolve(navEntry.duration / 1000)
      }
    })
    observer.observe({ type: "navigation", buffered: true })
  })
}

export function useFormTracking<T extends FieldValues>(
  formType: FormType,
  subscribe: UseFromSubscribe<T>,
  control: Control<T>,
  nameSuffix: string = ""
) {
  const [formStarted, setFormStarted] = useState(false)
  const lastAccessedField = useRef<string | undefined>(undefined)
  const formState = useFormState({ control })
  const previousErrors = useRef<FieldErrors<T>>({})

  useEffect(() => {
    const errors = formState.errors
    const prevErrors = previousErrors.current

    const isNewErrors = !isEqual(errors, prevErrors)
    if (Object.keys(errors).length && isNewErrors) {
      const errorString = Object.values(errors)
        .map((err) => {
          if (err && "message" in err) {
            return err.message
          }
          const nested = Object.values(err ?? {}).find(
            (val) => val && typeof val === "object" && "message" in val
          )
          if (nested) {
            return nested.message
          }
          return undefined
        })
        .filter(Boolean)
        .join("|")

      trackFormValidationError(formType, errorString, nameSuffix)
      previousErrors.current = { ...errors }
    }
  }, [formType, formState, nameSuffix])

  useEffect(() => {
    const unsubscribe = subscribe({
      formState: { touchedFields: true },
      callback: (data) => {
        if ("name" in data) {
          lastAccessedField.current = data.name as string
        }

        if (!formStarted) {
          trackFormInputStarted(formType, nameSuffix)
          setFormStarted(true)
        }
      },
    })
    return () => unsubscribe()
  }, [subscribe, formType, nameSuffix, formStarted])

  useEffect(() => {
    if (!formStarted || !lastAccessedField.current) return

    const lastField = lastAccessedField.current

    function handleBeforeUnload() {
      trackFormAbandonment(formType, lastField, nameSuffix)
    }

    function handleVisibilityChange() {
      if (document.visibilityState === "hidden") {
        trackFormAbandonment(formType, lastField, nameSuffix)
      }
    }

    window.addEventListener("beforeunload", handleBeforeUnload)
    window.addEventListener("visibilitychange", handleVisibilityChange)

    return () => {
      window.removeEventListener("beforeunload", handleBeforeUnload)
      window.removeEventListener("visibilitychange", handleVisibilityChange)
    }
  }, [formStarted, formType, nameSuffix])
}
