import {
  CompleteMultipartUploadCommand,
  CreateMultipartUploadCommand,
  GetObjectCommand,
  S3Client,
  UploadPartCommand,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { Readable } from 'stream';
import { v4 as uuidv4 } from 'uuid';
import { enqueue } from '../external-services/redis.js';
import Sentry from '../external-services/sentry.js';
import generateSampleLength from '../generators/generateSampleLength.js';
import generateSampleWaveformFromBuffer from '../generators/generateSampleWaveform.js';
import query, { transaction } from '../server-utils/query.js';
import { declareHandler } from '../server-utils/routesHandler.js';
import { convertBufferToOpus } from '../signal-processing/conversions.js';
import { User, UserRole, isUuid } from '../types/serverTypes.js';
import { WorkQueue } from '../types/workQueue.js';
import { getObject, getObjectReadableStream, putObject } from '../utils/s3Buffers.js';
import trackServerEvent, { ActivityType } from '../utils/trackServerEvent.js';
import { audiocraftGenerateAudio } from './audiocraft.js';
import { parseSampleQuery } from './helpers/parseSampleQuery.js';
import interpretAudioGenPrompt from './interpretAudioGenPrompt.js';

/**
 * This file contains route handlers for our sample library.
 *
 * The sample library is a curated list of audio clips that we comissioned from humans.
 * Users might one day be able to upload their own samples.
 */

const s3Accelerated = new S3Client({ region: 'us-east-1', useAccelerateEndpoint: true });

export const SAMPLES_BUCKET = 'samples-test-1';

// 2023-11-29: relax constraint "part of a published pack" to "part of a pack" for reverse compatibility
const permissionCheckFragment = `
  (
    -- sample is part of a pack
    sample_packs.id IS NOT NULL

    -- sample is authored by the user
    OR COALESCE(samples.author_id = $1, FALSE)

    -- the user is an admin
    OR $2

    -- sample is accessible via a remix or remixable project
    OR (
      SELECT EXISTS (
        SELECT FROM project_samples ps
        JOIN projects p ON ps.project_id = p.id
        WHERE ps.sample_id = samples.id
        AND (
          -- sample is part of a remix created by the user
          COALESCE(p.user_id = $1, FALSE)

          -- sample is part of a project, the current revision of which is remixable
          OR p.superseded_by IS NULL AND p.remixable
        )
      )
    )
  )`;

const userPermissionCheckParams = (user: User | null) => [user?.id, user?.role === UserRole.Admin];
const requestPermissionCheckParams = (req) => userPermissionCheckParams(req.user);

export const filterPermittedSamples = async (sampleUuids: Array<string>, user: User | null): Promise<Array<string>> => {
  const permittedSamplesQuery = await query(
    `
      SELECT DISTINCT samples.uuid
      FROM samples
      LEFT JOIN sample_packs ON samples.pack_id = sample_packs.id
      WHERE
        samples.uuid = ANY($3::text[])
        AND ${permissionCheckFragment}
    `,
    [...userPermissionCheckParams(user), sampleUuids]
  );
  return permittedSamplesQuery.rows.map((row) => row.uuid);
};

export const searchSamples = declareHandler({
  func: async (req, res) => {
    const limit = parseInt(req.query.limit) || 100;
    const offset = parseInt(req.query.offset) || 0;
    const type = req.query.type || null;

    const searchTerm = (req.query.search || '') as string;

    // find any bpm queries in the overall search string (e.g. "120" or "120bpm").
    // split the search string into two parts: the bpm query and the rest of the search string
    // execute SQL query on the search string without the bpm query, then filter the results by parsing the bpm query on js side
    const { bpm: targetBpm, text: nonBPMQuery = '' } = parseSampleQuery(searchTerm);
    const hasBpmQuery = Boolean(targetBpm);

    const typeFragment = type ? `AND sample_tags.value = $4 AND sample_tags.category = 'Type'` : '';

    const bpmMinIndex = typeFragment ? 5 : 4;
    const bpmMaxIndex = typeFragment ? 6 : 5;

    const samples = await query(
      `
        WITH matching_samples AS ( SELECT DISTINCT sample_id FROM sample_tags WHERE LOWER(value) ILIKE '%' || LOWER($3) || '%' )

        SELECT
          samples.id,
          samples.uuid,
          samples.name,
          samples.pack_id,
          samples.length_in_ms,
          samples.waveform_svg,
          samples.warp::jsonb,
          samples.bpm,
          users.display_name AS author_name,
          json_agg(
            CASE
              -- Include the JSON object only when the category and value columns are not NULL
              WHEN sample_tags.category IS NOT NULL AND sample_tags.value IS NOT NULL
              THEN json_build_object('category', sample_tags.category, 'value', sample_tags.value)
              -- Return NULL otherwise
              ELSE NULL
            END
          ) FILTER (WHERE sample_tags.category IS NOT NULL AND sample_tags.value IS NOT NULL) AS tags
        FROM samples
          INNER JOIN users ON samples.author_id = users.id
          INNER JOIN sample_packs ON samples.pack_id = sample_packs.id
          LEFT JOIN sample_tags ON sample_tags.sample_id = samples.id
          LEFT JOIN matching_samples ON samples.id = matching_samples.sample_id
        WHERE
          (samples.name ILIKE '%' || $3 || '%' OR users.display_name ILIKE '%' || $3 || '%' OR matching_samples.sample_id = samples.id)
        ${typeFragment}
        AND ${permissionCheckFragment}
        ${req.user?.role === UserRole.Admin ? '' : 'AND sample_packs.published'}
        ${hasBpmQuery ? `AND (samples.bpm >= $${bpmMinIndex} AND samples.bpm <= $${bpmMaxIndex})` : ''}
        GROUP BY
          samples.id, samples.uuid, samples.name, samples.pack_id, samples.length_in_ms, samples.waveform_svg, samples.warp::jsonb, users.display_name
        ${hasBpmQuery ? 'ORDER BY samples.bpm DESC' : 'ORDER BY samples.name ASC'}
      `,
      [
        ...requestPermissionCheckParams(req),
        nonBPMQuery,
        ...(type ? [type] : []),
        ...(hasBpmQuery ? [targetBpm - 5, targetBpm + 5] : []),
      ]
    );

    const paginatedData = [];
    for (let i = offset; i < offset + limit; i++) {
      if (!!samples.rows[i]) paginatedData.push(samples.rows[i]);
    }

    const data = paginatedData.map((row) => ({
      id: row.id,
      uuid: row.uuid,
      name: row.name,
      packId: row.pack_id,
      lengthInMs: row.length_in_ms,
      author: row.author_name,
      warp: row.warp,
      waveformSvg: row.waveform_svg,
      bpm: row.bpm,
      tags: row.tags,
    }));
    res.send({ data, hasMore: samples.rows.length > offset + limit, count: samples.rows.length });
  },
});

export const getSampleData = declareHandler({
  func: async (req, res) => {
    const sampleQuery = await query(
      `
      SELECT
        samples.uuid,
        samples.name,
        COALESCE(samples.s3_key, samples.compressed_s3_key) AS s3_key,
        sample_packs.uuid as pack_uuid,
        samples.length_in_ms,
        samples.waveform_svg,
        samples.warp::jsonb,
        users.display_name AS author_name,
        json_agg(
          CASE
            -- Include the JSON object only when the category and value columns are not NULL
            WHEN sample_tags.category IS NOT NULL AND sample_tags.value IS NOT NULL
            THEN json_build_object('category', sample_tags.category, 'value', sample_tags.value)
            -- Return NULL otherwise
            ELSE NULL
          END
        ) FILTER (WHERE sample_tags.category IS NOT NULL AND sample_tags.value IS NOT NULL) AS tags
      FROM samples
        INNER JOIN users ON samples.author_id = users.id
        LEFT JOIN sample_tags ON sample_tags.sample_id = samples.id
        LEFT JOIN sample_packs ON samples.pack_id = sample_packs.id
      WHERE samples.uuid = $3
      AND ${permissionCheckFragment}
      GROUP BY
        samples.uuid, samples.name, samples.s3_key, samples.compressed_s3_key, samples.pack_id, samples.length_in_ms, samples.waveform_svg, samples.warp::jsonb, users.display_name, sample_packs.uuid
      `,
      [...requestPermissionCheckParams(req), req.params.uuid?.toLowerCase()]
    );

    if (!sampleQuery.rows?.[0]) return res.send({ error: 'sample not found' });

    const {
      uuid,
      length_in_ms: lengthInMs,
      warp,
      s3_key: s3Key,
      waveform_svg: waveformSvg,
      pack_uuid: packId,
      author_name: author,
      name,
      tags,
    } = sampleQuery.rows?.[0];
    const data = {
      uuid,
      s3Key,
      name,
      packId,
      lengthInMs,
      author,
      warp,
      waveformSvg,
      tags,
    };
    res.send(data);
  },
});

export const updateSampleData = declareHandler({
  func: async (req, res) => {
    if (req.user?.role !== UserRole.Admin) {
      res.status(403).send({ error: 'Not authorized' });
      return;
    }
    const { name, tags } = req.body;
    const sampleIdQuery = await query(`SELECT id FROM samples WHERE uuid = $1`, [req.params.uuid?.toLowerCase()]);
    if (!sampleIdQuery.rows[0]) {
      return res.send({ error: 'sample not found' });
    }
    const sampleId = sampleIdQuery.rows[0].id;
    if (name) {
      await query(`UPDATE samples SET name = $1 WHERE id = $2`, [name, sampleId]);
    }
    if (Array.isArray(tags) && tags.length > 0) {
      let queryString = `INSERT INTO sample_tags (sample_id, category, value) VALUES `;
      for (let i = 1, j = 0; j < tags.length; i += 3) {
        queryString += `($${i}, $${i + 1}, $${i + 2})${j === tags.length - 1 ? '' : ', '}`;
        j += 1;
      }
      queryString += ` ON CONFLICT (sample_id, category, value) DO NOTHING`;
      const valuesArr = tags.map((tag) => [sampleId, tag.category, tag.value]).flat();

      await query(queryString, valuesArr);
    }

    res.send({ success: true });
  },
});

export const addTagToSamplesInPack = declareHandler({
  func: async (req, res) => {
    if (req.user?.role !== UserRole.Admin) {
      res.status(403).send({ error: 'Not authorized' });
      return;
    }
    const { category, value } = req.body;

    const samplesInPackQuery = await query(
      `
      SELECT s.*
      FROM samples s
      INNER JOIN sample_packs sp
      ON s.pack_id = sp.id
      WHERE sp.uuid = $1
    `,
      [req.params.uuid?.toLowerCase()]
    );
    if (samplesInPackQuery.rows.length === 0) {
      return res.send({ error: 'samples not found' });
    }

    try {
      const sampleIds = samplesInPackQuery.rows.map(({ id }) => id);
      const promises = sampleIds.map((id) => {
        return query(`INSERT INTO sample_tags (sample_id, category, value) VALUES ($1, $2, $3)`, [id, category, value]);
      });
      await Promise.all(promises);
      res.send({ success: true });
    } catch (e) {
      res.esnd({ error: 'error adding tag to samples' });
    }
  },
});

export const deleteSampleTag = declareHandler({
  func: async (req, res) => {
    if (req.user?.role !== UserRole.Admin) {
      res.status(403).send({ error: 'Not authorized' });
      return;
    }
    const { uuid } = req.params;
    const { category, value } = req.body;
    const sampleIdQuery = await query(`SELECT id FROM samples WHERE uuid = $1`, [uuid?.toLowerCase()]);
    if (!sampleIdQuery.rows[0]) {
      return res.send({ error: 'sample not found' });
    }
    const sampleId = sampleIdQuery.rows[0].id;
    await query('DELETE FROM sample_tags WHERE sample_id = $1 AND category = $2 AND value = $3', [
      sampleId,
      category,
      value,
    ]);

    res.send({ success: true });
  },
});

export const getGeneratedSamplesByUserId = declareHandler({
  func: async (req, res) => {
    const samples = await query(
      `
        SELECT
          samples.id,
          samples.uuid,
          samples.name,
          samples.pack_id,
          samples.length_in_ms,
          samples.waveform_svg,
          samples.warp,
          users.display_name AS author_name
        FROM samples INNER JOIN users ON samples.author_id = users.id
        WHERE (samples.author_id = $1)
        AND (samples.id IN (SELECT sample_id FROM sample_tags WHERE value = 'Approved' AND category = 'Approval Status'))
        AND (samples.id IN (SELECT sample_id FROM sample_tags WHERE value = 'Generated' AND category = 'Type'))
        ORDER BY samples.created_at DESC
      `,
      [req.user.id]
    );
    const data = samples.rows.map((row) => ({
      id: row.id,
      uuid: row.uuid,
      name: row.name,
      packId: row.pack_id,
      lengthInMs: row.length_in_ms,
      author: row.author_name,
      warp: row.warp,
      waveformSvg: row.waveform_svg,
    }));
    res.send({ data });
  },
});

export const getSamplePacks = declareHandler({
  func: async (req, res) => {
    const limit = parseInt(req.query.limit) || 100;
    const offset = parseInt(req.query.offset) || 0;
    const searchTerm = req.query.search || '';
    let publishedClause = '';
    if (req.user?.role !== UserRole.Admin) {
      publishedClause = `AND published = TRUE`;
    }

    let samplePackQueryStr = `
    SELECT
      sample_packs.uuid AS uuid,
      sample_packs.name AS name,
      sample_packs.published AS published,
      users.display_name AS author_name,
      COUNT(samples.id) AS sample_count
    FROM sample_packs
    JOIN users ON sample_packs.author_id = users.id
    JOIN samples ON samples.pack_id = sample_packs.id
    ${
      searchTerm
        ? `WHERE (sample_packs.name ILIKE '%' || $1 || '%' OR users.display_name ILIKE '%' || $1 || '%' OR samples.name ILIKE '%' || $1 || '%')`
        : ''
    }
    ${publishedClause}
    GROUP BY sample_packs.id, users.display_name
    ORDER BY sample_packs.created_at DESC
  `;
    const samplePacks = await query(samplePackQueryStr, searchTerm ? [searchTerm] : []);

    const paginatedSamplePacks = [];
    for (let i = offset; i < offset + limit; i++) {
      if (!!samplePacks.rows[i]) {
        paginatedSamplePacks.push(samplePacks.rows[i]);
      }
    }

    const data = paginatedSamplePacks.map((row) => ({
      uuid: row.uuid,
      name: row.name,
      authorName: row.author_name,
      published: row.published,
      sampleCount: row.sample_count,
    }));
    res.send({ data, count: samplePacks.rows.length, hasMore: samplePacks.rows.length > limit + offset });
  },
});

export const insertSample = async ({
  name,
  uuid,
  s3Key,
  authorId,
  packId = null,
  warp = null,
  cacheKey = null,
  analysis = {},
  derivedFromSampleId = null,
  derivationType = null,
  tags = [],
  lengthInMs = null,
}: {
  name: string;
  uuid: string;
  s3Key: string;
  authorId: number;
  packId?: number | null;
  warp?: any | null;
  cacheKey?: string | null;
  analysis?: any;
  derivedFromSampleId?: number | null;
  derivationType?: string | null;
  lengthInMs?: number | null;
  tags?: { category: string; value: string }[];
}) => {
  return await transaction(async (query) => {
    const extension = s3Key.split('.').pop();
    const isCompressed = extension === 'ogg' || extension === 'mp3';
    const lowerUuid = uuid.toLowerCase();

    const sampleQuery = await query(
      `INSERT INTO samples (name, ${
        isCompressed ? 'compressed_s3_key' : 's3_key'
      }, author_id, pack_id, uuid, warp, cache_key, analysis, length_in_ms) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT (uuid) DO NOTHING RETURNING *`,
      [name, s3Key, authorId, packId, lowerUuid, warp, cacheKey, analysis, lengthInMs]
    );
    const insertedRow = sampleQuery.rows[0];

    if (insertedRow) {
      enqueue(WorkQueue.SampleUpload, { sampleId: insertedRow.id });

      if (derivedFromSampleId && derivationType) {
        await query(
          `INSERT INTO sample_derivations (sample_id, derived_from_id, derivation_type) VALUES ($1, $2, $3)`,
          [insertedRow.id, derivedFromSampleId, derivationType]
        );
      }

      for (let tag of tags) {
        await query(`INSERT INTO sample_tags (sample_id, value, category) VALUES ($1, $2, $3)`, [
          insertedRow.id,
          tag.value,
          tag.category,
        ]);
      }

      return insertedRow;
    }

    return (await query(`SELECT * FROM samples WHERE uuid = $1`, [lowerUuid])).rows[0];
  });
};

class SampleNotFoundError extends Error {}

export type GetSampleReturnType = {
  id: number;
  s3Key: string;
  name: string;
  lengthInMs: number;
  packId: number;
  authorId: number;
  uuid: string;
  warp: any;
  timestamp: Date;
  bpm: number;
  waveformSvg: string;
  analysis: any;
  buffer?: Buffer;
  readableStream?: Readable;
};

export type GetSampleDesignator = {
  id?: number;
  uuid?: string;
  cacheKey?: string;
  asUser?: User;
  bypassPermissionCheck?: boolean;
};

export type GetSampleLazyOptions = {
  returnLength?: boolean;
  ensureWaveform?: boolean;
  returnWaveform?: boolean;
  returnCompressed?: boolean;
  returnBuffer?: boolean;
  returnReadableStream?: boolean;
};

export const getSample = async (
  designator: GetSampleDesignator,
  lazyOptions: GetSampleLazyOptions
): Promise<GetSampleReturnType> => {
  let designatorColumn = '';
  let designatorValue;
  if (designator.cacheKey) {
    designatorColumn = 'cache_key';
    designatorValue = designator.cacheKey;
  } else if (designator.id) {
    designatorColumn = 'id';
    designatorValue = designator.id;
  } else if (designator.uuid) {
    designatorColumn = 'uuid';
    designatorValue = designator.uuid.toLowerCase();
  } else {
    throw new Error('Must specify id, uuid, or cacheKey');
  }

  const { bypassPermissionCheck, asUser } = designator;

  const sampleQueryResult = await query(
    `SELECT
      samples.id,
      samples.name,
      samples.s3_key,
      samples.length_in_ms,
      samples.pack_id,
      samples.author_id,
      samples.uuid,
      samples.waveform_svg IS NOT NULL has_waveform_svg,
      samples.warp,
      samples.created_at,
      samples.bpm,
      samples.compressed_s3_key,
      samples.analysis
     FROM samples
     LEFT JOIN sample_packs ON samples.pack_id = sample_packs.id
     WHERE samples.${designatorColumn} = $${bypassPermissionCheck ? 1 : 3}
     AND ${bypassPermissionCheck ? 'TRUE' : permissionCheckFragment}
     ORDER BY id DESC LIMIT 1`,
    [...(bypassPermissionCheck ? [] : userPermissionCheckParams(asUser)), designatorValue]
  );
  const sampleRow = sampleQueryResult.rows[0];

  if (!sampleRow) {
    if (designator.cacheKey) {
      return null;
    }
    throw new SampleNotFoundError(`Sample with ${designatorColumn} ${designatorValue} not found`);
  }

  let sample: GetSampleReturnType = {
    id: sampleRow.id,
    s3Key: sampleRow.s3_key || sampleRow.compressed_s3_key,
    name: sampleRow.name,
    lengthInMs: sampleRow.length_in_ms,
    packId: sampleRow.pack_id,
    authorId: sampleRow.author_id,
    uuid: sampleRow.uuid,
    warp: sampleRow.warp,
    timestamp: sampleRow.created_at,
    bpm: sampleRow.bpm,
    waveformSvg: null,
    analysis: sampleRow.analysis,
  };

  let buffer = null;
  const getBuffer = async () => {
    if (!buffer) {
      buffer = await getObject(SAMPLES_BUCKET, sample.s3Key);
    }
    return buffer;
  };

  const { returnLength, ensureWaveform, returnWaveform, returnCompressed, returnBuffer, returnReadableStream } =
    lazyOptions;

  if (sample.lengthInMs === null && returnLength) {
    const lengthInMs = await generateSampleLength(await getBuffer());
    await query('UPDATE samples SET length_in_ms = $1 WHERE id = $2', [lengthInMs, sample.id]);
    sample.lengthInMs = lengthInMs;
  }

  if (!sampleRow.has_waveform_svg && (ensureWaveform || returnWaveform)) {
    sample.waveformSvg = await generateSampleWaveformFromBuffer(await getBuffer());
    await query('UPDATE samples SET waveform_svg = $1 WHERE id = $2', [sample.waveformSvg, sample.id]);
  }

  if (sample.waveformSvg === null && returnWaveform) {
    const result = await query('SELECT waveform_svg FROM samples WHERE id = $1', [sample.id]);
    sample.waveformSvg = result.rows?.[0]?.waveform_svg;
  }

  if (returnCompressed) {
    if (!sampleRow.compressed_s3_key) {
      const compressedBuffer = await convertBufferToOpus(await getBuffer());
      const compressedS3Key = replaceFileExtension(sampleRow.s3_key, 'ogg');
      await putObject(SAMPLES_BUCKET, compressedS3Key, compressedBuffer);
      await query('UPDATE samples SET compressed_s3_key = $1 WHERE id = $2', [compressedS3Key, sample.id]);
      sample.s3Key = compressedS3Key;
    } else {
      sample.s3Key = sampleRow.compressed_s3_key;
    }
  }

  if (returnBuffer) {
    sample.buffer = await getBuffer();
  }

  if (returnReadableStream) {
    sample.readableStream = await getObjectReadableStream(SAMPLES_BUCKET, sample.s3Key);
  }

  return sample;
};

export const getSignedUrlForSampleS3Key = async (s3Key: string) => {
  return await getSignedUrl(
    s3Accelerated,
    new GetObjectCommand({
      Bucket: SAMPLES_BUCKET,
      Key: s3Key,
    }),
    { expiresIn: 3600 }
  );
};

export const generateSample = declareHandler({
  func: async (req, res) => {
    try {
      const user = req.user;
      const { prompt, bpm } = req.body;

      const generatedAudioQueryResult = await query(
        `SELECT COUNT(*) AS count FROM activity WHERE user_id = $1 AND action = 'Generate Audio' AND created_at > NOW() - INTERVAL '8 hours';`,
        [user.id]
      );
      if (generatedAudioQueryResult.rows[0].count > 100) {
        res.status(429).send({ error: 'Too many requests' });
        return;
      }
      if (!prompt) {
        res.status(400).send({ error: 'Missing Prompt' });
        return;
      }

      trackServerEvent(user.id, {
        name: ActivityType.GenerateAudio,
        properties: { prompt },
      });

      const config = await interpretAudioGenPrompt(prompt);

      const finalBPM = !config.bpm || Number.isNaN(config.bpm) ? bpm : config.bpm;

      const fullPrompt = config.soundEffect
        ? config.textPrompt
        : config.textPrompt +
          (config.key ? ` in the key of ${config.key}` : '') +
          (finalBPM ? ` at ${finalBPM} bpm` : '') +
          `${config.movieScene ? `, perfect for ${config.movieScene}` : ''}`;

      const barDuration = (60 / (finalBPM || 60)) * 4;
      const generationDuration = config.soundEffect ? 5 : barDuration * 3;

      if (process.env.ENVIRONMENT === 'development') {
        console.log({ prompt, fullPrompt });
      }

      const generatedResults = (await audiocraftGenerateAudio(
        fullPrompt,
        prompt,
        null,
        generationDuration,
        'ogg',
        user.id,
        config.soundEffect ? undefined : finalBPM,
        () => {},
        true
      )) as { uuid: string; url: string }[];

      const response: { uuid: string; lengthInMs: number; waveformSvg: string; url: string }[] = [];

      for (let i = 0; i < generatedResults.length; i++) {
        const { uuid, url } = generatedResults[i];
        const {
          id: sampleId,
          lengthInMs,
          waveformSvg,
        } = await getSample({ uuid, asUser: user }, { returnLength: true, returnWaveform: true });

        await transaction(async (query) => {
          await query('INSERT INTO sample_tags (sample_id, value, category) VALUES ($1, $2, $3)', [
            sampleId,
            'Generated',
            'Type',
          ]);
          await query('INSERT INTO sample_tags (sample_id, value, category) VALUES ($1, $2, $3)', [
            sampleId,
            'Pending',
            'Approval Status',
          ]);
        });
        response.push({ uuid, lengthInMs, waveformSvg, url });
      }
      res.send(response);
    } catch (e) {
      console.error('error generating sample', e);
      Sentry.captureException(e);
      res.status(500).send({ error: 'Error generating audio' });
      return;
    }
  },
});

const updateGeneratedSampleApprovalStatus = async (req, res, approvalStatus: string) => {
  const { uuid } = req.body;
  if (!uuid) {
    return res.send({ error: 'no sample uuid provided' });
  }

  let sample;
  try {
    sample = await getSample({ uuid, asUser: req.user }, {});
  } catch (e) {
    if (e instanceof SampleNotFoundError) {
      return res.send({ error: 'sample not found' });
    }
    throw e;
  }
  await query('UPDATE sample_tags SET value = $1 WHERE sample_id = $2 AND category = $3 RETURNING id', [
    approvalStatus,
    sample.id,
    'Approval Status',
  ]);
  return res.send({ success: true });
};

export const rejectGeneratedSample = declareHandler({
  func: async (req, res) => {
    return await updateGeneratedSampleApprovalStatus(req, res, 'Rejected');
  },
});

export const acceptGeneratedSample = declareHandler({
  func: async (req, res) => {
    return await updateGeneratedSampleApprovalStatus(req, res, 'Approved');
  },
});

export const replaceFileExtension = (fileName: string, newExtension: string): string => {
  const split = fileName.split('.');
  split.pop();
  return `${split.join('.')}.${newExtension}`;
};

export const getSampleWaveformByUuid = declareHandler({
  func: async (req, res) => {
    try {
      const sample = await getSample({ uuid: req.params.uuid, asUser: req.user }, { returnWaveform: true });
      return res.send({ waveformSvg: sample.waveformSvg });
    } catch (e) {
      if (e instanceof SampleNotFoundError) {
        return res.send({ error: 'sample not found' });
      }
      throw e;
    }
  },
});

export const getSampleUrlByUuid = declareHandler({
  func: async (req, res) => {
    const uncompressed = req.url.includes('uncompressed=true');

    try {
      const sample = await getSample(
        { uuid: req.params.uuid, asUser: req.user },
        { returnCompressed: !uncompressed, returnLength: true }
      );
      return res.send({ url: await getSignedUrlForSampleS3Key(sample.s3Key), lengthInMs: sample.lengthInMs });
    } catch (e) {
      if (e instanceof SampleNotFoundError) {
        return res.send({ error: 'sample not found' });
      }
      throw e;
    }
  },
});

export const createSamplePack = declareHandler({
  func: async (req, res) => {
    if (req.user.role !== UserRole.Admin) {
      res.status(403).send({ error: 'Not authorized' });
      return;
    }

    const packUuid = uuidv4();

    const newPackQuery = await query(
      `INSERT INTO sample_packs (name, uuid, author_id) VALUES ($1, $2, $3) RETURNING *`,
      [req.body.name, packUuid, req.body.authorId]
    );

    const newPack = newPackQuery.rows[0];
    res.send({ ...newPack });
  },
});

export const startSampleUpload = declareHandler({
  func: async (req, res) => {
    const { uuid, isCompressed } = req.body;
    const lowerUuid = uuid?.toLowerCase();

    // Do not move below isUuid check - there are some samples in production with non-uuid uuids
    const existsQuery = await query(
      `SELECT id, COALESCE(s3_key, compressed_s3_key) s3_key FROM samples WHERE uuid = $1`,
      [lowerUuid]
    );
    if (existsQuery.rows.length > 0) {
      return res.send({ exists: true, s3Key: existsQuery.rows[0].s3_key });
    }

    if (!isUuid(lowerUuid)) {
      return res.status(400).send({ error: 'Invalid payload' });
    }

    const s3Key = `${lowerUuid}.${isCompressed ? 'ogg' : 'wav'}`;

    const muCommand = new CreateMultipartUploadCommand({
      Bucket: SAMPLES_BUCKET,
      Key: s3Key,
    });
    const muResponse = await s3Accelerated.send(muCommand);
    const uploadId = muResponse.UploadId;

    res.send({ exists: false, s3Key, uploadId });
  },
});

export const getSampleUploadPartURL = declareHandler({
  func: async (req, res) => {
    const { s3Key, uploadId, partNumber } = req.body;

    const url = await getSignedUrl(
      s3Accelerated,
      new UploadPartCommand({
        Bucket: SAMPLES_BUCKET,
        Key: s3Key,
        UploadId: uploadId,
        PartNumber: partNumber,
      })
    );

    res.send({ url });
  },
});

export const finishSampleUpload = declareHandler({
  func: async (req, res) => {
    const { name, warp, authorId, packUuid, s3Key, uploadId, partTags } = req.body;

    if ((authorId || packUuid) && req.user.role !== UserRole.Admin) {
      res.status(403).send({ error: 'Not authorized to assign samples to other authors or packs.' });
      return;
    }

    let packId = null;
    if (packUuid) {
      const packIdResult = await query(`SELECT id FROM sample_packs WHERE uuid = $1`, [packUuid.toLowerCase()]);
      if (packIdResult.rows.length === 0) {
        res.status(400).send({ error: 'Invalid pack uuid' });
        return;
      }
      packId = packIdResult.rows[0].id;
    }

    if (typeof name !== 'string') {
      res.status(400).send({ error: 'Must specify name' });
      return;
    }
    if (
      typeof s3Key !== 'string' ||
      typeof uploadId !== 'string' ||
      !Array.isArray(partTags) ||
      partTags.length === 0 ||
      partTags.some((tag) => typeof tag !== 'string')
    ) {
      res.status(400).send({ error: 'Invalid s3Key, uploadId, or partTags' });
      return;
    }

    const cuCommand = new CompleteMultipartUploadCommand({
      Bucket: SAMPLES_BUCKET,
      Key: s3Key,
      UploadId: uploadId,
      MultipartUpload: {
        Parts: partTags.map((tag, i) => ({ ETag: tag, PartNumber: i + 1 })),
      },
    });
    const cuResponse = await s3Accelerated.send(cuCommand);
    if (cuResponse.Key !== s3Key) {
      res.status(500).send({ error: 'CompleteMultipartUpload failed' });
      return;
    }

    const uuid = s3Key.split('.')[0];
    const insertedRow = await insertSample({
      name,
      uuid,
      s3Key,
      authorId: authorId || req.user.id,
      packId,
      warp,
    });

    res.send({ success: true, sampleId: insertedRow.id });
  },
});

export const getSamplePackByUuid = declareHandler({
  func: async (req, res) => {
    const packQuery = await query(
      `
        SELECT
          sp.id as pack_id,
          sp.name as pack_name,
          sp.uuid as pack_uuid,
          sp.author_id,
          u.display_name as author_name,
          (
            SELECT COALESCE(json_agg(row_to_json((SELECT t FROM (SELECT st.category, st.value) t))), '[]'::json)
            FROM sample_tags st
            WHERE s.id = st.sample_id
          ) as tags,
          s.id as sample_id,
          s.uuid as sample_uuid,
          s.name as sample_name,
          s.length_in_ms as sample_length_in_ms,
          s.warp as sample_warp,
          s.bpm as sample_bpm,
          s.waveform_svg as sample_waveform_svg
        FROM sample_packs sp
          LEFT JOIN users u ON sp.author_id = u.id
          LEFT JOIN samples s ON sp.id = s.pack_id
        WHERE sp.uuid = $1
        GROUP BY sp.id, s.id, u.display_name
        ORDER BY s.name ASC;
      `,
      [req.params.uuid?.toLowerCase()]
    );

    const firstSample = packQuery.rows[0];

    const data = {
      uuid: firstSample.pack_uuid,
      name: firstSample.pack_name,
      authorName: firstSample.author_name,
      samples: packQuery.rows.map((row) => ({
        id: row.sample_id,
        uuid: row.sample_uuid,
        name: row.sample_name,
        packId: row.pack_id,
        lengthInMs: row.sample_length_in_ms,
        author: row.author_name,
        warp: row.sample_warp,
        waveformSvg: row.sample_waveform_svg,
        bpm: row.sample_bpm,
        tags: row.tags,
      })),
    };

    res.send(data);
  },
});

export const getSamplePermitted = declareHandler({
  func: async (req, res) => {
    const { uuid } = req.params;
    const lowerUuid = uuid.toLowerCase();

    const permittedQuery = await query(
      `
        SELECT
          (samples.id IS NOT NULL) AS permitted
        FROM samples
          LEFT JOIN sample_packs ON samples.pack_id = sample_packs.id
        WHERE
          (samples.uuid = $1)
          AND (
            (samples.author_id = $2)
            OR (sample_packs.published)
            OR ($3)
          )
      `,
      [lowerUuid, req.user?.id, req.user?.role === UserRole.Admin]
    );

    const newSample = !permittedQuery.rows[0];
    const existsAndPermitted = permittedQuery.rows[0]?.permitted;

    res.send({ permitted: Boolean(newSample || existsAndPermitted) });
  },
});

const BASE_FILTERS = `author_id = $1 AND pack_id IS NULL`;
const STEM_SPLIT_FRAGMENT = `AND EXISTS (SELECT FROM sample_tags WHERE sample_id = samples.id AND category = 'Stem')`;
const VOICE_CONVERSION_FRAGMENT = `AND EXISTS (SELECT FROM sample_tags WHERE sample_id = samples.id AND category = 'Type' AND value = 'Voice Conversion Output')`;
const AUDIO_GENERATION_FRAGMENT = `AND EXISTS (SELECT FROM sample_tags WHERE sample_id = samples.id AND category = 'Type' AND value = 'Generated')`;
const MANUAL_FRAGMENT = `AND id NOT IN (SELECT id FROM samples WHERE ${BASE_FILTERS} ${STEM_SPLIT_FRAGMENT}) AND id NOT IN (SELECT id FROM samples WHERE ${BASE_FILTERS} ${AUDIO_GENERATION_FRAGMENT})`;

const SEARCH_FRAGMENT = `AND (name ILIKE '%' || $2 || '%')`;

export const getMySamples = declareHandler({
  func: async (req, res) => {
    const limit = parseInt(req.query.limit) || 100;
    const offset = parseInt(req.query.offset) || 0;
    const searchTerm = (req.query.search || '').trim();

    const createdBy = req.query.createdBy || '';
    let createdByFragment = '';
    if (createdBy === 'stemSplit') {
      createdByFragment = STEM_SPLIT_FRAGMENT;
    } else if (createdBy === 'voiceConversion') {
      createdByFragment = VOICE_CONVERSION_FRAGMENT;
    } else if (createdBy === 'audioGeneration') {
      createdByFragment = AUDIO_GENERATION_FRAGMENT;
    } else if (createdBy === 'manual') {
      createdByFragment = MANUAL_FRAGMENT;
    }

    const results = await query(
      `
        SELECT
          id,
          uuid,
          name,
          length_in_ms,
          warp,
          bpm,
          waveform_svg,
          created_at
        FROM samples
        WHERE ${BASE_FILTERS}
        ${createdByFragment}
        ${searchTerm ? SEARCH_FRAGMENT : ''}
        ORDER BY created_at DESC
      `,
      searchTerm ? [req.user?.id, searchTerm] : [req.user?.id]
    );

    const pagedResults = results.rows.slice(offset, offset + limit).map((r) => ({
      id: r.id,
      uuid: r.uuid,
      name: r.name,
      lengthInMs: r.length_in_ms,
      warp: r.warp,
      bpm: r.bpm,
      waveformSvg: r.waveform_svg,
      createdAt: r.created_at,
    }));

    const childIds = results.rows.map((r) => r.id);
    if (createdBy === 'stemSplit' || createdBy === 'voiceConversion') {
      const derivationType = {
        stemSplit: 'demucs',
        voiceConversion: 'voiceConversion',
      }[createdBy];
      const parentSampleResults = await query(
        `
          SELECT
            sample_derivations.sample_id AS child_id,
            samples.uuid,
            samples.name
          FROM samples
          JOIN sample_derivations ON samples.id = sample_derivations.derived_from_id
          WHERE sample_derivations.derivation_type = $1 AND sample_derivations.sample_id = ANY($2)
        `,
        [derivationType, childIds]
      );
      pagedResults.forEach((childSample) => {
        const parentSample = parentSampleResults.rows.find((r) => r.child_id === childSample.id);
        if (parentSample) {
          childSample.parentSample = {
            uuid: parentSample.uuid,
            name: parentSample.name,
          };
        }
      });
    }

    if (createdBy === 'voiceConversion') {
      const sourceModelResults = await query(
        `
        SELECT
          sample_voice_conversion_models.sample_id AS child_id,
          voice_conversion_models.name,
          voice_conversion_models.uuid
        FROM voice_conversion_models
        JOIN sample_voice_conversion_models ON voice_conversion_models.id = sample_voice_conversion_models.voice_conversion_model_id
        WHERE sample_voice_conversion_models.sample_id = ANY($1);
        `,
        [childIds]
      );
      pagedResults.forEach((childSample) => {
        const parentModel = sourceModelResults.rows.find((r) => r.child_id === childSample.id);
        if (parentModel) {
          childSample.parentVoiceConversionModel = {
            uuid: parentModel.uuid,
            name: parentModel.name,
          };
        }
      });
    }

    pagedResults.forEach((d) => {
      delete d['id'];
    });

    res.send({ data: pagedResults, hasMore: results.rows.ength > offset + limit, count: results.rows.length });
  },
});
