console.log(`[qc] script loaded`)

// Which checkout UI this store is on (legacy vs redesign), injected server-side so we can
// tag every PostHog event/exception with it. See entity.StoreCheckoutConfig.IsRedesignUIEnabled.
const qcIsRedesignUI = `false` === 'true'
const qcCheckoutVersion = qcIsRedesignUI ? 'redesign' : 'legacy'

const initializePosthog = () => {
  !function(t,e){var o,n,p,r;e.__SV||(window.posthog && window.posthog.__loaded)||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init rs ls wi ns us ts ss capture calculateEventProperties vs register register_once register_for_session unregister unregister_for_session gs getFeatureFlag getFeatureFlagPayload getFeatureFlagResult isFeatureEnabled reloadFeatureFlags updateFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSurveysLoaded onSessionId getSurveys getActiveMatchingSurveys renderSurvey displaySurvey cancelPendingSurvey canRenderSurvey canRenderSurveyAsync identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException startExceptionAutocapture stopExceptionAutocapture loadToolbar get_property getSessionProperty fs ds createPersonProfile setInternalOrTestUser ps Qr opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing get_explicit_consent_status is_capturing clear_opt_in_out_capturing hs debug M cs getPageViewId captureTraceFeedback captureTraceMetric Kr".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
    posthog.init('phc_jYUzT3zEYG4ZdnGs6jFi5cSnl06IM9Wx2rQ7CPtVB7R', {
        api_host: 'https://us.i.posthog.com',
        defaults: '2026-01-30',
        person_profiles: 'identified_only',
        autocapture: false,
        capture_pageview: false,
        capture_pageleave: false,
        disable_session_recording: true,
        capture_exceptions: false,
        disable_surveys: true,
        capture_heatmaps: false,
        enable_heatmaps: false,
        capture_dead_clicks: false,
        capture_performance: false,
        loaded: (posthog) => {
            posthog.register({
                app_name: 'checkout',
                checkout_version: qcCheckoutVersion,
                is_redesign_ui_enabled: qcIsRedesignUI,
            })
        },
    })
}

initializePosthog();

// ============================================================================
// ERROR HANDLING & LOGGING
// ============================================================================

// Simple logger for bigcommerce script
const logger = {
  debug: (message, data) => {
    console.debug(`[qc] ${message}`, data !== undefined ? data : '')
  },
  info: (message, data) => {
    console.log(`[qc] ${message}`, data !== undefined ? data : '')
  },
  warn: (message, data) => {
    console.warn(`[qc] ${message}`, data !== undefined ? data : '')
  },
  error: (message, error, data) => {
    console.error(`[qc] ${message}`, error || '', data !== undefined ? data : '')

    // Capture to PostHog via the SDK's captureException so the event is built with
    // the proper $exception_list schema. A hand-rolled capture('$exception', {...})
    // with only $exception_type/$exception_message has no $exception_list, so PostHog
    // can't form an issue from it and the event fails to ingest.
    if (typeof posthog !== 'undefined' && posthog.captureException) {
      try {
        const errorObj = error instanceof Error ? error : new Error(String(error || message))
        posthog.captureException(errorObj, {
          source: 'bigcommerce_script',
          app_name: 'checkout',
          additional_context: data,
          script_url: window.location.href,
        })
      } catch (captureError) {
        console.error('[qc] Failed to capture error to PostHog:', captureError)
      }
    }
  }
}

// NOTE: We intentionally do NOT install global window.onerror /
// window.onunhandledrejection handlers here. This script is injected into the
// merchant's top-level storefront page (not an isolated iframe), so a global handler
// captures the merchant's own theme + third-party + browser errors, not ours. That
// flooded PostHog with ~175K malformed $exception events (most days 0, bursting to
// ~100K/day during merchant/Shopify error waves) and dropped crash-free sessions to
// ~37%. Report QuickCheckout's own errors explicitly via logger.error() inside our
// try/catch blocks instead.
// See .claude/docs/investigations/2026-06-15-posthog-shopify-script-exception-flood.md

// Report a checkout-open failure as a single grouped PostHog issue ("FailedToOpenCheckout")
// plus the human-readable "why". logger.error routes to posthog.captureException with the
// proper $exception_list schema and source/app_name tags.
function reportFailedToOpenCheckout(reason, originalError, context) {
  // Dedup: a deeper layer may already have reported this exact failure before it bubbled
  // up to a generic catch. Mark the error so we report it once.
  if (originalError && originalError.qcReported) return
  if (originalError) originalError.qcReported = true

  const err = new Error(`Failed to open checkout: ${reason}`)
  err.name = 'FailedToOpenCheckout'
  if (originalError && originalError.stack) err.stack = originalError.stack
  logger.error('Failed to open checkout', err, {
    reason,
    original_error: originalError ? originalError.message || String(originalError) : undefined,
    ...context,
  })
}

// Guarded session-recording start/stop so we capture the replay of a failing checkout open.
function startCheckoutRecording(ctx) {
  if (typeof posthog !== 'undefined' && posthog.startSessionRecording) {
    try {
      posthog.startSessionRecording()
      logger.info('Session recording started', ctx)
    } catch (e) {
      logger.error('Failed to start session recording', e)
    }
  }
}

function stopCheckoutRecording(ctx) {
  if (typeof posthog !== 'undefined' && posthog.stopSessionRecording) {
    try {
      posthog.stopSessionRecording()
      logger.info('Session recording stopped', ctx)
    } catch (e) {
      logger.error('Failed to stop session recording', e)
    }
  }
}

// Mirror an "expected" native-checkout fallback into PostHog as an analytics event (NOT
// an exception) so expected fallbacks and real failures are both visible in one place.
// These run alongside sendMonitoringPostRequest (which still feeds Slack + the cases DB).
function trackCheckoutFallback(reason, context) {
  if (typeof posthog !== 'undefined' && posthog.capture) {
    try {
      posthog.capture('checkout_fallback', {
        reason,
        source: 'bigcommerce_script',
        app_name: 'checkout',
        ...context,
      })
    } catch (e) {
      logger.error('Failed to capture checkout_fallback', e)
    }
  }
}

{
  const userAgent = navigator.userAgent

  // polyfill event.path for Safari and Firefox
  if (!('path' in Event.prototype)) {
    Object.defineProperty(Event.prototype, 'path', {
      get: function () {
        var path = []
        var currentElem = this.target
        while (currentElem) {
          path.push(currentElem)
          currentElem = currentElem.parentElement
        }
        if (path.indexOf(window) === -1 && path.indexOf(document) === -1)
          path.push(document)
        if (path.indexOf(window) === -1) path.push(window)
        return path
      },
    })
  }

  // used to send logs in case of default chackout opening
  // CreateCheckoutMonitoringCaseRequestBody
  // requestBodyType = { storeId: string, reason: string, isExpected?: boolean, details?: string }
  async function sendMonitoringPostRequest(requestBody) {
    // this constructor is needed solely for PR environments
    // prod env should always have the same url
    // 'https://api.qcpg.cc/api/v2/checkouts/monitoring'

    const checkoutUrl = `https://checkout.qcpg.cc`
    const regex = /https:\/\/checkout\.(.*?)\.qcpg\.cc/
    const match = checkoutUrl.match(regex)
    let apiEnv = ''

    if (match && match[1]) {
      apiEnv = match[1] + '.'
    }

    const url = 'https://api.' + apiEnv + 'qcpg.cc/api/v2/checkouts/monitoring'

    return fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(requestBody),
    })
      .then((response) => {
        if (!response.ok) {
          throw new Error(`HTTP error! Status: ${response.status}`)
        }
        return response.json()
      })
      .catch((error) => {
        console.error('Monitoring request error:', error)
      })
  }

  const removeHashFromUrl = (url) => {
    const urlObj = new URL(url)
    urlObj.hash = ''
    return urlObj.toString()
  }

  // function to retrieve GA params from url
  const getGAUrlParams = () => {
    const searchParams = window.location.href.split('?')[1]

    const gclid = new URLSearchParams(searchParams).get('gclid')
    const utm_id = new URLSearchParams(searchParams).get('utm_id')
    const utm_campaign = new URLSearchParams(searchParams).get('utm_campaign')
    const utm_source = new URLSearchParams(searchParams).get('utm_source')
    const utm_medium = new URLSearchParams(searchParams).get('utm_medium')
    const utm_content = new URLSearchParams(searchParams).get('utm_content')
    const utm_term = new URLSearchParams(searchParams).get('utm_term')

    return {
      ...(gclid && { gclid }),
      ...(utm_id && { utm_id }),
      ...(utm_campaign && { utm_campaign }),
      ...(utm_source && { utm_source }),
      ...(utm_medium && { utm_medium }),
      ...(utm_content && { utm_content }),
      ...(utm_term && { utm_term }),
    }
  }

  const setGaParamsToCookie = (gaParams) => {
    // set cookie for each parameter of gaParams object
    // where ket is the name of the cookie and value is the value of the cookie
    for (const [key, value] of Object.entries(gaParams)) {
      document.cookie = `${key}=${value};path=/;max-age=31536000;`
    }
  }

  const getGaParamsFromCookie = () => {
    const gaParams = {}

    // get all cookies
    const cookies = document.cookie.split(';')

    // iterate through cookies
    for (let i = 0; i < cookies.length; i++) {
      const cookie = cookies[i].trim()

      // check if cookie name starts with 'utm_'
      if (cookie.startsWith('utm_') || cookie.startsWith('gclid')) {
        const cookieName = cookie.split('=')[0]
        const cookieValue = cookie.split('=')[1]

        // add cookie to gaParams object
        gaParams[cookieName] = cookieValue
      }
    }

    return gaParams
  }

  // function to convert object to string which is passed to iframe source
  const paramsObjectToString = (object) => {
    let string = ''

    if (!object) return string

    for (const [key, value] of Object.entries(object)) {
      string += `&${key}=${value}`
    }

    return string
  }

  const gaUrlParams = getGAUrlParams()
  setGaParamsToCookie(gaUrlParams)

  //func to get GA client_id from cookie directly if gtag function failed
  function getGaClientIdFromCookies() {
    const gaClientIdCookieValue = document.cookie
      .split('; ')
      .find((row) => row.startsWith('_ga'))
      ?.split('=')[1]

    const regex = /GA1\.1\.(\d+\.\d+)/
    const match = gaClientIdCookieValue?.match(regex)

    if (match && match[1]) {
      console.debug('[qc] extracted client_id directly from cookie:', match?.[1])
      return match[1] // client_id
    } else {
      console.debug('[qc] failed to get ga client_id from cookie')
      return ''
    }
  }

  //func to get GA session_id from cookie directly if gtag function failed
  function getGaSessionIdFromCookies() {
    const id = ``.slice(2)
    const pattern = new RegExp('_ga_' + id + '=GS\\d\\.\\d\\.(.+?)(?:;|$)')
    const match = document.cookie.match(pattern)
    const parts = match?.[1].split('.')

    if (!parts) {
      console.debug('[qc] failed to get ga session_id from cookie')
      return ''
    }

    console.debug('[qc] extracted ga session_id directly from cookie:', parts?.[0])
    return parts?.[0] // session id
  }

  let client_id = ''
  let session_id = ''
  //attempt to extract gaClientId with gtag function
  setTimeout(() => {
    console.debug('[qc] attempting to extract client_id and session_id with gtag')

    gtag('get', ``, 'client_id', (id) => {
      client_id = id
      console.debug('[qc] successfuly extracted client_id with gtag:', gaClientId)
    })

    gtag('get', ``, 'session_id', (id) => {
      session_id = id
      console.debug('[qc] successfuly extracted session_id with gtag:', session_id)
    })
  }, 1500)

  // helper functions

  /**
   * redirectToNativeCheckout is used to redirect to the native checkout
   * @param checkoutUrl string | undefined checkout url
   */
  const redirectToNativeCheckout = (checkoutUrl) => {
    // Default fallback to checkout page
    if (!checkoutUrl) {
      window.location.href = '/checkout'
      return
    }
    window.location.href = checkoutUrl
  }


  /**
   * getCart is used to get user cart contents
   * @return {Promise<Array<{ variantId: number, quantity: number, productId: number }>> | undefined}
   */
  const getCart = async () => {
    const resp = await fetch('/api/storefront/carts')
    const cart = await resp.json()

    try {
      if (!cart) {
        throw new Error('No cart found')
      }

      const physicalItems = cart[0].lineItems.physicalItems
      const digitalItems = cart[0].lineItems.digitalItems
      const giftCertificates = cart[0].lineItems.giftCertificates
      const customItems = cart[0].lineItems.customItems

      // check for digital products
      if (digitalItems.length !== 0 && physicalItems.length === 0) {
        trackCheckoutFallback('cart contains only digital products', {
          flow: 'checkout',
          expected: true,
        })
        sendMonitoringPostRequest({
          storeId: `76fd4027-cad9-4fd8-ae63-d8eebde4b19e`,

          reason: 'cart contains only digital products',
          isExpected: true,
          details: JSON.stringify({
            products: digitalItems,
            userAgent,
          }),
        })

        throw new Error('cart contains only digital products')
      }

      // check for gift certificates
      if (giftCertificates.length !== 0) {
        trackCheckoutFallback('order contains gift certificates', {
          flow: 'checkout',
          expected: true,
        })
        sendMonitoringPostRequest({
          storeId: `76fd4027-cad9-4fd8-ae63-d8eebde4b19e`,
          reason: 'order contains gift certificates',
          isExpected: true,
          details: JSON.stringify({
            products: [...physicalItems, ...digitalItems],
            userAgent,
          }),
        })

        throw new Error('gift cerificates detected')
      }

      // check for custom items
      if (customItems.length !== 0) {
        trackCheckoutFallback('order contains custom items', {
          flow: 'checkout',
          expected: true,
        })
        sendMonitoringPostRequest({
          storeId: `76fd4027-cad9-4fd8-ae63-d8eebde4b19e`,
          reason: 'order contains custom items',
          isExpected: true,
          details: JSON.stringify({
            products: customItems,
            userAgent,
          }),
        })

        throw new Error('custom items detected')
      }

      // return line items
      return {vendorCartId: cart[0].id, lineItems: physicalItems.map((t) => ({
        variantId: t.variantId.toString(),
        productId: t.productId.toString(),
        quantity: t.quantity,
      }))}
    }
    catch (error) {
      console.error('[qc] getting cart error:', error.message)

      sendMonitoringPostRequest({
        storeId: `76fd4027-cad9-4fd8-ae63-d8eebde4b19e`,
        reason: `[script]: /api/storefront/carts fetch request failed with error: ${error.message}`,
        details: JSON.stringify({ userAgent }),
      })
      reportFailedToOpenCheckout(error.message, error, {
        storeId: `76fd4027-cad9-4fd8-ae63-d8eebde4b19e`,
        userAgent,
        flow: 'cart_fetch',
      })

      // Rethrow as already-reported; handleCheckoutClick owns the native-checkout redirect.
      error.qcReported = true
      throw error
    }
  }

  /**
   * openCheckout is used to open QuickCheckout page popup
   * @param products Array<{ variantId: number, productId: number, quantity: number}> list of products
   * @param vendorCartId string vendor cart id
   * @param isBuyNow boolean whether this is a buy now flow
   */
  const openCheckout = ({products, vendorCartId, isBuyNow = false}) => {
    // create iframe
    let iframe = document.createElement('iframe')

    // remove search params from location.href and location.search to prevent parsing issues
    const hrefRaw = window.location.href.split('?')[0]
    const href = removeHashFromUrl(hrefRaw)

    // check if gaClientId and gaSessionId are already extracted with gtag
    // if not, try to extract them from cookies
    if (client_id === '') {
      client_id = getGaClientIdFromCookies() // streing value
    }

    if (session_id === '') {
      session_id = getGaSessionIdFromCookies() // streing value
    }

    const gaParams = getGaParamsFromCookie()

    // it any GA params found, pass them to iframe later
    const gaParamsString = gaParams !== '{}' ? paramsObjectToString(gaParams) : ''
    const gaClientIdString = client_id !== '' ? `&client_id=${client_id}` : ''
    const gaSessionIdString = session_id !== '' ? `&session_id=${session_id}` : ''

    const src = `https://checkout.qcpg.cc/delivery?storeId=76fd4027-cad9-4fd8-ae63-d8eebde4b19e&href=${href}&products=${JSON.stringify(
      products,
    )}${gaParamsString}${gaClientIdString}${gaSessionIdString}`
    iframe.src = !vendorCartId ? src : src + '&vendorCartId=' + vendorCartId

    // check Google Analytics
    if (window.ga) {
      // get track id
      ga(function (tracker) {
        iframe.src += '&clientId=' + tracker.get('clientId')
      })

      // get Google analytics "_ga" param for cross domain tracking
      if (window.gaplugins) {
        const tracker = window.ga.getAll()[0]
        const linker = new window.gaplugins.Linker(tracker)
        iframe.src = linker.decorate(iframe.src)
      }
    }

    // set iframe styles
    iframe.style.cssText =
      'width: 100%; height:100%; position:fixed; top:0; left:0; z-index:10000000000; border:none;'
    document.head.style.display = 'block'
    document.body.style.overflow = 'hidden'

    // listen to messages from iframe
    window.addEventListener('message', (message) => {
      console.debug('received message', { message })
      const { type, reason, products, redirectUrl, errorMessage } =
        typeof message.data === 'string' ? JSON.parse(message.data) : message.data || {}

      if (type === 'close') {
        console.log('closing checkout')
        stopCheckoutRecording({ reason: 'checkout closed' })
        document.body.removeChild(iframe)
        document.body.style.overflow = 'auto'
      } else if (type === 'error') {
        console.error('closing checkout, because error occurred')

        // Track the failed open in PostHog (error tracking + linked replay)
        reportFailedToOpenCheckout(reason || 'checkout iframe error', undefined, {
          errorMessage,
          storeId: `76fd4027-cad9-4fd8-ae63-d8eebde4b19e`,
          userAgent,
          flow: 'iframe',
        })
        stopCheckoutRecording({ reason: 'checkout error', error_reason: reason, errorMessage })

        document.body.removeChild(iframe)
        document.body.style.overflow = 'auto'
        console.debug('[qc] redirecting to the native checkout because of error')

        // do not send monitoring request if error is caused by create checkout request
        if (reason !== '[app] Create checkout request failed') {
          sendMonitoringPostRequest({
            storeId: `76fd4027-cad9-4fd8-ae63-d8eebde4b19e`,
            reason: reason + ` => Details: ${errorMessage}`,
            isExpected: false,
            details: JSON.stringify({ products, userAgent }),
          }).finally(() => {
            if (isBuyNow && redirectUrl) {
              redirectToNativeCheckout(redirectUrl)
            } else {
              redirectToNativeCheckout()
            }
          })
        }

        if (isBuyNow && redirectUrl) {
          redirectToNativeCheckout(redirectUrl)
        } else {
          redirectToNativeCheckout()
        }
      } else if (type === 'open_default') {
        console.log('open_default', { redirectUrl })
        stopCheckoutRecording({ reason: 'redirecting to default checkout' })
        document.body.removeChild(iframe)
        document.body.style.overflow = 'auto'
        console.debug('[qc] redirecting to the native checkout')

        trackCheckoutFallback(reason, { flow: 'iframe', expected: true })
        sendMonitoringPostRequest({
          storeId: `76fd4027-cad9-4fd8-ae63-d8eebde4b19e`,
          reason,
          isExpected: true,
          details: JSON.stringify({ products, userAgent }),
        }).finally(() => {
          if (isBuyNow && redirectUrl) {
            redirectToNativeCheckout(redirectUrl)
          } else {
            redirectToNativeCheckout()
          }
        })
      } else if (type === 'purchase') {
        fetch(`/api/storefront/carts/${vendorCartId}`, {
          method: 'delete',
        })
      }
    })

    // add iframe to document
    document.body.appendChild(iframe)
  }


    /**
   * getProductByIdAndOptions is used to get product by id and options
   * @param productId (string | number)
   * @param optionValueIds Array<{ optionEntityId: string, valueEntityId: string }>
   * @return {Promise<{ data: { site: { product: { variants: { edges: { node: { entityId: string }[] } } } } } }>}
   */
  async function fetchProductByIdAndOptions(productId, optionValueIds) {
    // docs https://developer.bigcommerce.com/graphql-storefront/playground
    const query = `
    query productById {
      site {
        product(entityId: ${productId}, useDefaultOptionSelections: true) {
          type
          variants(optionValueIds: ${JSON.stringify(optionValueIds).replace(/["']/g, "")}) {
            edges {
              node {
                entityId
              }
            }
          }
        }
      }
    }`;

    const response = await fetch('/graphql', {
      method: 'POST',
      credentials: 'same-origin',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer `,
      },
      body: JSON.stringify({ query }),
    });

    if (!response.ok) {
      throw new Error(`GraphQL error: ${response.status} - ${response.statusText}`);
    }

    return response.json(); // returns the full JSON data
  }

  const getProductTypeFromGraphql = (res) => res.data?.site?.product?.type

  /**
   * parseProductForm is used to parse the product form
   * @param form FormData
   * @return {{ productId: string, optionValueIds: Array<{ optionEntityId: string, valueEntityId: string }>, quantity: number }>}
   */
  const parseProductFormParams = (entries) => {
    let productId;
    let optionValueIds = [];
    let quantity = 1;

    for (const [key, value] of entries) {
      if (key === 'product_id') {
        productId = value
      } else if (key.startsWith('attribute[')) {
        optionValueIds.push({
          optionEntityId: key.split('[')[1].split(']')[0],
          valueEntityId: value,
        })
      } else if (key === 'qty[]') {
        // value as integer
        quantity = parseInt(value, 10)
      }
    }

    return { productId, optionValueIds, quantity }
  }

  function parseBuyNowLinkParams(queryString) {
    // strip leading '?'
    const query = queryString[0] === '?' ? queryString.slice(1) : queryString;
    const params = new URLSearchParams(query);
    
    // We'll collect data similarly to parseProductForm
    let productId;
    let quantity = 1;
    const optionValueIds = [];
  
    for (const [key, value] of params.entries()) {
      if (key === 'product_id') {
        productId = value;
      } else if (key.startsWith('attribute[')) {
        // e.g. attribute[108] => 108
        const optionEntityId = key.match(/^attribute\[(\d+)\]$/)?.[1];
        if (optionEntityId) {
          optionValueIds.push({
            optionEntityId,
            valueEntityId: value,
          });
        }
      } else if (key === 'qty' || key === 'qty[]') {
        quantity = value;
      }
    }
  
    return { productId, optionValueIds, quantity };
  }

  function getCartForm() {
    return document.querySelector('form[action*="cart.php"]');
  }

  function getCartLink() {
    return document.querySelector('a[href*="cart.php"]');
  }

  /**
   * redirectToBuyNow is used to redirect to the native buy now flow for a specific product
   * @param productId string | number product id
   * @param quantity number quantity of the product
   * @param optionValueIds Array<{ optionEntityId: string, valueEntityId: string }> optional product options
   */
  const redirectToBuyNow = (productId, quantity = 1, optionValueIds = []) => {
    console.debug(`[qc] redirecting to native buy now for product ${productId}`)
    
    const url = new URL('/cart.php', window.location.origin)
    url.searchParams.append('action', 'buy')
    url.searchParams.append('product_id', productId)
    
    // Add option values if available
    if (optionValueIds && optionValueIds.length > 0) {
      optionValueIds.forEach(option => {
        url.searchParams.append(`attribute[${option.optionEntityId}]`, option.valueEntityId)
      })
    }
    
    url.searchParams.append('qty', quantity)
    
    window.location.href = url.toString()
  }

  /**
   * Checks if the product form is valid using native browser validation
   * @param {HTMLFormElement} form - The product form element
   * @return {boolean} - Whether the form is valid
   */
  const isProductFormValid = (form) => {
    if (!form || typeof form.checkValidity !== 'function') {
      return true;
    }
    
    return form.checkValidity();
  }

  const handleBuyNowClick = async (event) => {
    event.stopImmediatePropagation()
    event.preventDefault()

    let parsedParams;
      
    try {
      // form for selecting product options
      const cartForm = getCartForm()
      
      // Check form validity if it's a form-based buy now
      if (cartForm) {
        if (!isProductFormValid(cartForm)) {
          console.debug('[qc] product form validation failed, letting native handler take over')
          const newEvent = new MouseEvent(event.type, {
            bubbles: true,
            cancelable: true,
            view: window,
            detail: event.detail,
          });
          
          // newEvent._qcProcessed = true;
          event.target.dispatchEvent(newEvent);
          return;
        }
        
        const entries = Array.from(
          new FormData(cartForm).entries(),
        )
        parsedParams = parseProductFormParams(entries) 
      }
      else {
        const cartLink = getCartLink()
        if (cartLink){
          parsedParams = parseBuyNowLinkParams(cartLink.href)
        }
        else {
          throw new Error('No cart form or link found')
        }
      }

      // find the storefront product by id and selected options (we need it for a variant id)
      const res = await fetchProductByIdAndOptions(parsedParams.productId, parsedParams.optionValueIds)
      
      const productType = getProductTypeFromGraphql(res)
      const variantEntityId = getVariantEntityIdFromGraphql(res)
      
      if (!variantEntityId || productType !== 'Physical') {
        console.error('[qc] product is not supported for buy now flow')
        redirectToBuyNow(parsedParams.productId, parsedParams.quantity, parsedParams.optionValueIds)
        return
      }

      openCheckout({
        products: [{variantId: variantEntityId.toString(), quantity: parsedParams.quantity, properties: {
          productId: parsedParams.productId.toString(),
        }}],
        isBuyNow: true
      })
    }
    catch (error) {
      console.error('[qc] error in buy now flow:', error.message)
      reportFailedToOpenCheckout(error.message, error, {
        storeId: `76fd4027-cad9-4fd8-ae63-d8eebde4b19e`,
        userAgent,
        flow: 'buy_now',
      })
      stopCheckoutRecording({ reason: 'buy now error' })

      // If we have parsed params, use them for direct checkout
      if (parsedParams?.productId) {
        redirectToBuyNow(parsedParams.productId, parsedParams.quantity, parsedParams.optionValueIds)
      } else {
        // Fallback to default checkout if we couldn't parse product info
        redirectToNativeCheckout()
      }
    }
  }

  const getVariantEntityIdFromGraphql = (res) => {
    const variants = res.data?.site?.product?.variants?.edges || []
    if (variants.length === 0) {
      return null
    }
  
    const variantEntityId = variants[0]?.node?.entityId
    if (!variantEntityId) {
      return null
    }
  
    return variantEntityId
  }

  const handleCheckoutClick = async (event) => {
    event.stopImmediatePropagation()
    event.preventDefault()

    console.debug(`[qc] attempt to open quickcheckout...`)
    try {
      const {vendorCartId, lineItems} = await getCart()
      if (!lineItems || !vendorCartId) {
        throw new Error(lineItems ? 'No vendorCartId' : 'No lineItems')
      }
      openCheckout({
        products: lineItems, 
        vendorCartId,
        isBuyNow: false
      })
    }
    catch (error) {
      console.error(`[qc] getting cart error:`, error.message)
      reportFailedToOpenCheckout(error.message, error, {
        storeId: `76fd4027-cad9-4fd8-ae63-d8eebde4b19e`,
        userAgent,
        flow: 'checkout',
      })
      stopCheckoutRecording({ reason: 'checkout error' })
      redirectToNativeCheckout()
    }
  }

  const handler = async (event) => {
    // prevent untrusted clicks from running the handler
    if (event.isTrusted === false) {
      return
    }

    const buyNowSelectors = `#form-action-buyNow,a[href*="cart.php"][href*="action=buy"]`;
    const checkoutSelectors = `a[href*="/checkout"]`;

    const isBuyNow = buyNowSelectors && event.target.matches(buyNowSelectors)
    const isCheckout = checkoutSelectors && event.target.matches(checkoutSelectors)

    // Start recording now (before cart fetch) so failed opens are captured in replay.
    if (isBuyNow || isCheckout) {
      startCheckoutRecording({ trigger: isBuyNow ? 'buy_now' : 'checkout' })
    }

    try {
      if (isBuyNow) {
        await handleBuyNowClick(event);
      }
    }
    catch (error) {
      console.error(`[qc] error handling buy now click:`, error.message)
      reportFailedToOpenCheckout(error.message, error, {
        storeId: `76fd4027-cad9-4fd8-ae63-d8eebde4b19e`,
        userAgent,
        flow: 'buy_now',
      })
      stopCheckoutRecording({ reason: 'buy now handler error' })
    }

    try {
      if (isCheckout) {
        await handleCheckoutClick(event);
      }
    }
    catch (error) {
      console.error(`[qc] error handling checkout click:`, error.message)
      reportFailedToOpenCheckout(error.message, error, {
        storeId: `76fd4027-cad9-4fd8-ae63-d8eebde4b19e`,
        userAgent,
        flow: 'checkout',
      })
      stopCheckoutRecording({ reason: 'checkout handler error' })
    }
  }

  // track every click
  window.addEventListener('touchstart', handler, true)
  window.addEventListener('click', handler, true)
}
