// packages/core-shared/src/instrumentation/sentry/init-client-react.ts
// Browser-side Sentry init for Vite/React runtimes (TanStack Start). PII scrubbing is
// applied via beforeSend/beforeSendTransaction because browser does NOT use the OTel pipeline.
// PII field lists imported from otel/pii-fields.ts (vendor-neutral).
import * as SentryReact from "@sentry/react";
import type { InitClientOpts } from "./init-client";
import {
  PII_KEY_SUBSTRINGS,
  PII_QUERY_PARAM_SUBSTRINGS,
  REDACTED_VALUE,
  REDACTED_IP,
  IPV4_REGEX,
  IPV6_REGEX,
} from "../otel/pii-fields";

// Inline scrub helpers for browser-side Sentry (server uses OTel processors instead).
function keyContainsPii(key: string): boolean {
  const lower = key.toLowerCase();
  return PII_KEY_SUBSTRINGS.some((s) => lower.includes(s));
}

function queryParamContainsPii(key: string): boolean {
  const lower = key.toLowerCase();
  return PII_QUERY_PARAM_SUBSTRINGS.some((s) => lower.includes(s));
}

function redactString(s: string): string {
  const ipv4 = new RegExp(IPV4_REGEX.source, "g");
  const ipv6 = new RegExp(IPV6_REGEX.source, "g");
  return s.replace(ipv4, REDACTED_IP).replace(ipv6, REDACTED_IP);
}

function deepScrub(value: unknown, parentKey = ""): unknown {
  if (value === null || value === undefined) return value;
  if (typeof value === "string") {
    return parentKey && keyContainsPii(parentKey)
      ? REDACTED_VALUE
      : redactString(value);
  }
  if (typeof value === "number" || typeof value === "boolean") {
    return parentKey && keyContainsPii(parentKey) ? REDACTED_VALUE : value;
  }
  if (Array.isArray(value)) return value.map((v) => deepScrub(v, parentKey));
  if (typeof value === "object") {
    const out: Record<string, unknown> = {};
    for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
      out[k] = keyContainsPii(k) ? REDACTED_VALUE : deepScrub(v, k);
    }
    return out;
  }
  return value;
}

function scrubUrl(url: string): string {
  try {
    const u = new URL(url, "http://placeholder.local");
    for (const [k] of Array.from(u.searchParams.entries())) {
      if (queryParamContainsPii(k)) u.searchParams.set(k, REDACTED_VALUE);
    }
    return url.startsWith("/") ? `${u.pathname}${u.search}` : u.toString();
  } catch {
    return url;
  }
}

/**
 * Client-side init for non-Next.js (Vite/React) runtimes (TanStack Start).
 * Mirrors init-client.ts but uses @sentry/react directly. Same PII,
 * replay, and scrubbing requirements apply.
 */
export function initSentryClientReact(opts: InitClientOpts): void {
  if (!opts.dsn) return;

  const isProd = process.env["NODE_ENV"] === "production";
  const { nonce } = opts;
  const tracesSampleRate =
    process.env["SENTRY_TRACES_SAMPLE_RATE"] !== undefined
      ? Number(process.env["SENTRY_TRACES_SAMPLE_RATE"])
      : isProd
        ? 0.1
        : 1.0;

  const environment =
    process.env["SENTRY_ENVIRONMENT"] ??
    process.env["NODE_ENV"] ??
    "development";
  const release = opts.release ?? "unknown";

  type InitOpts = Parameters<typeof SentryReact.init>[0];
  type SentryEvent = {
    extra?: Record<string, unknown> | null;
    contexts?: Record<string, Record<string, unknown> | undefined>;
    request?: {
      url?: string;
      headers?: Record<string, string | undefined>;
      [key: string]: unknown;
    };
    transaction?: string;
    [key: string]: unknown;
  };

  SentryReact.init({
    dsn: opts.dsn,
    environment,
    release,
    tracesSampleRate,
    sendDefaultPii: false,
    beforeSend: ((event: SentryEvent) =>
      deepScrub(event)) as unknown as NonNullable<InitOpts>["beforeSend"],
    beforeSendTransaction: ((event: SentryEvent) => {
      const out = { ...event };
      if (out.request?.url)
        out.request = { ...out.request, url: scrubUrl(out.request.url) };
      if (
        out.transaction &&
        (out.transaction.includes("?") || out.transaction.includes("="))
      ) {
        out.transaction = scrubUrl(out.transaction);
      }
      return out;
    }) as unknown as NonNullable<InitOpts>["beforeSendTransaction"],
    replaysSessionSampleRate: 0.0,
    replaysOnErrorSampleRate: 1.0,
    integrations: [
      // mandatory mask flags; allowlist starts empty
      SentryReact.replayIntegration({
        maskAllText: true,
        maskAllInputs: true,
        blockAllMedia: true,
      }),
      ...(SentryReact.feedbackIntegration
        ? [
            SentryReact.feedbackIntegration({
              ...(nonce ? { styleNonce: nonce, scriptNonce: nonce } : {}),
            }),
          ]
        : []),
    ],
    initialScope: { tags: { app: opts.app } },
  });
}
