import multer from 'multer';
import { Socket } from 'socket.io';
import { v4 as uuidv4 } from 'uuid';
import generateSampleLength from '../generators/generateSampleLength.js';
import query, { transaction } from '../server-utils/query.js';
import { declareHandler } from '../server-utils/routesHandler.js';
import { SocketEvent } from '../server-utils/socketMessageHandler.js';
import { convertBufferToOpus } from '../signal-processing/conversions.js';
import { detectDownbeatsAndBPM } from '../signal-processing/downbeats.js';
import getAudioFromVideoUrl, { getAudioMetadataFromUrl } from '../signal-processing/getAudioFromVideoUrl.js';
import { User } from '../types/serverTypes.js';
import { putObject } from '../utils/s3Buffers.js';
import { runDemucs } from './demucs.js';
import { GetSampleReturnType, SAMPLES_BUCKET, getSample, insertSample } from './samples.js';
import { getUserById } from './users.js';

const DEFAULT_PACK_UUID = '52717858-964b-41f0-9393-9af14e23d133';

type QuickstartAction = 'rip';

export type LoadingLine = { text: string; progress: number };

const quickstartSample = async (
  user: User,
  sample: GetSampleReturnType,
  withStems: boolean,
  onProgress: (progress: LoadingLine[]) => void
) => {
  let interval: NodeJS.Timer;
  try {
    const defaultPack = await query(`SELECT * FROM sample_packs WHERE uuid = $1;`, [DEFAULT_PACK_UUID]);
    const defaultPackId = defaultPack.rows[0]?.id;
    if (!defaultPackId) {
      throw new Error('Default pack not found');
    }

    const tempoLine = { text: 'Analyzing Timing', progress: 0 };
    const stemLine = { text: 'Separating Instruments', progress: 0 };

    const hasDownbeatDetection =
      (
        await query(
          `SELECT * FROM sample_tags WHERE sample_id = $1 AND category = 'Analyzed' AND value = 'Downbeat Detection (Quickstart June 4 2024)'`,
          [sample.id]
        )
      )?.rows?.length > 0;

    if (!withStems) {
      onProgress([tempoLine]);

      let warp;
      if (hasDownbeatDetection) {
        warp = sample.warp;
      } else {
        const { bpm, downbeats } = await detectDownbeatsAndBPM(sample, (progress: number) => {
          tempoLine.progress = progress;
          onProgress([tempoLine]);
        });

        if (bpm) {
          const offsetBeats = downbeats[0]?.time * (bpm / 60);
          warp = {
            enabled: true,
            sourceBPM: bpm!,
            anchors: downbeats.reduce(
              (acc, { time }, index) => ({
                ...acc,
                [(bpm / 60) * time]: {
                  destination: offsetBeats + index,
                },
              }),
              {}
            ),
          };
        }
      }

      tempoLine.progress = 1;
      onProgress([tempoLine]);

      await transaction(async (query) => {
        if (!hasDownbeatDetection) {
          await query(
            `INSERT INTO sample_tags (sample_id, category, value) VALUES ($1, 'Analyzed', 'Downbeat Detection (Quickstart June 4 2024)')`,
            [sample.id]
          );
        }
        if (warp) {
          await query(`UPDATE samples SET warp=$1 WHERE uuid=$2 AND warp IS NULL`, [JSON.stringify(warp), sample.uuid]);
        }
        await query(`UPDATE samples SET pack_id=$1 WHERE uuid=$2 AND pack_id IS NULL`, [defaultPackId, sample.uuid]);
      });

      return {
        success: true,
        uuid: sample.uuid,
        bpm: warp.sourceBPM,
        duration: sample.lengthInMs,
      };
    }

    onProgress([tempoLine, stemLine]);

    const [warp, stems] = await Promise.all([
      hasDownbeatDetection
        ? Promise.resolve(sample.warp)
        : (async () => {
            onProgress([tempoLine, stemLine]);

            const { bpm, downbeats } = await detectDownbeatsAndBPM(sample, (progress: number) => {
              tempoLine.progress = progress;
              onProgress([tempoLine, stemLine]);
            });

            if (bpm) {
              const offsetBeats = downbeats[0]?.time * (bpm / 60);
              return {
                enabled: true,
                sourceBPM: bpm!,
                anchors: downbeats.reduce(
                  (acc, { time }, index) => ({
                    ...acc,
                    [(bpm / 60) * time]: {
                      destination: offsetBeats + index,
                    },
                  }),
                  {}
                ),
              };
            }
          })(),
      runDemucs(user, sample.uuid, defaultPackId, (progress: number) => {
        stemLine.progress = progress / 100;
        onProgress([tempoLine, stemLine]);
      }).then((stems) => {
        stemLine.progress = 1;
        onProgress([tempoLine, stemLine]);
        return stems;
      }),
    ]);

    tempoLine.progress = 1;
    stemLine.progress = 1;

    onProgress([tempoLine, stemLine]);

    await transaction(async (query) => {
      if (!hasDownbeatDetection) {
        await query(
          `INSERT INTO sample_tags (sample_id, category, value) VALUES ($1, 'Analyzed', 'Downbeat Detection (Quickstart June 4 2024)')`,
          [sample.id]
        );
      }
      if (warp) {
        await query(`UPDATE samples SET warp=$1 WHERE uuid=ANY($2) AND warp IS NULL`, [
          warp,
          [...Object.values(stems).map((s) => s.uuid), sample.uuid],
        ]);
      }
      await query(`UPDATE samples SET pack_id=$1 WHERE uuid=ANY($2) AND pack_id IS NULL`, [
        defaultPackId,
        [...Object.values(stems).map((s) => s.uuid), sample.uuid],
      ]);
    });

    onProgress([tempoLine, stemLine]);

    const bpm = warp?.sourceBPM;
    return {
      success: true,
      bpm,
      stems: Object.entries(stems).reduce((acc, [key, value]) => ({ ...acc, [key]: value.uuid }), {
        original: sample.uuid,
      }),
      duration: sample.lengthInMs,
    };
  } catch (e) {
    console.error(e);
    clearInterval(interval);
    return { success: false, error: e.message };
  }
};

const ripSample = async (
  url: string,
  name: string,
  withStems: boolean,
  onProgress: (progressLines: LoadingLine[]) => void
) => {
  onProgress([{ text: 'Downloading audio', progress: 0 }]);

  try {
    const user = await getUserById(1);

    const defaultPack = await query(`SELECT * FROM sample_packs WHERE uuid = $1;`, [DEFAULT_PACK_UUID]);
    const defaultPackId = defaultPack.rows[0]?.id;
    if (!defaultPackId) {
      throw new Error('Default pack not found');
    }

    const cacheKey = `[quickstart]${url}`;
    let existingSample = await getSample({ cacheKey, asUser: user }, { returnLength: true });

    if (!existingSample) {
      const uuid = uuidv4();
      const s3Key = `${uuid}.ogg`;
      const audioBuffer = await getAudioFromVideoUrl(url, uuid, s3Key);
      onProgress([{ text: 'Downloading audio', progress: 0.85 }]);

      // manually getting lengthInMs here also confirms that we have a valid audioBuffer
      const lengthInMs = await generateSampleLength(audioBuffer);
      onProgress([{ text: 'Downloading audio', progress: 0.9 }]);

      await putObject(SAMPLES_BUCKET, s3Key, audioBuffer);

      onProgress([{ text: 'Downloading audio', progress: 0.95 }]);

      await insertSample({
        name,
        uuid,
        s3Key,
        authorId: user.id,
        cacheKey,
        lengthInMs,
        packId: defaultPackId,
      });

      onProgress([{ text: 'Downloading audio', progress: 0.98 }]);

      existingSample = await getSample({ cacheKey, asUser: user }, { returnLength: true });
    }

    return await quickstartSample(user, existingSample, withStems, (progress: LoadingLine[]) =>
      onProgress([{ text: 'Downloading audio', progress: 1 }, ...progress])
    );
  } catch (e) {
    console.error(e);
    return { success: false, error: e.message };
  }
};

const upload: any = multer({ limits: { fieldSize: Math.pow(10, 8) } }); // 100MB limit
export const uploadAnonymousSample = declareHandler({
  middleware: upload.single('file'),
  func: async (req, res) => {
    const defaultPack = await query(`SELECT * FROM sample_packs WHERE uuid = $1;`, [DEFAULT_PACK_UUID]);
    const defaultPackId = defaultPack.rows[0]?.id;
    if (!defaultPackId) {
      throw new Error('Default pack not found');
    }
    const opusBuffer = await convertBufferToOpus(req.file.buffer, 5 * 60);
    const sampleUUID = uuidv4();
    const s3Key = sampleUUID + '.ogg';
    await putObject(SAMPLES_BUCKET, s3Key, opusBuffer);
    const user = await getUserById(1);
    let filenameWithoutExtension = req.file.originalname.split('.').slice(0, -1).join('.') || req.file.originalname;
    await insertSample({
      name: filenameWithoutExtension,
      uuid: sampleUUID,
      s3Key,
      authorId: user.id,
      lengthInMs: await generateSampleLength(opusBuffer),
      packId: defaultPackId,
    });
    res.send(sampleUUID);
  },
});

export const quickstartMessageHandler = async (socket: Socket, message: any) => {
  const action = message.action as QuickstartAction;

  if (action === 'rip') {
    socket.emit(
      SocketEvent.Final,
      await ripSample(message.payload.url, message.payload.name, message.payload.withStems, (progress: LoadingLine[]) =>
        socket.emit(SocketEvent.Progress, [...progress])
      )
    );
    return;
  } else if (action === 'uploaded') {
    const user = await getUserById(1);
    const sample = await getSample({ uuid: message.payload.uuid, asUser: user }, { returnLength: true });
    socket.emit(
      SocketEvent.Final,
      await quickstartSample(user, sample, message.payload.withStems, (progress: LoadingLine[]) =>
        socket.emit(SocketEvent.Progress, progress)
      )
    );
    return;
  }

  socket.emit(SocketEvent.Final, { error: true, message: 'Unknown action' });
};

export const getQuickstartMetadata = declareHandler({
  func: async (req, res) => {
    const { url } = req.body;
    try {
      const videoMetadata = await getAudioMetadataFromUrl(url);
      res.send(videoMetadata);
    } catch (e) {
      console.error('error getting metadata:', e);
      res.status(500).send({ success: false, error: e.message });
    }
  },
});
