import type { tracing as sdkTracing, logs as sdkLogs } from "@opentelemetry/sdk-node";
import { SeverityNumber } from "@opentelemetry/api-logs";
import type { LogRecord } from "@opentelemetry/sdk-logs";

type SpanProcessor = sdkTracing.SpanProcessor;
type LogRecordProcessor = sdkLogs.LogRecordProcessor;

export type SentryOtelBridgeOpts = {
  /** Sentry DSN. When empty, no Sentry processors are returned (Noop boot). */
  dsn: string;
};

export type SentryOtelBridge = {
  spanProcessor: SpanProcessor | null;
  logRecordProcessor: LogRecordProcessor | null;
};

type SentryModule = {
  captureException: (
    err: Error,
    opts: {
      tags?: Record<string, string>;
      extra?: Record<string, unknown>;
      fingerprint?: string[];
    },
  ) => void;
  captureMessage: (
    msg: string,
    level: string,
    opts: { tags?: Record<string, string>; extra?: Record<string, unknown> },
  ) => void;
};

/**
 * Consumes OTel LogRecords and forwards them to Sentry via the user-facing
 * Sentry SDK API. This is the Sentry-coupled bridge — the only file in
 * core-shared (besides sentry/init-server.ts etc.) that imports `@sentry/*`.
 *
 * Double-report note: ERROR records may also arrive via the span-event path
 * (OtelTracer.recordException → SentrySpanProcessor). Sentry's native dedup
 * handles this (same stack + message). Future hardening can refine.
 *
 * @param sentry — injectable Sentry module reference; defaults to lazy-require
 *   of `@sentry/nextjs`. Pass a mock in tests to avoid require() interception
 *   limitations with Vitest's vi.doMock.
 */
export class SentryLogRecordForwarder implements LogRecordProcessor {
  private readonly sentry: SentryModule;

  constructor(sentry?: SentryModule) {
    // eslint-disable-next-line @typescript-eslint/no-require-imports
    this.sentry = sentry ?? (require("@sentry/nextjs") as SentryModule);
  }

  onEmit(record: LogRecord): void {
    const Sentry = this.sentry;

    const attrs = record.attributes ?? {};
    const tags = extractTags(attrs);
    const extra = extractExtras(attrs);

    const severityNumber = record.severityNumber ?? SeverityNumber.INFO;

    if (severityNumber >= SeverityNumber.ERROR) {
      // Reconstruct the error from OTel semantic convention attributes
      const message =
        (attrs["exception.message"] as string | undefined) ?? String(record.body ?? "");
      const err = new Error(message);
      if (attrs["exception.type"]) {
        err.name = attrs["exception.type"] as string;
      }
      if (attrs["exception.stacktrace"]) {
        err.stack = attrs["exception.stacktrace"] as string;
      }

      const fingerprint = attrs["sentry.fingerprint"]
        ? (attrs["sentry.fingerprint"] as string).split("|")
        : undefined;

      Sentry.captureException(err, { tags, extra, ...(fingerprint ? { fingerprint } : {}) });
    } else {
      // Map severity to Sentry level
      const level = severityNumber >= SeverityNumber.WARN ? "warning" : "info";

      Sentry.captureMessage(String(record.body ?? ""), level, { tags, extra });
    }
  }

  forceFlush(): Promise<void> {
    return Promise.resolve();
  }

  shutdown(): Promise<void> {
    return Promise.resolve();
  }
}

/**
 * Creates Sentry-as-OTel-exporter processors. The OTel SDK uses these to
 * forward spans and log records to Sentry. This is the ONLY file in
 * core-shared that imports from `@sentry/opentelemetry` — all other Sentry
 * coupling is excluded by the R40/R52 ESLint allowlist.
 */
export function createSentryOtelBridge(opts: SentryOtelBridgeOpts): SentryOtelBridge {
  if (!opts.dsn) {
    return { spanProcessor: null, logRecordProcessor: null };
  }
  // eslint-disable-next-line @typescript-eslint/no-require-imports
  const sentryOtel = require("@sentry/opentelemetry");
  return {
    spanProcessor: new sentryOtel.SentrySpanProcessor() as SpanProcessor,
    logRecordProcessor: new SentryLogRecordForwarder(),
  };
}

// ── Attribute helpers ────────────────────────────────────────────────────────

function extractTags(attrs: Record<string, unknown>): Record<string, string> {
  const tags: Record<string, string> = {};
  for (const [k, v] of Object.entries(attrs)) {
    if (k.startsWith("tag.")) {
      tags[k.slice(4)] = String(v);
    }
  }
  return tags;
}

function extractExtras(attrs: Record<string, unknown>): Record<string, unknown> {
  const extras: Record<string, unknown> = {};
  for (const [k, v] of Object.entries(attrs)) {
    if (k.startsWith("extra.")) {
      extras[k.slice(6)] = v;
    }
  }
  return extras;
}
