/**
 * Sentry initialization wrapper middleware.
 *
 * - Reads `SENTRY_DSN` from runtime env bindings.
 * - If absent, is a no-op (all Sentry calls become nops).
 * - Installs a `beforeSend` hook that scrubs PII fields:
 *   `phone`, `email`, `address`, `display_name`, `ip`, `ipAddress`.
 *
 * NOTE: @sentry/astro is already configured in the Astro integration in
 * astro.config.mjs. This middleware provides the runtime-env DSN injection
 * and PII scrubbing that can't be done at build time.
 */

import { defineMiddleware } from 'astro:middleware';
import { env as runtimeEnv } from '@/server/env.js';
import type * as SentryAstro from '@sentry/astro';
import { scrubPii, scrubStringValue } from '@/server/observability/pii-scrub.js';

// Lazily-loaded Sentry module — only imported when SENTRY_DSN is present.
// Kept as a module-level reference so captureException works after initSentry().
// Dynamic import defers @sentry/astro (→ @sentry/cloudflare, 1.6MB) from cold-start.
let sentryRef: typeof SentryAstro | null = null;

// Inline types to avoid importing @sentry/core directly (it is a nested dep)
type Breadcrumb = {
  type?: string;
  level?: string;
  event_id?: string;
  category?: string;
  message?: string;
  data?: Record<string, unknown>;
  timestamp?: number;
};
type Exception = {
  type?: string;
  value?: string;
  module?: string;
  thread_id?: number;
  [key: string]: unknown;
};

function scrubBreadcrumbs(breadcrumbs: Breadcrumb[] | undefined): Breadcrumb[] | undefined {
  if (!breadcrumbs) return breadcrumbs;
  return breadcrumbs.map((crumb) => ({
    ...crumb,
    message: crumb.message ? scrubStringValue(crumb.message) : crumb.message,
    data: crumb.data ? (scrubPii(crumb.data) as Record<string, unknown>) : crumb.data,
  }));
}

function scrubExceptionValues(exceptions: Exception[] | undefined): Exception[] | undefined {
  if (!exceptions) return exceptions;
  return exceptions.map((ex) => ({
    ...ex,
    value: ex.value ? scrubStringValue(ex.value) : ex.value,
  }));
}

// ---------------------------------------------------------------------------
// Init state
// ---------------------------------------------------------------------------

let initialized = false;

async function initSentry(dsn: string): Promise<void> {
  if (initialized) return;
  initialized = true;

  // Dynamic import — defers @sentry/astro bundle from module cold-start eval
  sentryRef = await import('@sentry/astro');

  sentryRef.init({
    dsn,
    // Low sample rate for performance - events are still captured fully
    tracesSampleRate: 0.1,
    beforeSend(event) {
      // 1. Scrub PII from user object
      if (event.user) {
        event.user = scrubPii(event.user) as typeof event.user;
        // Always redact IP at the top-level user object
        if (event.user?.ip_address) {
          event.user.ip_address = '[Scrubbed]';
        }
      }

      // 2. Scrub PII from request body / cookies / headers
      if (event.request) {
        if (event.request.data) {
          event.request.data = scrubPii(event.request.data as unknown) as string;
        }
        if (event.request.cookies) {
          // Never send raw cookie values
          const cookies = event.request.cookies as Record<string, string>;
          event.request.cookies = Object.fromEntries(
            Object.keys(cookies).map((k) => [k, '[Scrubbed]']),
          );
        }
        // Never send the real IP
        if (event.request.env) {
          const env = event.request.env as Record<string, unknown>;
          if ('REMOTE_ADDR' in env) {
            env['REMOTE_ADDR'] = '[Scrubbed]';
          }
        }
      }

      // 3. Scrub PII from extra / contexts
      if (event.extra) {
        event.extra = scrubPii(event.extra) as typeof event.extra;
      }
      if (event.contexts) {
        event.contexts = scrubPii(event.contexts) as typeof event.contexts;
      }

      // 4. Scrub PII patterns from breadcrumb messages and data
      // In Sentry v8, event.breadcrumbs is Breadcrumb[] directly. Cast to/from
      // our local Breadcrumb type to avoid depending on @sentry/core directly.
      if (event.breadcrumbs) {
        event.breadcrumbs =
          (scrubBreadcrumbs(event.breadcrumbs as Breadcrumb[]) as typeof event.breadcrumbs) ?? [];
      }

      // 5. Scrub PII from exception messages (e.g. DB errors that echo user input)
      if (event.exception?.values) {
        event.exception.values =
          (scrubExceptionValues(
            event.exception.values as Exception[],
          ) as typeof event.exception.values) ?? [];
      }

      return event;
    },
  });
}

// ---------------------------------------------------------------------------
// Middleware
// ---------------------------------------------------------------------------

export const sentryMiddleware = defineMiddleware(async (_context, next) => {
  const env = runtimeEnv;
  const dsn = env.SENTRY_DSN;

  if (dsn) {
    await initSentry(dsn);
  }

  try {
    return await next();
  } catch (err) {
    if (dsn && initialized && sentryRef) {
      sentryRef.captureException(err);
    }
    throw err;
  }
});
