import https from 'https';
import { Socket } from 'socket.io';
import { Readable } from 'stream';
import { v4 as uuidv4 } from 'uuid';
import Sentry from '../external-services/sentry.js';
import { logger } from '../server-utils/logger.js';
import query from '../server-utils/query.js';
import { SocketEvent } from '../server-utils/socketMessageHandler.js';
import { User } from '../types/serverTypes.js';
import runServiceWithQuota from '../utils/runServiceWithQuota.js';
import { putObject } from '../utils/s3Buffers.js';
import { GetSampleReturnType, SAMPLES_BUCKET, getSample, getSignedUrlForSampleS3Key, insertSample } from './samples.js';

type LalalaResponse =
  | { status: 'success'; id: string; expires: number; error: undefined }
  | { status: 'error'; id: undefined; expires: undefined; error: string };

const lalalaUpload = (stream: Readable, filename: string): Promise<LalalaResponse> => {
  return new Promise((resolve, reject) => {
    stream.pipe(
      https.request(
        'https://www.lalal.ai/api/upload/',
        {
          method: 'POST',
          headers: {
            'Content-Disposition': `attachment; filename=${filename}`,
            Authorization: `license ${process.env.LALALA_API_KEY}`,
          },
        },
        async (response) => {
          try {
            let responseBody: string = '';
            response.on('data', (chunk) => {
              responseBody += chunk;
            });

            response.on('end', () => {
              const parsedJson = JSON.parse(responseBody);
              resolve(parsedJson);
            });
          } catch (e) {
            reject(e);
          }
        }
      )
    );
  });
};

export const getLalalaFileId = async (existingSample: GetSampleReturnType, user: User): Promise<string> => {
  const existingFileResult = await query(
    `
    SELECT lalala_id FROM lalala_files WHERE sample_id = $1 AND expires_at > NOW();
  `,
    [existingSample.id]
  );
  const existingFileId = existingFileResult.rows?.[0]?.lalala_id;
  if (existingFileResult.rows?.[0]?.lalala_id) {
    console.log('had existing file id! returning.');
    return existingFileId;
  }

  const { readableStream, uuid } = await getSample(
    { id: existingSample.id, asUser: user },
    { returnReadableStream: true }
  );
  const response = await lalalaUpload(readableStream, `${uuid}.ogg`);
  const { status, id, error, expires } = response;

  if (status === 'error') {
    throw new Error(error);
  } else {
    await query(
      `
        INSERT INTO lalala_files (sample_id, lalala_id, expires_at) VALUES ($1, $2, $3);
      `,
      [existingSample.id, id, new Date(expires * 1000)]
    );
    return id;
  }
};

const queueLalalaSplit = async (
  id: string,
  stem: string,
  filter: number = 1,
  dereverb: boolean = false
): Promise<void> => {
  const params = {
    id,
    stem,
    filter,
    dereverb_enabled: dereverb,
  };
  const response = await fetch(`https://www.lalal.ai/api/split/`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
      Authorization: `license ${process.env.LALALA_API_KEY}`,
    },
    body: `params=${JSON.stringify([params])}`,
  });
  const { status, error } = await response.json();
  if (status === 'error') {
    // this appears to happen when we queue the same stem twice.
    if (error === 'Invalid argument') {
      console.log('Invalid argument error, probably a double-queue.');
    } else {
      throw new Error(error);
    }
  }
};

const queueLalalaCleanup = async (
  id: string,
  stem: string,
  filter: number = 1,
  dereverb: boolean = true,
  noiseCancellingLevel: number = 2
): Promise<void> => {
  const params = {
    id,
    stem,
    filter,
    dereverb_enabled: dereverb,
    noise_cancelling_level: noiseCancellingLevel,
  };
  const response = await fetch(`https://www.lalal.ai/api/split/`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
      Authorization: `license ${process.env.LALALA_API_KEY}`,
    },
    body: `params=${JSON.stringify([params])}`,
  });
  const { status, error } = await response.json();
  if (status === 'error') {
    // this appears to happen when we queue the same stem twice.
    if (error === 'Invalid argument') {
      console.log('Invalid argument error, probably a double-queue.');
    } else {
      throw new Error(error);
    }
  }
};

export const runLalala = async (user, { stem, bufferId, processMethod, dereverb, ncLevel }, onProgress) => {
  try {
    const stemCacheKey = `${bufferId}[${stem}-${processMethod || 'split'}-${dereverb ? 'dereverb' : 'no_dereverb'}-${
      ncLevel || 0
    }]`;
    const backingCacheKey = `${bufferId}[${stem}_backing]`;
    const [existingStemSample, existingBackingSample] = await Promise.all([
      getSample({ cacheKey: stemCacheKey, asUser: user }, {}),
      getSample({ cacheKey: backingCacheKey, asUser: user }, {}),
    ]);

    if (existingStemSample && existingBackingSample) {
      return {
        stem: {
          uuid: existingStemSample.uuid,
          url: await getSignedUrlForSampleS3Key(existingStemSample.s3Key),
        },
        backing: {
          uuid: existingBackingSample.uuid,
          url: await getSignedUrlForSampleS3Key(existingBackingSample.s3Key),
        },
      };
    }

    const existingSample = await getSample(
      { uuid: bufferId, asUser: user },
      {
        returnLength: true,
        ensureWaveform: false,
        returnWaveform: false,
        returnCompressed: false,
        returnBuffer: false,
        returnReadableStream: false,
      }
    );

    if (existingSample.lengthInMs > 5 * 60 * 1000) {
      return {
        error: true,
        message: 'Please choose an audio clip which is under 5 minutes in length.',
      };
    }

    const lalalaFileId = await getLalalaFileId(existingSample, user);

    logger.info(`got file id ${lalalaFileId} for buffer ${bufferId}`);

    if (processMethod === 'cleanup') {
      await queueLalalaCleanup(lalalaFileId, stem, undefined, dereverb, ncLevel);
    } else if (processMethod === 'split') {
      await queueLalalaSplit(lalalaFileId, stem, undefined, dereverb);
    } else {
      logger.info('no audio processing method specified, defaulting to split');
      await queueLalalaSplit(lalalaFileId, stem, undefined, dereverb);
    }

    logger.info(`queued split for buffer ${bufferId}`);

    const receiveSplit = async (stemUrl: string, isStemTrack: boolean): ReturnType<typeof insertSample> => {
      const sampleUUID = uuidv4();
      const s3Key = sampleUUID + '.ogg';
      logger.info('will save split:', s3Key);
      const response = await fetch(stemUrl);
      logger.info('putting object...');
      await putObject(SAMPLES_BUCKET, s3Key, Buffer.from(await response.arrayBuffer()));
      logger.info('put was successful.');
      let partTitleCase = stem
        .split('_')
        .map((word) => `${word[0].toUpperCase()}${word.slice(1)}`)
        .join(' ');
      const cacheKey = isStemTrack ? stemCacheKey : backingCacheKey;
      return await insertSample({
        uuid: sampleUUID,
        name: isStemTrack
          ? `${partTitleCase} from ${existingSample.name}`
          : `${existingSample.name} without ${partTitleCase}`,
        s3Key,
        authorId: existingSample.authorId,
        cacheKey,
        derivedFromSampleId: existingSample.id,
        derivationType: 'lalala',
        tags: [{ value: isStemTrack ? partTitleCase : `Without ${partTitleCase}`, category: 'Stem' }],
      });
    };

    const pollLimit = 1000;
    let pollCount = 0;

    const pollCheck = async (
      resolve: (result: { stem: { uuid: string; url: string }; backing: { uuid: string; url: string } }) => void,
      reject: (error: Error) => void
    ) => {
      const response = await fetch(`https://www.lalal.ai/api/check/?id=${lalalaFileId}`, {
        method: 'POST',
        headers: {
          Authorization: `license ${process.env.LALALA_API_KEY}`,
        },
      });
      const responseJSON = await response.json();
      if (responseJSON.status === 'error') {
        logger.error('error checking upload status', JSON.stringify(responseJSON, null, 2));
        reject(responseJSON.error);
      } else if (
        responseJSON.result[lalalaFileId]?.split?.stem_track &&
        responseJSON.result[lalalaFileId]?.split?.back_track
      ) {
        logger.info('saving samples');
        const [stemSample, backingSample] = await Promise.all([
          receiveSplit(responseJSON.result[lalalaFileId].split.stem_track, true),
          receiveSplit(responseJSON.result[lalalaFileId].split.back_track, false),
        ]);
        logger.info('getting urls', stemSample, backingSample);
        const [stemUrl, backingUrl] = await Promise.all([
          await getSignedUrlForSampleS3Key(stemSample.compressed_s3_key),
          await getSignedUrlForSampleS3Key(backingSample.compressed_s3_key),
        ]);
        onProgress(100);
        resolve({
          stem: { uuid: stemSample.uuid, url: stemUrl },
          backing: { uuid: backingSample.uuid, url: backingUrl },
        });
      } else if (pollCount++ > pollLimit) {
        logger.error('error: failed to emit after 1000 polling steps');
        reject(new Error('Timeout'));
      } else {
        const progress = responseJSON.result[lalalaFileId]?.task?.progress;
        onProgress(progress);
        setTimeout(() => pollCheck(resolve, reject), 500);
      }
    };
    return await new Promise((resolve, reject) => pollCheck(resolve, reject));
  } catch (e) {
    logger.error('error extracting stem', e);
    Sentry.captureException(e);
    return {
      error: true,
      message: 'Error extracting stem',
    };
  }
};

const handleRequest = async (socket: Socket, message: any) => {
  const user = (socket.request as any).user;
  const onProgress = (progress) => socket.emit(SocketEvent.Progress, progress);
  return await runLalala(user, message, onProgress);
};

export const lalalaMessageHandler = async (socket: Socket, message: any) => {
  const user = (socket.request as any).user;
  await runServiceWithQuota(
    user,
    'lalala',
    async () => {
      socket.emit(SocketEvent.Response, await handleRequest(socket, message));
    },
    () => {
      socket.emit(SocketEvent.Response, {
        error: true,
        message:
          'Quota exceeded. WavTool places quotas on some features to prevent abuse. You may request an exemption by emailing us at hello@wavtool.com.',
      });
    }
  );
};
