// media/job/sticker-ingest.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0

'use strict';

const DTP_STICKER_HEIGHT = 100;

const path = require('path');
const fs = require('fs');

const mongoose = require('mongoose');
const Sticker = mongoose.model('Sticker');

const sharp = require('sharp');

const { SiteWorkerProcess } = require(path.join(__dirname, '..', '..', '..', '..', 'lib', 'site-lib'));

class StickerIngestJob extends SiteWorkerProcess {

  static get COMPONENT ( ) {
    return {
      name: 'stickerIngestJob',
      slug: 'sticker-ingest-job',
    };
  }

  constructor (worker) {
    super(worker, StickerIngestJob.COMPONENT);
    this.processors = {
      processStickerSharp: this.processStickerSharp.bind(this),
      processStickerFFMPEG: this.processStickerFFMPEG.bind(this),
    };
  }

  async start ( ) {
    await super.start();

    this.queue = await this.getJobQueue('media', this.dtp.config.jobQueues.media);

    this.log.info('registering job processor', { queue: this.queue.name, name: 'sticker-ingest' });
    this.queue.process('sticker-ingest', 1, this.processStickerIngest.bind(this));
  }

  async stop ( ) {
    await super.stop();
  }

  async processStickerIngest (job) {
    try {
      this.log.info('received sticker ingest job', { id: job.id, data: job.data });
      await this.fetchSticker(job);
      await this.resetSticker(job);

      // call the chosen file processor to render the sticker for distribution
      await this.processors[job.data.processor](job);

      //TODO: emit a completion event which should cause a refresh of the
      // creator's view to display the processed sticker
    } catch (error) {
      this.log.error('failed to process sticker', { stickerId: job.data.stickerId, error });
      throw error;
    } finally {
      if (job.data.workPath) {
        this.log.info('cleaning up sticker work path', { workPath: job.data.workPath });
        await fs.promises.rm(job.data.workPath, { recursive: true });
      }
    }
  }

  async fetchSticker (job) {
    const { minio: minioService, sticker: stickerService } = this.dtp.services;
    job.data.sticker = await stickerService.getById(job.data.stickerId, true);

    job.data.workPath = path.join(
      process.env.DTP_STICKER_WORK_PATH,
      this.dtp.config.component.slug,
      job.data.sticker._id.toString(),
    );

    this.jobLog(job, 'creating work directory', { worthPath: job.data.workPath });
    await fs.promises.mkdir(job.data.workPath, { recursive: true });

    switch (job.data.sticker.original.type) {
      case 'image/jpeg':
        job.data.origFilePath = path.join(job.data.workPath, `${job.data.sticker._id}.jpg`);
        job.data.procFilePath = path.join(job.data.workPath, `${job.data.sticker._id}.proc.jpg`);
        job.data.processor = 'processStickerSharp';
        job.data.sharpFormat = 'jpeg';
        job.data.sharpFormatParameters = { quality: 85 };
        break;

      case 'image/png':
        job.data.origFilePath = path.join(job.data.workPath, `${job.data.sticker._id}.png`);
        job.data.procFilePath = path.join(job.data.workPath, `${job.data.sticker._id}.proc.png`);
        job.data.processor = 'processStickerSharp';
        job.data.sharpFormat = 'png';
        job.data.sharpFormatParameters = { compression: 9 };
        break;

      case 'image/gif':
        job.data.origFilePath = path.join(job.data.workPath, `${job.data.sticker._id}.gif`);
        job.data.procFilePath = path.join(job.data.workPath, `${job.data.sticker._id}.proc.mp4`);
        job.data.processor = 'processStickerFFMPEG';
        break;

      case 'image/webp': // process as PNG
        job.data.origFilePath = path.join(job.data.workPath, `${job.data.sticker._id}.webp`);
        job.data.procFilePath = path.join(job.data.workPath, `${job.data.sticker._id}.proc.png`);
        job.data.processor = 'processStickerSharp';
        job.data.sharpFormat = 'png';
        job.data.sharpFormatParameters = { compression: 9 };
        break;

      case 'image/webm': // process as MP4
        job.data.origFilePath = path.join(job.data.workPath, `${job.data.sticker._id}.webm`);
        job.data.procFilePath = path.join(job.data.workPath, `${job.data.sticker._id}.proc.mp4`);
        job.data.processor = 'processStickerFFMPEG';
        break;

      case 'video/mp4':
        job.data.origFilePath = path.join(job.data.workPath, `${job.data.sticker._id}.mp4`);
        job.data.procFilePath = path.join(job.data.workPath, `${job.data.sticker._id}.proc.mp4`);
        job.data.processor = 'processStickerFFMPEG';
        break;

      default:
        throw new Error(`unsupported sticker type: ${job.data.sticker.original.type}`);
    }

    this.jobLog(job, 'fetching original media', { // blah 62096adcb69874552a0f87bc
      stickerId: job.data.sticker._id,
      slug: job.data.sticker.slug,
      type: job.data.sticker.original.type,
      worthPath: job.data.origFilePath,
    });
    await minioService.downloadFile({
      bucket: job.data.sticker.original.bucket,
      key: job.data.sticker.original.key,
      filePath: job.data.origFilePath,
    });
  }

  async resetSticker (job) {
    const { minio: minioService } = this.dtp.services;
    const { sticker } = job.data;

    const updateOp = { $set: { status: 'processing' } };

    if (sticker.encoded) {
      this.log.info('removing existing encoded sticker media', { media: sticker.encoded });
      await minioService.removeObject(sticker.encoded.bucket, sticker.encoded.key);
      delete sticker.encoded;
      updateOp.$unset = { encoded: '' };
    }
    
    await Sticker.updateOne({ _id: sticker._id }, updateOp);
  }

  async processStickerSharp (job) {
    const { minio: minioService } = this.dtp.services;

    const sharpImage = sharp(job.data.origFilePath);
    const metadata = await sharpImage.metadata();
    this.log.info('sticker metadata from Sharp', { stickerId: job.data.sticker._id, metadata });

    let chain = sharpImage
      .clone()
      .toColorspace('srgb')
      .resize({ height: DTP_STICKER_HEIGHT });

    chain = chain[job.data.sharpFormat](job.data.sharpFormatParameters);

    await chain.toFile(job.data.procFilePath);

    job.data.outFileStat = await fs.promises.stat(job.data.procFilePath);

    const bucket = process.env.MINIO_VIDEO_BUCKET;
    const key = `/stickers/${job.data.sticker._id.toString().slice(0, 3)}/${job.data.sticker._id}.encoded.${job.data.sharpFormat}`;

    await minioService.uploadFile({
      bucket,
      key,
      filePath: job.data.procFilePath,
      metadata: {
        'Content-Type': `image/${job.data.sharpFormat}`,
        'Content-Length': job.data.outFileStat.size,
      },
    });

    await Sticker.updateOne(
      { _id: job.data.sticker._id },
      {
        $set: {
          status: 'live',
          encoded: {
            bucket,
            key,
            type: `image/${job.data.sharpFormat}`,
            size: job.data.outFileStat.size,
          }
        },
      },
    );
  }

  async processStickerFFMPEG (job) {
    const { media: mediaService, minio: minioService } = this.dtp.services;

    const codecVideo = (process.env.DTP_ENABLE_GPU === 'enabled') ? 'h264_nvenc' : 'libx264';

    // generate the encoded sticker
    // Output height is 100 lines by [aspect] width with width and height being
    // padded to be divisible by 2. The video stream is given a bit rate of
    // 128Kbps, and the media is flagged for +faststart. Audio is stripped if
    // present.

    const ffmpegStickerArgs = [
      '-y', '-i', job.data.origFilePath,
      '-vf', `scale=-1:${DTP_STICKER_HEIGHT},pad=ceil(iw/2)*2:ceil(ih/2)*2`,
      '-pix_fmt', 'yuv420p',
      '-c:v', codecVideo,
      '-b:v', '128k',
      '-movflags', '+faststart',
      '-an',
      job.data.procFilePath,
    ];

    this.jobLog(job, `transcoding motion sticker: ${job.data.sticker.slug}`);
    this.log.debug('transcoding motion sticker', { ffmpegStickerArgs });
    await mediaService.ffmpeg(ffmpegStickerArgs);

    job.data.outFileStat = await fs.promises.stat(job.data.procFilePath);

    const bucket = process.env.MINIO_VIDEO_BUCKET;
    const key = `/stickers/${job.data.sticker._id.toString().slice(0, 3)}/${job.data.sticker._id}.encoded.mp4`;

    this.jobLog(job, 'uploading encoded media file');
    await minioService.uploadFile({
      bucket, key,
      filePath: job.data.procFilePath,
      metadata: {
        'Content-Type': 'video/mp4',
        'Content-Length': job.data.outFileStat.size,
      },
    });

    this.jobLog(job, 'updating Sticker to live status');

    await Sticker.updateOne(
      { _id: job.data.sticker._id },
      {
        $set: {
          status: 'live',
          encoded: {
            bucket,
            key,
            type: 'video/mp4',
            size: job.data.outFileStat.size,
          },
        },
      },
    );
  }
}

module.exports = StickerIngestJob;