import A from 'async';
import createDebug from 'debug';

const debug = createDebug('commands:create-test');

import fs from 'node:fs';
import path from 'node:path';
import { PutObjectCommand } from '@aws-sdk/client-s3';
import { createBOM, enrichPackageJson, prettyPrint } from './bom.ts';
import createS3Client from './create-s3-client.ts';
import { getBucketName } from './util.ts';

// Minimal structural view of an S3-compatible sync client (the Azure
// platform passes a custom client).
type SyncClient = { send: (command: any) => Promise<any> };

function tryCreateTest(scriptPath: string, options: Record<string, any>) {
  createTest(scriptPath, options);
}

async function createTest(
  scriptPath: string,
  options: Record<string, any>,
  callback?: (
    err: Error | null | undefined,
    context: Record<string, any>
  ) => void
) {
  const absoluteScriptPath = path.resolve(process.cwd(), scriptPath);

  const contextPath = options.context
    ? path.resolve(options.context)
    : path.dirname(absoluteScriptPath);

  debug('script:', absoluteScriptPath);
  debug('root:', contextPath);

  const context: Record<string, any> = {
    contextDir: contextPath,
    scriptPath: absoluteScriptPath,
    originalScriptPath: scriptPath,
    name: options.name, // test name, eg simple-bom or aht_$UUID
    manifestPath: options.manifestPath,
    packageJsonPath: options.packageJsonPath,
    flags: options.flags
  };

  if (typeof options.config === 'string') {
    const absoluteConfigPath = path.resolve(process.cwd(), options.config);
    context.configPath = absoluteConfigPath;
  }

  if (options.customSyncClient) {
    context.customSyncClient = options.customSyncClient;
  }

  return new Promise((resolve, reject) => {
    A.waterfall(
      [
        A.constant(context),
        async (context: Record<string, any>) => {
          if (!context.customSyncClient) {
            context.s3Bucket = await getBucketName();
            return context;
          } else {
            context.s3Bucket = 'S3_BUCKET_ARGUMENT_NOT_USED_ON_AZURE';
            return context;
          }
        },
        prepareManifest,
        printManifest,
        syncS3,
        writeTestMetadata
      ],
      (err: Error | null | undefined, context: Record<string, any>) => {
        if (err) {
          console.log(err);
          return;
        }

        if (callback) {
          callback(err, context);
        } else if (err) {
          reject(err);
        } else {
          resolve(context);
        }
      }
    );
  });
}

function prepareManifest(
  context: Record<string, any>,
  callback: (err: Error | null, context?: Record<string, any>) => void
) {
  let fileToAnalyse = context.scriptPath;
  const extraFiles: string[] = [];
  if (context.configPath) {
    debug('context has been provided; extraFiles =', extraFiles);
    fileToAnalyse = context.configPath;
    extraFiles.push(context.scriptPath);
  }

  createBOM(
    fileToAnalyse,
    extraFiles,
    {
      packageJsonPath: context.packageJsonPath,
      flags: context.flags,
      scenarioPath: context.scriptPath
    },
    (err: Error | null, bom: unknown) => {
      debug(err);
      debug(bom);
      context.manifest = bom;
      return callback(err, context);
    }
  );
}

function printManifest(
  context: Record<string, any>,
  callback: (err: Error | null, context?: Record<string, any>) => void
) {
  prettyPrint(context.manifest);
  return callback(null, context);
}

async function syncS3(context: Record<string, any>) {
  let s3: SyncClient;
  if (context.customSyncClient) {
    s3 = context.customSyncClient;
  } else {
    s3 = createS3Client();
  }

  const prefix = `tests/${context.name}`;

  context.s3Prefix = prefix;

  debug('Will try syncing to:', context.s3Bucket);

  debug('Manifest: ', context.manifest);

  // Iterate through manifest, for each element: has orig (local source) and noPrefix (S3
  // destination) properties
  return new Promise((resolve, reject) => {
    A.eachLimit(
      context.manifest.files,
      3,
      async (item: Record<string, any>, eachDone) => {
        // If we can't read the file, it may have been specified with a
        // template in its name, e.g. a payload file like:
        // {{ $environment }}-users.csv
        // If so, ignore it, hope config.includeFiles was used, and let
        // "artillery run" in the worker deal with it.
        let body: Buffer | undefined;
        try {
          body = fs.readFileSync(item.orig);
        } catch (fsErr) {
          debug(fsErr);
        }

        if (!body) {
          return eachDone(null, context);
        }

        // Filter bundled packages from package.json and enrich with detected
        // module versions before upload.
        if (item.noPrefix === 'package.json') {
          body = Buffer.from(
            enrichPackageJson(body.toString(), context.manifest.moduleVersions)
          );
        }

        const key = `${context.s3Prefix}/${item.noPrefixPosix}`;
        await s3.send(
          new PutObjectCommand({
            Bucket: context.s3Bucket,
            Key: key,
            Body: body
          })
        );
      },
      (err) => {
        if (err) {
          reject(err);
        } else {
          resolve(context);
        }
      }
    );
  });
}

// create just overwrites an existing test for now
async function writeTestMetadata(context: Record<string, any>) {
  const metadata: Record<string, any> = {
    createdOn: Date.now(),
    name: context.name,
    modules: context.manifest.modules
  };

  // Here we need to provide config information (if given) -- so that the worker knows how to load it
  if (context.configPath) {
    const res = context.manifest.files.filter((o: Record<string, any>) => {
      return o.orig === context.configPath;
    });
    const newConfigPath = res[0].noPrefixPosix; // if we have been given a config, we must have an entry
    metadata.configPath = newConfigPath;
  }

  const newScriptPath = context.manifest.files.filter(
    (o: Record<string, any>) => {
      return o.orig === context.scriptPath;
    }
  )[0].noPrefixPosix;
  metadata.scriptPath = newScriptPath;

  debug('metadata', metadata);

  let s3: SyncClient;
  if (context.customSyncClient) {
    s3 = context.customSyncClient;
  } else {
    s3 = createS3Client();
  }

  const key = `${context.s3Prefix}/metadata.json`; // TODO: Rename to something less likely to clash
  debug('metadata location:', `${context.s3Bucket}/${key}`);
  await s3.send(
    new PutObjectCommand({
      Body: JSON.stringify(metadata),
      Bucket: context.s3Bucket,
      Key: key
    })
  );

  return context;
}

export { tryCreateTest, createTest, syncS3, prepareManifest };
