import { TrackContentType } from '../conductor/types.js';
import query, { transaction } from '../server-utils/query.js';
import { declareHandler } from '../server-utils/routesHandler.js';
import { UserRole } from '../types/serverTypes.js';
import semanticRank from '../utils/semanticSearch.js';

/**
 * This file contains route handlers for all track presets and their tags.
 *  - known as track_presets in the db
 *
 * Track presets are a way of allowing users to choose from a set of pre-defined instruments / sounds.
 * Each track preset can contain device presets as well as other settings to be applied
 *
 * Note: `presets` refer to device presets and can be found in the devices.ts file
 *   Device presets are a way of allowing users to save a set of settings for a device.
 *   A device can be a sound generator (e.g. ), or an effect (e.g. reverb, delay, etc.)
 *
 */

type PresetTag = {
  value: string;
  category: string;
};

const validateTrackPreset = (preset) => {
  const errors = [];
  if (!preset.id) {
    errors.push('Preset must have an id');
  }
  if (!preset.type) {
    errors.push('Preset must have a type');
  } else {
    if (!Object.values(TrackContentType).includes(preset.type)) {
      errors.push('Preset type must be a valid type');
    }
  }
  if (!preset.name) {
    errors.push('Preset must have a name');
  }
  if (!preset.color) {
    errors.push('Preset must have a color');
  }
  return errors;
};

const trackPresetExistsByPresetId = async (id) => {
  const preset = await query(
    `
      SELECT
        preset_data
      FROM
        track_presets
      WHERE
        preset_data->>'id' = $1
    `,
    [id]
  );
  return preset.rows.length > 0;
};

const trackPresetExistsByDbId = async (id) => {
  if (isNaN(id)) return false;
  const preset = await query(
    `
      SELECT
        id
      FROM
        track_presets
      WHERE
        id = $1
    `,
    [id]
  );
  return preset.rows.length > 0;
};

const getTrackPresetDbIdByPresetId = async (id) => {
  const preset = await query(
    `
      SELECT
        id
      FROM
        track_presets
      WHERE
        preset_data->>'id' = $1
    `,
    [id]
  );
  return preset.rows.length > 0 ? preset.rows[0].id : null;
};

const trackPresetTagExists = async (trackPresetId, value, category) => {
  const presetTag = await query(
    `
      SELECT
        id
      FROM
        track_preset_tags
      WHERE
        track_preset_id = $1
      AND
        value = $2
      AND
        category = $3
    `,
    [trackPresetId, value, category]
  );
  return presetTag.rows.length > 0;
};

export const getTrackPresets = declareHandler({
  func: async (req, res) => {
    const role = req.query.role;
    const returnPresets = [];
    try {
      let queryStr = `SELECT id, preset_data FROM track_presets WHERE published = true`;
      const values = [];

      if (role) {
        queryStr += `
        AND EXISTS (
          SELECT 1
          FROM jsonb_array_elements_text(track_presets.preset_data->'instrumentRoles') AS ir(role)
          WHERE ir.role = $1
        )`;
        values.push(role);
      }

      const presets = await query(queryStr, values);

      presets.rows.forEach((element) => {
        returnPresets.push(element.preset_data);
      });
    } catch (e) {
      res.send({ success: false, error: 'Error getting presets' });
      return;
    }

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

export const addTrackPreset = declareHandler({
  func: async (req, res) => {
    if (req.user?.role !== UserRole.Admin) return res.send({ success: false, error: 'Unauthorized' });

    let hasErrors = false;
    const errorsHash = {};
    const successfulPresets = [];
    const { presets } = req.body;
    if (!presets) return res.send({ success: false, error: 'No presets provided' });
    for (const i in presets) {
      const preset = presets[i];
      const errors = validateTrackPreset(preset);
      if (errors.length) {
        errorsHash[i] = errors;
        hasErrors = true;
      } else {
        if (await trackPresetExistsByPresetId(preset.id)) {
          errorsHash[i] = [`Preset already exists`, `ID: ${preset.id}`];
          hasErrors = true;
          continue;
        }
        successfulPresets.push(preset.id);
        // Insert preset into database
        await query(
          `
            INSERT INTO track_presets (
              preset_data
            ) VALUES (
              $1::jsonb
            )
          `,
          [JSON.stringify(preset)]
        );
      }
    }
    if (hasErrors) return res.send({ success: false, errors: errorsHash, successfulPresets });
    else res.send({ success: true, successfulPresets });
  },
});

export const addTrackPresetTags = declareHandler({
  func: async (req, res) => {
    if (req.user?.role !== UserRole.Admin) return res.send({ success: false, error: 'Unauthorized' });

    const trackPresetId = req.params.trackPresetId;
    const { tags }: { tags: PresetTag[] } = req.body;
    if (!trackPresetId || !tags) return res.send({ success: false, error: 'Missing trackPresetId or tags' });

    let presetDBId = null;
    if (await trackPresetExistsByDbId(trackPresetId)) {
      presetDBId = trackPresetId;
    }
    if (presetDBId === null) {
      presetDBId = await getTrackPresetDbIdByPresetId(trackPresetId);
    }
    if (presetDBId === null) {
      return res.send({ success: false, error: 'Invalid trackPresetId' });
    }

    // Add tags to database
    for (const t of tags) {
      const { value, category } = t;
      if (!value || !category) {
        return res.send({
          success: false,
          error: `Malformed tag object: ${JSON.stringify(t)}\nExpected 'value' and 'category' keys`,
        });
      }
    }
    const addedTags = [];
    for (const tag of tags) {
      if (!(await trackPresetTagExists(presetDBId, tag.value, tag.category))) {
        await query(
          `
          INSERT INTO track_preset_tags (
            track_preset_id,
            value,
            category
          ) VALUES (
            $1,
            $2,
            $3
          )
        `,
          [presetDBId, tag.value, tag.category]
        );
        addedTags.push(tag);
      }
    }

    res.send({ success: true, addedTags, ignoredTags: tags.filter((tag) => !addedTags.includes(tag)) });
  },
});

export const deleteTrackPresetTags = declareHandler({
  func: async (req, res) => {
    if (req.user?.role !== UserRole.Admin) return res.send({ success: false, error: 'Unauthorized' });

    const trackPresetId = req.params.trackPresetId;
    const { tags }: { tags: PresetTag[] } = req.body;
    if (!trackPresetId || !tags) return res.send({ success: false, error: 'Missing trackPresetId or tag' });

    let presetDBId = null;
    if (await trackPresetExistsByDbId(trackPresetId)) {
      presetDBId = trackPresetId;
    }
    if (presetDBId === null) {
      presetDBId = await getTrackPresetDbIdByPresetId(trackPresetId);
    }
    if (presetDBId === null) {
      return res.send({ success: false, error: 'Invalid trackPresetId' });
    }

    // Delete tags from database
    for (const t of tags) {
      const { value, category } = t;
      if (!value || !category) {
        return res.send({
          success: false,
          error: `Malformed tag object: ${JSON.stringify(t)}\nExpected 'value' and 'category' keys`,
        });
      }
    }
    const deletedTags = [];
    for (const tag of tags) {
      if (await trackPresetTagExists(presetDBId, tag.value, tag.category)) {
        await query(
          `
          DELETE FROM
            track_preset_tags
          WHERE
            track_preset_id = $1
          AND
            value = $2
          AND
            category = $3
        `,
          [presetDBId, tag.value, tag.category]
        );
        deletedTags.push(tag);
      }
    }

    res.send({ success: true, deletedTags, ignoredTags: tags.filter((tag) => !deletedTags.includes(tag)) });
  },
});

export const updateTrackPresetTags = declareHandler({
  func: async (req, res) => {
    if (req.user?.role !== UserRole.Admin) return res.send({ success: false, error: 'Unauthorized' });

    const trackPresetId = req.params.trackPresetId;
    const { tags }: { tags: PresetTag[] } = req.body;

    let presetDBId = null;
    if (await trackPresetExistsByDbId(trackPresetId)) {
      presetDBId = trackPresetId;
    }
    if (presetDBId === null) {
      presetDBId = await getTrackPresetDbIdByPresetId(trackPresetId);
    }
    if (presetDBId === null) {
      return res.send({ success: false, error: 'Invalid trackPresetId' });
    }

    await transaction(async (transactionQuery) => {
      // delete tags for this preset
      await transactionQuery(`DELETE FROM track_preset_tags WHERE track_preset_id = $1`, [presetDBId]);

      // add tags for this preset
      for (const t of tags) {
        const { value, category } = t;
        if (!value || !category) {
          return res.send({
            success: false,
            error: `Malformed tag object: ${JSON.stringify(t)}\nExpected 'value' and 'category' keys`,
          });
        }
      }
      for (const tag of tags) {
        await transactionQuery(`INSERT INTO track_preset_tags (track_preset_id, value, category) VALUES ($1, $2, $3)`, [
          presetDBId,
          tag.value,
          tag.category,
        ]);
      }

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

export const getTrackPreset = declareHandler({
  func: async (req, res) => {
    const trackPresetId = req.params.trackPresetId;
    if (!trackPresetId) return res.send({ success: false, error: 'Missing trackPresetId' });
    const presetDBId = await getTrackPresetDbIdByPresetId(trackPresetId);
    const presetQuery = await query(`SELECT * FROM track_presets WHERE id = $1;`, [presetDBId]);

    res.send({ success: true, preset: presetQuery.rows[0] });
  },
});

export const updateTrackPreset = declareHandler({
  func: async (req, res) => {
    if (req.user?.role !== UserRole.Admin) return res.send({ success: false, error: 'Unauthorized' });
    const trackPresetId = req.params.trackPresetId;
    if (!trackPresetId) return res.send({ success: false, error: 'Missing trackPresetId' });
    const { preset } = req.body;
    const presetDBId = await getTrackPresetDbIdByPresetId(trackPresetId);
    await query(`UPDATE track_presets SET preset_data = $1::jsonb WHERE id = $2;`, [
      JSON.stringify(preset),
      presetDBId,
    ]);

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

export const getTrackPresetTags = declareHandler({
  func: async (req, res) => {
    const trackPresetId = req.params.trackPresetId;
    if (!trackPresetId) return res.send({ success: false, error: 'Missing trackPresetId' });

    let presetDBId = null;
    if (await trackPresetExistsByDbId(trackPresetId)) {
      presetDBId = trackPresetId;
    }
    if (presetDBId === null) {
      presetDBId = await getTrackPresetDbIdByPresetId(trackPresetId);
    }
    if (presetDBId === null) {
      return res.send({ success: false, error: 'Invalid trackPresetId' });
    }
    const presetTagsQuery = await query(
      `
        SELECT
          value, category
        FROM
          track_preset_tags
        WHERE
          track_preset_id = $1
      `,
      [presetDBId]
    );
    const presetTags = presetTagsQuery.rows.map((row) => {
      return { value: row.value, category: row.category };
    });

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

export const getAllTrackPresetCategories = declareHandler({
  func: async (req, res) => {
    const presets = await query(
      `
        SELECT
          category
        FROM
          track_preset_tags
        GROUP BY
          category
      `,
      []
    );
    const categories = [];
    presets.rows.forEach((preset) => {
      categories.push(preset.category);
    });
    res.send({ success: true, categories });
  },
});

export const getAllTrackPresetTags = declareHandler({
  func: async (req, res) => {
    const presets = await query(
      `
        SELECT
          value
        FROM
          track_preset_tags
        GROUP BY
          value
      `,
      []
    );
    const tags = [];
    presets.rows.forEach((preset) => {
      tags.push(preset.value);
    });
    res.send({ success: true, tags });
  },
});

const SIMILARITY_THRESHOLD = 0.7;

export const findTrackPresets = declareHandler({
  func: async (req, res) => {
    try {
      const { query: searchQuery } = req.params;
      const role = req.query.role;

      const values = [];
      let queryStr = `
      SELECT
        tp.*,
        JSON_AGG(JSON_BUILD_OBJECT('category', tpt.category, 'value', tpt.value)) AS track_preset_tags
      FROM
        track_presets AS tp
      LEFT JOIN
        track_preset_tags AS tpt ON tp.id = tpt.track_preset_id
      WHERE published = true
      `;

      if (role) {
        queryStr += `
        AND EXISTS (
          SELECT 1
          FROM jsonb_array_elements_text(tp.preset_data->'instrumentRoles') AS ir(role)
          WHERE ir.role = $1
        )`;
        values.push(role);
      }

      queryStr += `GROUP BY tp.id;`;

      const allTrackPresets = await query(queryStr, values);

      const searchString = allTrackPresets.rows.map((preset) => {
        const presetId = preset.id;
        const presetData = preset.preset_data;
        const presetName = presetData.name;
        const tagData = preset.track_preset_tags;
        const tagStr = tagData.map((tag) => `${tag.category}:${tag.value}`).join(',');
        const presetDescription = presetData.description;
        return `${presetId}.${presetName}-${tagStr}${presetDescription ? `-description:${presetDescription}` : ''}`;
      });

      const bestTagStrs = (await semanticRank(searchQuery, searchString)).filter(
        (r) => r.similarity > SIMILARITY_THRESHOLD
      );

      const results = bestTagStrs.map((r) => {
        const [id, ...rest] = r.option.split('.');
        return { id, similarity: r.similarity };
      });

      const trackPresetIds = results.sort((a, b) => b.similarity - a.similarity).map((r) => r.id);

      const recommendedTrackPresets = await query(
        `
      SELECT t.*
      FROM track_presets AS t
      JOIN unnest($1::int[]) WITH ORDINALITY AS u(id, ord) USING (id)
      ORDER BY u.ord
      `,
        [trackPresetIds]
      );

      res.send({ presets: recommendedTrackPresets.rows.map((row) => row.preset_data), success: true });
    } catch (e) {
      console.log(e);
      res.send({ success: false, error: e.message });
    }
  },
});
