import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { W3CTraceContextPropagator } from "@opentelemetry/core";
import { NodeSDK } from "@opentelemetry/sdk-node";
import {
  AlwaysOffSampler,
  ParentBasedSampler,
  TraceIdRatioBasedSampler,
} from "@opentelemetry/sdk-trace-base";
import { setNodeSdkConfig } from "./config-manager.js";
import buildConsoleExporters from "./exporter/console.js";
import buildGrpcExporters from "./exporter/grpc.js";
import buildHttpExporters from "./exporter/http.js";
import type { Exporters } from "./exporter/index.js";
import type { NodeSDKConfig } from "./index.js";
import { _shutdownHook } from "./internals/hooks.js";
import { ObservabilityResourceDetector } from "./resource.js";
import { UrlSampler } from "./url-sampler.js";

export default async function buildNodeInstrumentation(
  config?: NodeSDKConfig,
): Promise<NodeSDK | undefined> {
  if (!config) {
    console.warn(
      "observability config not set. Skipping NodeJS OpenTelemetry instrumentation.",
    );
    return;
  }

  if (!config.collectorUrl) {
    console.warn(
      "collectorUrl not set. Skipping NodeJS OpenTelemetry instrumentation.",
    );
    return;
  }

  if (!isUrl(config.collectorUrl)) {
    console.error(
      "collectorUrl does not use a valid format. Skipping NodeJS OpenTelemetry instrumentation.",
    );
    return;
  }

  if (!config.detection) {
    config.detection = {
      email: true,
    };
  }

  if (config.detection.email === undefined) {
    config.detection.email = true;
  }

  // Init configManager to make it available to all o11y utils.
  setNodeSdkConfig(config);

  const urlSampler = new UrlSampler(
    config.ignoreUrls,
    new TraceIdRatioBasedSampler(config.traceRatio ?? 1),
  );

  const mainSampler = new ParentBasedSampler({
    root: urlSampler,
    remoteParentSampled: urlSampler,
    remoteParentNotSampled: new AlwaysOffSampler(),
    localParentSampled: urlSampler,
    localParentNotSampled: new AlwaysOffSampler(),
  });

  diag.setLogger(
    new DiagConsoleLogger(),
    config.diagLogLevel ? DiagLogLevel[config.diagLogLevel] : DiagLogLevel.INFO,
  );

  try {
    const nodeSdkInstrumentation = getNodeAutoInstrumentations({
      "@opentelemetry/instrumentation-fs": {
        enabled: config.enableFS ?? false,
      },
    });

    let exporter: Exporters;

    if (config.protocol === "http") {
      exporter = buildHttpExporters(config);
    } else if (config.protocol === "console") {
      exporter = buildConsoleExporters(config);
    } else {
      exporter = await buildGrpcExporters(config);
    }

    const sdk = new NodeSDK({
      resourceDetectors: [
        new ObservabilityResourceDetector(config.resourceAttributes),
      ],
      spanProcessors: exporter.spans,
      serviceName: config.serviceName,
      metricReader: exporter.metrics,
      logRecordProcessors: exporter.logs,
      sampler: mainSampler,
      textMapPropagator: new W3CTraceContextPropagator(),
      instrumentations: [nodeSdkInstrumentation],
    });

    sdk.start();
    console.log("NodeJS OpenTelemetry instrumentation started successfully.");

    _shutdownHook(sdk);
    return sdk;
  } catch (error) {
    console.error(
      "Error starting NodeJS OpenTelemetry instrumentation:",
      error,
    );
  }
}

function isUrl(url: string): boolean {
  try {
    new URL(url);
    return true;
  } catch (_) {
    return false;
  }
}
