import { fs, Listr, log, S3, t, util } from '../common';

type IBundleArgs = {
  bundleDir: string;
  bucket: string;
  bucketKey: string;
  silent?: boolean;
};

type IManifestArgs = { bucket: string; source: string; target: string; silent?: boolean };

/**
 * Push API.
 */
export function push(args: t.IS3Config & { cli: t.ICmdApp }) {
  const s3 = fs.s3(args);
  const { cli } = args;
  return {
    bundle: (args: IBundleArgs) => bundle({ ...args, s3, cli }),
    manifest: (args: IManifestArgs) => manifest({ ...args, s3, cli }),
  };
}

/**
 * Pushes the specified bundle to S3.
 */
export async function bundle(args: {
  cli: t.ICmdApp;
  s3: S3;
  bundleDir: string;
  bucket: string;
  bucketKey: string;
  silent?: boolean;
}) {
  const { cli } = args;
  const bucket = args.s3.bucket(args.bucket);
  const bundleDir = fs.resolve(args.bundleDir);

  // Ensure the key is not prepended with the bucket name.
  const bucketKey = args.bucketKey.replace(new RegExp(`^${args.bucket}\/`), '');

  // Calculate the list of files to push.
  const files = await fs.glob.find(fs.join(bundleDir, '**'));
  const items = files.map((source) => {
    const key = fs.join(bucketKey, source.substring(bundleDir.length + 1));
    return { source, key };
  });

  // Log activity.
  if (!args.silent) {
    const { formatPath } = util;
    const version = fs.basename(bundleDir);
    const size = await fs.size.dir(bundleDir);
    const endpoint = fs.join(bucket.endpoint, args.bucket, args.bucketKey);
    log.info();
    log.info.cyan(`Pushing bundle ${log.white(version)} to S3 ☝`);
    log.info();
    log.info.gray(`  size:  ${log.magenta(size.toString())}`);
    log.info.gray(`  from:  ${formatPath(bundleDir)}`);
    log.info.gray(`  to:    ${formatPath(endpoint)}`);
    log.info();
  }

  // Push to S3.
  const dirSize = await fs.size.dir(bundleDir);
  const renderer = args.silent ? 'silent' : undefined;
  const tasks = new Listr(
    items.map((item) => {
      const { source, key } = item;
      const file = fs.basename(key);
      const fileSize = dirSize.files.find((item) => item.path.endsWith(`/${file}`));
      let size = fileSize ? fileSize.toString({ round: 0, spacer: '' }) : '';
      size = `${size}        `.substring(0, 8);
      return {
        title: `${size} ${file}`,
        task: async () => {
          const data = await fs.readFile(source);
          const res = await bucket.put({ data, key, acl: 'public-read' });
          if (!res.ok) {
            throw res.error;
          }
        },
      };
    }),
    { concurrent: true, renderer, exitOnError: false },
  );
  try {
    await tasks.run();
  } catch (error) {
    log.error(`\nFailed while pushing to S3.\n`);
    cli.exit(1);
  }

  // Finish up.
  return {
    bucket: args.bucket,
    endpoint: bucket.endpoint,
    s3: items,
    manifest,
  };
}

/**
 * Pushes the overall manifest to S3.
 */
export async function manifest(args: {
  cli: t.ICmdApp;
  s3: S3;
  bucket: string;
  source: string;
  target: string;
  silent?: boolean;
}) {
  const { cli, silent } = args;
  const source = fs.resolve(args.source);
  const bucket = args.s3.bucket(args.bucket);

  const filename = fs.basename(source);
  let target = args.target;
  target = target.endsWith(filename) ? target : fs.join(target, filename);

  // Log activity.
  if (!args.silent) {
    const { formatPath } = util;
    const size = await fs.size.file(source);
    const endpoint = fs.join(bucket.endpoint, args.bucket, target);
    log.info();
    log.info.cyan(`Pushing manifest to S3 ☝`);
    log.info();
    log.info.gray(`  size:  ${log.magenta(size.toString())}`);
    log.info.gray(`  from:  ${formatPath(source)}`);
    log.info.gray(`  to:    ${formatPath(endpoint)}`);
    log.info();
  }

  // Push to S3.
  const title = `push ${fs.basename(source)}`;
  const tasks = cli.task(title, async (e) => {
    const data = await fs.readFile(source);
    await bucket.put({ data, key: target, acl: 'public-read' });
  });

  await tasks.run({ silent });
  return { manifest };
}
