// This file configures the initialization of Sentry on the client.
// The config you add here will be used whenever a user loads a page in their browser.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/

import * as Sentry from '@sentry/nextjs'
import { hasConsented } from 'common'
import { IS_PLATFORM } from 'common/constants/environment'

import { MIRRORED_BREADCRUMBS } from '@/lib/breadcrumbs'
import { sanitizeArrayOfObjects, sanitizeUrlHashParams } from '@/lib/sanitize'

const DEFAULT_ERROR_SAMPLE_RATE = 1.0
const LOW_PRIORITY_ERROR_SAMPLE_RATE = 0.01
const CHUNK_LOAD_ERROR_PATTERNS = [
  /ChunkLoadError/i,
  /Loading chunk [\d]+ failed/i,
  /Loading CSS chunk [\d]+ failed/i,
]

// This is a workaround to ignore hCaptcha related errors.
function isHCaptchaRelatedError(event: Sentry.Event): boolean {
  const errors = event.exception?.values ?? []
  for (const error of errors) {
    if (
      error.value?.includes('is not a function') &&
      error.stacktrace?.frames?.some((f) => f.filename === 'api.js')
    ) {
      return true
    }
  }
  return false
}

// Filter browser wallet extension errors (e.g., Gate.io wallet)
// These errors come from injected wallet scripts and are not actionable
// Examples: BRIVEN-APP-AFC, BRIVEN-APP-92A
export function isBrowserWalletExtensionError(event: Sentry.Event): boolean {
  const frames = event.exception?.values?.flatMap((e) => e.stacktrace?.frames || []) || []
  return frames.some((frame) => {
    const filename = frame.filename || frame.abs_path || ''
    return filename.includes('gt-window-provider') || filename.includes('wallet-provider')
  })
}

// Filter user-aborted operations (intentional cancellations)
// These are expected when users cancel requests or navigate away
// Examples: BRIVEN-APP-BG6, BRIVEN-APP-BG7
export function isUserAbortedOperation(error: unknown, event: Sentry.Event): boolean {
  const errorMessage = error instanceof Error ? error.message : ''
  const eventMessage = event.message || ''
  const message = errorMessage || eventMessage

  return (
    message.includes('operation was aborted') ||
    message.includes('signal is aborted') ||
    message.includes('manually canceled') ||
    message.includes('AbortError')
  )
}

// Filter cancellation promise rejections (e.g., from query cancellation)
// These occur when operations are intentionally cancelled by the user
// Example: BRIVEN-APP-353 (~466k events)
export function isCancellationRejection(event: Sentry.Event): boolean {
  const serialized = event.extra?.__serialized__ as Record<string, unknown> | undefined
  return serialized?.type === 'cancelation'
}

// Filter challenge/captcha expired errors (user timeout)
// These happen when users don't complete captcha in time - expected behavior
// Example: BRIVEN-APP-ACC
export function isChallengeExpiredError(error: unknown, event: Sentry.Event): boolean {
  const errorMessage = error instanceof Error ? error.message : ''
  const eventMessage = event.message || ''
  const message = errorMessage || eventMessage

  return message.includes('challenge-expired')
}

function isChunkLoadError(error: unknown, event: Sentry.Event): boolean {
  const errorMessage = error instanceof Error ? error.message : ''
  const eventMessage = event.message || ''
  const exceptionMessages = event.exception?.values?.map((ex) => ex.value ?? '') ?? []
  const combinedMessages = [errorMessage, eventMessage, ...exceptionMessages].filter(Boolean)

  return CHUNK_LOAD_ERROR_PATTERNS.some((pattern) =>
    combinedMessages.some((message) => pattern.test(message))
  )
}

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  ...(process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT && {
    environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT,
  }),
  // Setting this option to true will print useful information to the console while you're setting up Sentry.
  debug: false,

  // Enable performance monitoring
  tracesSampleRate: 0.02,

  integrations: (() => {
    const thirdPartyErrorFilterIntegration = (Sentry as any).thirdPartyErrorFilterIntegration
    if (!thirdPartyErrorFilterIntegration) return []

    // Tag errors whose stack trace only contains third-party frames (browser extensions,
    // injected scripts, etc.). This uses build-time code annotation via the applicationKey
    // in next.config.ts to reliably distinguish our code from third-party code.
    // We use 'apply-tag' instead of 'drop' so that beforeSend can exempt error boundary
    // crashes — these may originate in third-party code but are caused by first-party bugs.
    return [
      thirdPartyErrorFilterIntegration({
        filterKeys: ['briven-studio'],
        behaviour: 'apply-tag-if-exclusively-contains-third-party-frames',
      }),
    ]
  })(),

  // Only capture errors originating from our own code.
  // This is a whitelist on the source URL in stack frames — it drops errors from
  // browser extensions, injected scripts, third-party widgets, etc. (FE-2094)
  allowUrls: [
    /https?:\/\/(.*\.)?briven\.(com|co|green|io)/,
    /app:\/\//, // Next.js rewrites source URLs to app:// with source maps
  ],
  beforeBreadcrumb(breadcrumb, _hint) {
    const cleanedBreadcrumb = { ...breadcrumb }

    if (cleanedBreadcrumb.category === 'navigation') {
      if (typeof cleanedBreadcrumb.data?.from === 'string') {
        cleanedBreadcrumb.data.from = sanitizeUrlHashParams(cleanedBreadcrumb.data.from)
      }
      if (typeof cleanedBreadcrumb.data?.to === 'string') {
        cleanedBreadcrumb.data.to = sanitizeUrlHashParams(cleanedBreadcrumb.data.to)
      }
    }

    MIRRORED_BREADCRUMBS.pushBack(cleanedBreadcrumb)
    return cleanedBreadcrumb
  },
  beforeSend(event, hint) {
    const consent = hasConsented()

    if (!consent) {
      return null
    }

    if (!IS_PLATFORM) {
      return null
    }

    const isErrorBoundaryCrash =
      event.tags?.globalErrorBoundary === true || event.tags?.globalErrorBoundary === 'true'
    const isThirdPartyOnly =
      event.tags?.third_party_code === true || event.tags?.third_party_code === 'true'

    // Drop third-party-only errors UNLESS they crashed the page via the global error boundary.
    // This preserves noise reduction for browser extensions and injected scripts,
    // while ensuring page-crashing errors from third-party libs (caused by first-party bugs)
    // are always reported.
    if (isThirdPartyOnly && !isErrorBoundaryCrash) {
      return null
    }

    // Downsample only known high-noise classes; keep all other errors at full rate.
    const isInvalidUrlEvent = (hint.originalException as any)?.message?.includes(
      `Failed to construct 'URL': Invalid URL`
    )
    const isSessionTimeoutEvent = (hint.originalException as any)?.message?.includes(
      'Session error detected'
    )
    const isChunkLoadFailure = isChunkLoadError(hint.originalException, event)

    const codeSampleRate =
      isInvalidUrlEvent || isSessionTimeoutEvent || isChunkLoadFailure
        ? LOW_PRIORITY_ERROR_SAMPLE_RATE
        : DEFAULT_ERROR_SAMPLE_RATE

    if (Math.random() > codeSampleRate) {
      return null
    }

    event.tags = {
      ...event.tags,
      codeSampleRate: codeSampleRate.toString(),
    }

    if (isHCaptchaRelatedError(event)) {
      return null
    }

    // Drop events where every exception has no stack trace — these are not debuggable.
    // Exempt error boundary crashes: even without stack frames, a page crash is always worth reporting.
    const exceptions = event.exception?.values ?? []
    if (
      !isErrorBoundaryCrash &&
      exceptions.length > 0 &&
      exceptions.every((ex) => !ex.stacktrace?.frames?.length)
    ) {
      return null
    }

    // Filter out errors like 'e._5BLbSXV[t] is not a function' or anything matching '[t] is not a function'
    if (
      hint.originalException instanceof Error &&
      hint.originalException.message.includes('[t] is not a function')
    ) {
      return null
    }

    if (isBrowserWalletExtensionError(event)) {
      return null
    }
    if (isUserAbortedOperation(hint.originalException, event)) {
      return null
    }
    if (isCancellationRejection(event)) {
      return null
    }
    if (isChallengeExpiredError(hint.originalException, event)) {
      return null
    }

    if (event.breadcrumbs) {
      event.breadcrumbs = sanitizeArrayOfObjects(event.breadcrumbs) as Sentry.Breadcrumb[]
    }
    return event
  },
  ignoreErrors: [
    // === Monaco Editor ===
    'ResizeObserver',
    's.getModifierState is not a function',
    /^Uncaught NetworkError: Failed to execute 'importScripts' on 'WorkerGlobalScope'/,

    // === Browser extension errors ===
    // Gate.io wallet
    'shouldSetTallyForCurrentProvider is not a function',
    // SAP browser extensions (SAP GUI, SAP Companion)
    'sap is not defined',
    // Non-Error objects thrown as exceptions (e.g., Event objects)
    '[object Event]',

    // === Third-party SDK errors ===
    // stripe-js: https://github.com/stripe/stripe-js/issues/26
    'Failed to load Stripe.js',
    // hCaptcha
    "undefined is not an object (evaluating 'n.chat.setReady')",
    "undefined is not an object (evaluating 'i.chat.setReady')",

    // === Next.js internals ===
    // Ref: https://github.com/briven/briven/pull/9729
    /The provided `href` \(\/org\/\[slug\]\/.*\) value is missing query values/,
    // Next.js throws these during navigation, not actual errors
    'NEXT_NOT_FOUND',
    'NEXT_REDIRECT',

    // === User input errors (not bugs) ===
    // sql-formatter lexer on invalid SQL input
    /^Parse error: Unexpected ".+" at line \d+ column \d+$/,

    // === Network / infrastructure (not actionable on FE) ===
    /504 Gateway Time-out/,
    'Network request failed',
    'Failed to fetch',
    'Load failed',
    'AbortError',
    'TypeError: cancelled',
    'TypeError: Cancelled',

    // === Browser extensions & Google Translate DOM manipulation ===
    'Node.insertBefore: Child to insert before is not a child of this node',
    'Node.removeChild: The node to be removed is not a child of this node',
    "NotFoundError: Failed to execute 'removeChild' on 'Node'",
    "NotFoundError: Failed to execute 'insertBefore' on 'Node'",
    'NotFoundError: The object can not be found here.',
    "Cannot read properties of null (reading 'parentNode')",
    "Cannot read properties of null (reading 'removeChild')",
    "TypeError: can't access dead object",
    /^NS_ERROR_/,

    // === Non-Error throws (extensions, third-party libs throwing strings/objects) ===
    'Non-Error exception captured',
    'Non-Error promise rejection captured',
    /^Object captured as exception with keys:/,

    // === Cross-origin script errors (no useful info) ===
    'Script error.',
    'Script error',

    // === React hydration mismatches caused by extensions modifying DOM ===
    // Note: we only suppress the generic browser messages, NOT "Hydration failed because..."
    // which can indicate real SSR/client mismatches in our own code.
    /text content does not match/i,
    /There was an error while hydrating/i,

    // === Web crawler / bot errors ===
    'instantSearchSDKJSBridgeClearHighlight',

    // === Third-party library race conditions ===
    // cmdk: useSyncExternalStore subscribe called before store context is available
    "Cannot read properties of undefined (reading 'subscribe')",
    "undefined is not an object (evaluating 't.subscribe')",

    // === Misc known noise ===
    'r.default.setDefaultLevel is not a function',
    // Clipboard permission denied
    'The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.',
    // Facebook pixel
    'fb_xd_fragment',
  ],
})

// This export will instrument router navigations, and is only relevant if you enable tracing.
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart
