/* eslint-env node */
import type * as Sentry from '@sentry/node';
import type {Span} from '@sentry/types';
import crypto from 'node:crypto';
import https from 'node:https';
import os from 'node:os';
import type webpack from 'webpack';

const {
  NODE_ENV,
  SENTRY_INSTRUMENTATION,
  SENTRY_WEBPACK_WEBHOOK_SECRET,
  GITHUB_SHA,
  GITHUB_REF,
  SENTRY_DEV_UI_PROFILING,
} = process.env;

const IS_CI = !!GITHUB_SHA;

const PLUGIN_NAME = 'SentryInstrumentation';
const GB_BYTE = 1073741824;

const createSignature = function (secret: string, payload: string) {
  const hmac = crypto.createHmac('sha1', secret);
  return `sha1=${hmac.update(payload).digest('hex')}`;
};

const INCREMENTAL_BUILD_TXN = 'incremental-build';
const INITIAL_BUILD_TXN = 'initial-build';

class SentryInstrumentation {
  hasInitializedBuild: boolean = false;

  Sentry?: typeof Sentry;

  span?: Span;

  constructor() {
    // Only run if SENTRY_INSTRUMENTATION` is set or when in ci,
    // only in the javascript suite that runs webpack
    if (!SENTRY_INSTRUMENTATION && !SENTRY_DEV_UI_PROFILING) {
      return;
    }

    const sentry = require('@sentry/node') as typeof Sentry;
    const {nodeProfilingIntegration} = require('@sentry/profiling-node');

    sentry.init({
      dsn: 'https://3d282d186d924374800aa47006227ce9@sentry.io/2053674',
      environment: IS_CI ? 'ci' : 'local',
      tracesSampleRate: 1.0,
      integrations: [nodeProfilingIntegration()],
      profilesSampler: ({transactionContext}) => {
        if (transactionContext.name === INCREMENTAL_BUILD_TXN) {
          return 0;
        }
        return 1;
      },
      _experiments: {
        // 5 minutes should be plenty
        maxProfileDurationMs: 5 * 60 * 1000,
      },
    });

    if (IS_CI) {
      sentry.setTag('branch', GITHUB_REF);
    }

    const cpus = os.cpus();
    sentry.setTag('platform', os.platform());
    sentry.setTag('arch', os.arch());
    sentry.setTag(
      'cpu',
      cpus?.length ? `${cpus[0].model} (cores: ${cpus.length})}` : 'N/A'
    );

    this.Sentry = sentry;

    this.span = sentry.startInactiveSpan({
      op: 'webpack-build',
      name: INITIAL_BUILD_TXN,
    });
  }

  /**
   * Measures the file sizes of assets emitted from the entrypoints
   */
  measureAssetSizes(compilation: webpack.Compilation) {
    if (!SENTRY_WEBPACK_WEBHOOK_SECRET) {
      return;
    }

    [...compilation.entrypoints].forEach(([entrypointName, entry]) =>
      entry.chunks.forEach(chunk =>
        Array.from(chunk.files)
          .filter(assetName => !assetName.endsWith('.map'))
          .forEach(assetName => {
            const asset = compilation.assets[assetName];
            const size = asset.size();
            const file = assetName;
            const body = JSON.stringify({
              file,
              entrypointName,
              size,
              commit: GITHUB_SHA,
              environment: IS_CI ? 'ci' : '',
              node_env: NODE_ENV,
            });

            const req = https.request({
              host: 'product-eng-webhooks-vmrqv3f7nq-uw.a.run.app',
              path: '/metrics/webpack/webhook',
              method: 'POST',
              headers: {
                'Content-Type': 'application/json',
                'Content-Length': Buffer.byteLength(body),
                'x-webpack-signature': createSignature(
                  SENTRY_WEBPACK_WEBHOOK_SECRET,
                  body
                ),
              },
            });
            req.end(body);
          })
      )
    );
  }

  measureBuildTime(startTime: number, endTime: number) {
    if (!this.Sentry) {
      return;
    }

    const span = !this.hasInitializedBuild
      ? this.span
      : this.Sentry.startInactiveSpan({
          op: 'webpack-build',
          name: INCREMENTAL_BUILD_TXN,
          startTime,
        });

    if (!span) {
      return;
    }

    this.Sentry.withActiveSpan(span, () => {
      this.Sentry?.startInactiveSpan({
        op: 'build',
        name: 'webpack build',
        attributes: {
          os: `${os.platform()} ${os.arch()} v${os.release()}`,
          memory: os.freemem()
            ? `${os.freemem() / GB_BYTE} / ${os.totalmem() / GB_BYTE} GB (${
                (os.freemem() / os.totalmem()) * 100
              }% free)`
            : 'N/A',
          loadavg: os.loadavg(),
        },
        startTime,
      }).end(endTime);
    });

    span.end();
  }

  apply(compiler: webpack.Compiler) {
    compiler.hooks.done.tapAsync(
      PLUGIN_NAME,
      async ({compilation, startTime, endTime}, done) => {
        // Only record this once and only on Travis
        // Don't really care about asset sizes during local dev
        if (IS_CI && !this.hasInitializedBuild) {
          this.measureAssetSizes(compilation);
        }

        if (this.Sentry) {
          this.measureBuildTime(startTime / 1000, endTime / 1000);
          await this.Sentry.flush();
        }

        this.hasInitializedBuild = true;

        done();
      }
    );
  }
}

export default SentryInstrumentation;