// Mini Sentry transport — no @sentry/* peer dep, isolated from any
// host-app Sentry instance. Sends events to our own DSN via the
// envelope endpoint and silently no-ops if DSN is empty.

// Clé rotatée en 1.0.20. La précédente (7a216c9c..., en place depuis la 1.0.12)
// est partagée par toutes les versions du terrain, qui saturent à elles seules
// le quota de l'organisation : ~23 400 events/mois pour 5 000 alloués.
//
// PROCHAINE ÉTAPE, une fois Nice-Matin migré sur cette version : désactiver
// l'ancienne clé dans Sentry (Settings -> Client Keys). Les events des versions
// historiques seront alors rejetés à l'ingestion, donc non décomptés du quota,
// sans affecter la remontée de la 1.0.20. Ne pas la désactiver avant : c'est la
// seule visibilité restante sur la production tant que la migration n'est pas
// faite. Les rate limits par clé étant réservés aux plans Business, la rotation
// est le seul levier disponible ici.
const DSN = 'https://967b58a586e840c725d53bec750a5793@o258830.ingest.us.sentry.io/4511332325654528';

const SDK_VERSION = '1.0.21';
const MAX_EVENTS_PER_MINUTE = 10;

let recentTimestamps: number[] = [];
let parsed: { url: string; publicKey: string } | null = null;
let parseAttempted = false;

function parseDsn(dsn: string): typeof parsed {
  const m = dsn.match(/^(https?):\/\/([^@]+)@([^/]+)\/(.+)$/);
  if (!m) return null;
  const [, protocol, publicKey, host, projectId] = m;
  return {
    url: `${protocol}://${host}/api/${projectId}/envelope/?sentry_key=${publicKey}&sentry_version=7&sentry_client=viewpay-react-native/${SDK_VERSION}`,
    publicKey,
  };
}

function ensureParsed() {
  if (parseAttempted) return;
  parseAttempted = true;
  if (DSN) parsed = parseDsn(DSN);
}

function rateLimitOk(): boolean {
  const now = Date.now();
  recentTimestamps = recentTimestamps.filter((t) => now - t < 60_000);
  if (recentTimestamps.length >= MAX_EVENTS_PER_MINUTE) return false;
  recentTimestamps.push(now);
  return true;
}

function uuid(): string {
  let s = '';
  for (let i = 0; i < 32; i++) s += Math.floor(Math.random() * 16).toString(16);
  return s;
}

export interface CaptureContext {
  source?: 'sdk' | 'webview-js' | 'webview-load' | 'webview-render';
  siteId?: string;
  vpuid?: string;
  step?: number;
  extra?: Record<string, unknown>;
  tags?: Record<string, string>;
  /**
   * Probabilité d'envoi entre 0 et 1, pour les catégories à fort volume dont
   * seule la tendance importe. Absent = toujours envoyé.
   */
  sampleRate?: number;
}

export function captureException(error: unknown, context: CaptureContext = {}): void {
  ensureParsed();
  if (!parsed) return;
  if (context.sampleRate !== undefined && Math.random() >= context.sampleRate) return;
  if (!rateLimitOk()) return;

  const errObj = error instanceof Error ? error : new Error(String(error));
  const eventId = uuid();
  const ts = Date.now() / 1000;

  const tags: Record<string, string> = {
    sdk: 'viewpay-react-native',
    source: context.source || 'sdk',
  };
  if (context.siteId) tags.siteId = context.siteId;
  if (context.tags) Object.assign(tags, context.tags);

  const extra: Record<string, unknown> = {};
  if (context.vpuid) extra.vpuid = context.vpuid;
  if (typeof context.step === 'number') extra.step = context.step;
  if (context.extra) Object.assign(extra, context.extra);

  const stackFrames = errObj.stack
    ? errObj.stack
        .split('\n')
        .slice(1)
        .map((line) => ({ filename: '<rn>', function: line.trim() }))
        .reverse()
    : undefined;

  const event = {
    event_id: eventId,
    timestamp: ts,
    platform: 'javascript',
    level: 'error',
    logger: 'viewpay-sdk',
    release: `viewpay-react-native@${SDK_VERSION}`,
    environment: 'production',
    sdk: { name: 'viewpay-react-native', version: SDK_VERSION },
    tags,
    extra,
    exception: {
      values: [
        {
          type: errObj.name || 'Error',
          value: errObj.message || String(error),
          ...(stackFrames ? { stacktrace: { frames: stackFrames } } : {}),
        },
      ],
    },
  };

  const envelope = [
    JSON.stringify({ event_id: eventId, sent_at: new Date().toISOString() }),
    JSON.stringify({ type: 'event' }),
    JSON.stringify(event),
  ].join('\n');

  fetch(parsed.url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-sentry-envelope' },
    body: envelope,
  }).catch(() => {});
}

export const WEBVIEW_ERROR_PREFIX = '__vp_err__:';

// Injected at content-load start. Captures top-frame `error` and
// `unhandledrejection` events and forwards them to RN via postMessage.
// Cross-origin iframes (incl. VPAID about:srcdoc) are unreachable from
// here — those have to be surfaced by the player itself.
export const WEBVIEW_ERROR_HOOK = `
(function(){
  if (window.__vpErrInstalled) return;
  window.__vpErrInstalled = true;
  var send = function(payload){
    try {
      if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) {
        window.ReactNativeWebView.postMessage('${WEBVIEW_ERROR_PREFIX}'+JSON.stringify(payload));
      }
    } catch(_){}
  };
  window.addEventListener('error', function(e){
    send({
      kind: 'error',
      message: e && e.message,
      filename: e && e.filename,
      lineno: e && e.lineno,
      colno: e && e.colno,
      stack: e && e.error && e.error.stack
    });
  });
  window.addEventListener('unhandledrejection', function(e){
    var r = e && e.reason;
    send({
      kind: 'unhandledrejection',
      message: r && (r.message || String(r)),
      stack: r && r.stack
    });
  });
})();
true;
`;

export function tryHandleWebViewErrorMessage(
  data: unknown,
  context: CaptureContext,
): boolean {
  if (typeof data !== 'string' || !data.startsWith(WEBVIEW_ERROR_PREFIX)) return false;
  try {
    const payload = JSON.parse(data.slice(WEBVIEW_ERROR_PREFIX.length));

    // Cross-origin scripts loaded without `crossorigin="anonymous"` surface as
    // opaque "Script error." events (lineno/colno 0, empty filename, no stack):
    // the browser scrubs them for same-origin-policy reasons. The CDN now sets
    // `crossorigin`, so genuine errors arrive fully detailed; anything still
    // opaque is unactionable (e.g. YouTube iframe_api, or errors already caught
    // internally) and would only burn the Sentry quota with empty events. Drop
    // it here — but log so it stays visible in dev rather than vanishing.
    const message = typeof payload.message === 'string' ? payload.message.trim() : '';
    const isOpaqueScriptError =
      (message === 'Script error.' || message === 'Script error') &&
      !payload.lineno &&
      !payload.filename &&
      !payload.stack;
    if (isOpaqueScriptError) {
      console.warn('[viewpay-sdk] dropping opaque cross-origin WebView error', payload);
      return true;
    }

    const err = new Error(payload.message || 'WebView JS error');
    if (payload.stack) err.stack = payload.stack;
    captureException(err, {
      ...context,
      source: 'webview-js',
      extra: {
        kind: payload.kind,
        filename: payload.filename,
        lineno: payload.lineno,
        colno: payload.colno,
        ...(context.extra || {}),
      },
    });
  } catch {}
  return true;
}
