import { v4 as uuidv4 } from 'uuid';
import Sentry from '../external-services/sentry.js';
import query, { transaction } from '../server-utils/query.js';
import { declareHandler } from '../server-utils/routesHandler.js';
import { filterPermittedSamples } from './samples.js';
import { areProjectsEqual } from './helpers/projectNeutralizer.js';

// several users saved projects with the same uuid (derived from a welcome flow project)
// permanently divert this uuid to a user-specific uuid
// see also db/migrations/1701295062_project_same_uuid_different_users.sql
// see also db/data-modifications/project_same_uuid_different_users.sql
const SHARED_PROJECT_UUID = 'projectState-b22f188e-ecb3-4ef5-96af-a41fb18a8660';

const transformProjectId = (projectId, userId) => {
  return projectId === SHARED_PROJECT_UUID
    ? `projectState-b22f188e-ecb3-4ef5-96af-a41fb1${userId.toString(10).padStart(6, '0')}`
    : projectId;
};

const getProjectId = async (projectUuid, userId) => {
  const result = await query(
    `SELECT id FROM projects WHERE uuid = $1 AND user_id = $2 AND superseded_by IS NULL LIMIT 1`,
    [transformProjectId(projectUuid, userId), userId]
  );
  return result.rows?.[0]?.id;
};

export const cloudSaveProject = declareHandler({
  func: async (req, res) => {
    const { project, samples, remixable } = req.body;
    const { id: userId } = req.user;

    if (samples === undefined && remixable !== undefined) {
      return res.send({ error: 'Cannot mark remixable without knowing referenced samples' });
    }

    // check access to referenced samples
    const distinctSamples = [...new Set((samples as string[]).map((s) => s.toLowerCase()))];
    const permittedSamples = await filterPermittedSamples(distinctSamples, req.user);
    const unpermittedSamples = distinctSamples.filter((s) => !permittedSamples.includes(s));

    const transformedId = transformProjectId(project.id, userId);

    const updateResult = await transaction(async (query) => {
      // Check if project is trying to save to a project that is owned by another user
      const projectQueryResult = await query(
        `SELECT id, remixable, project_state FROM projects WHERE uuid = $1 AND user_id = $2 AND superseded_by IS NULL FOR UPDATE`,
        [transformedId, userId]
      );
      const lastRemixable = projectQueryResult?.rows?.[0]?.remixable ?? true;

      const lastProjectState = projectQueryResult?.rows?.[0]?.project_state;
      // Check if the last project and current project are the same
      if (lastProjectState && areProjectsEqual(project, lastProjectState)) {
        // Update existing project saved_at time
        await query(`UPDATE projects SET saved_at = NOW() WHERE id = $1`, [projectQueryResult.rows[0].id]);
        return { success: true, unpermittedSamples };
      }

      const insertQueryResult = await query(
        `INSERT INTO projects (user_id, uuid, project_state, remixable) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING RETURNING id`,
        [userId, transformedId, JSON.stringify(project), remixable ?? lastRemixable]
      );
      if (insertQueryResult.rows.length === 0) {
        Sentry.captureMessage(`User ${userId} tried to cloud save to different user project ${transformedId}`);
        return { error: 'Project with this UUID is owned by another user' };
      }
      if (unpermittedSamples.length > 0) {
        Sentry.captureMessage(`User ${userId} saved project ${transformedId} with unpermitted samples`);
      }
      await query(
        `INSERT INTO project_samples (project_id, sample_id) SELECT $1, id FROM samples WHERE uuid = ANY($2::text[])`,
        [insertQueryResult.rows[0].id, permittedSamples]
      );
      if (projectQueryResult.rows.length > 0) {
        await query(`UPDATE projects SET superseded_by = $1 WHERE id = $2`, [
          insertQueryResult.rows[0].id,
          projectQueryResult.rows[0].id,
        ]);
      }
      return { success: true, unpermittedSamples };
    });

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

export const getCloudSavedProjects = declareHandler({
  func: async (req, res) => {
    const { id: userId } = req.user;
    // Get all projects that are not deleted and not superseded
    // Join on project tags table and extract favourite and archived tags
    const result = await query(
      `
      WITH project_tags_agg AS (
          SELECT
              p.uuid,
              p.project_state->>'name' AS name,
              COALESCE(bool_or(pt.tag = 'archived' AND pt.category = 'system'), false) AS is_archived,
              COALESCE(bool_or(pt.tag = 'favourite' AND pt.category = 'system'), false) AS is_favourite,
              max(p.updated_at) AS updated_at,
              max(p.created_at) AS created_at
          FROM
              projects p
          LEFT JOIN
              project_tags pt ON p.id = pt.project_id
          WHERE
              p.user_id = $1 AND
              p.is_deleted = false AND
              p.superseded_by IS NULL
          GROUP BY
              p.uuid, p.project_state->>'name'
      )
      SELECT *
      FROM project_tags_agg
      ORDER BY
          is_favourite DESC, updated_at DESC;

      `,
      [userId]
    );

    res.send({ projects: result.rows });
  },
});

export const getCloudSavedProjectByUuid = declareHandler({
  func: async (req, res) => {
    const { id: userId } = req.user;
    const { uuid: projectId } = req.params;
    let result = await query(
      `SELECT uuid, project_state, remixable, user_id FROM projects WHERE (user_id = $1 OR remixable) AND is_deleted = FALSE AND superseded_by IS NULL AND uuid = ANY($2::text[]) LIMIT 1;`,
      [userId, [projectId, transformProjectId(projectId, userId)]]
    );
    const isAuthor = result.rows?.[0]?.user_id === userId;
    res.send({ ...result.rows?.[0], isAuthor } || { project_state: null, uuid: null });
  },
});

export const getCloudSavedProjectHistory = declareHandler({
  func: async (req, res) => {
    const { id: userId } = req.user;
    const { uuid: projectId } = req.params;
    const result = await query(
      `
      SELECT
          id as version,
          uuid,
          superseded_by IS NULL as is_current,
          COALESCE(saved_at, created_at) as saved_at
        FROM projects
        WHERE user_id = $1 AND is_deleted = FALSE AND uuid = ANY($2::text[])
        ORDER BY created_at DESC;
      `,
      [userId, [projectId, transformProjectId(projectId, userId)]]
    );
    res.send({ projects: result.rows });
  },
});

export const getCloudSavedProjectVersion = declareHandler({
  func: async (req, res) => {
    const { id: userId } = req.user;
    const { uuid: projectId, version } = req.params;

    const result = await query(
      `
      SELECT project_state
      FROM projects
      WHERE user_id = $1 AND is_deleted = FALSE AND uuid = ANY($2::text[]) AND id = $3::int
      LIMIT 1;
      `,
      [userId, [projectId, transformProjectId(projectId, userId)], version]
    );

    res.send({ project_state: result.rows?.[0]?.project_state || null });
  },
});
// This assumes the function calling it has already checked
// that the project exists and the user has rights to the project
const addTagToProject = async (projectId, tag, category = 'system') => {
  // Check if project already has archived tag
  const projectQueryResult = await query(
    `
      SELECT id FROM project_tags
      WHERE project_id = $1 AND tag = $2 AND category = $3
    `,
    [projectId, tag, category]
  );
  if (projectQueryResult.rows.length > 0) {
    // Project already has archived tags
  } else {
    await query(
      `
      INSERT INTO project_tags (project_id, tag, category)
      VALUES ($1, $2, $3)
      `,
      [projectId, tag, category]
    );
  }
};

const removeTagFromProject = async (projectId, tag, category = 'system') => {
  const result = await query(
    `
    DELETE FROM project_tags
    WHERE project_id = $1 AND tag = $2 AND category = $3
    `,
    [projectId, tag, category]
  );
  return result.rowCount > 0;
};

export const archiveCloudSave = declareHandler({
  func: async (req, res) => {
    const { uuid: projectUuid } = req.params;
    const { id: userId } = req.user;
    const transformedId = transformProjectId(projectUuid, userId);
    // Check if project exists
    const projectId = await getProjectId(transformedId, userId);
    if (!projectId) {
      return res.send({ success: false, error: 'Invalid Project ID' });
    }
    await addTagToProject(projectId, 'archived');
    res.send({ success: true });
  },
});

export const unarchiveCloudSave = declareHandler({
  func: async (req, res) => {
    const { uuid: projectUuid } = req.params;
    const { id: userId } = req.user;
    const transformedId = transformProjectId(projectUuid, userId);
    // Check if project exists
    const projectId = await getProjectId(transformedId, userId);
    if (!projectId) {
      return res.send({ success: false, error: 'Invalid Project ID' });
    }
    const result = await removeTagFromProject(projectId, 'archived');
    result ? res.send({ success: true }) : res.send({ success: false });
  },
});

export const favouriteCloudSave = declareHandler({
  func: async (req, res) => {
    const { uuid: projectUuid } = req.params;
    const { id: userId } = req.user;
    const transformedId = transformProjectId(projectUuid, userId);
    // Check if project exists
    const projectId = await getProjectId(transformedId, userId);
    if (!projectId) {
      return res.send({ success: false, error: 'Invalid Project ID' });
    }
    await addTagToProject(projectId, 'favourite');
    res.send({ success: true });
  },
});

export const unfavouriteCloudSave = declareHandler({
  func: async (req, res) => {
    const { uuid: projectUuid } = req.params;
    const { id: userId } = req.user;
    const transformedId = transformProjectId(projectUuid, userId);
    // Check if project exists
    const projectId = await getProjectId(transformedId, userId);
    if (!projectId) {
      return res.send({ success: false, error: 'Invalid Project ID' });
    }
    const result = await removeTagFromProject(projectId, 'favourite');
    result ? res.send({ success: true }) : res.send({ success: false });
  },
});

export const deleteCoudSave = declareHandler({
  func: async (req, res) => {
    const { uuid: projectId } = req.params;
    if (!projectId) return res.send({ success: false, error: 'No project id provided' });
    await query(`UPDATE projects SET is_deleted = true WHERE user_id = $1 AND uuid = $2`, [
      req.user.id,
      transformProjectId(projectId, req.user.id),
    ]);
    res.send({ success: true });
  },
});

export const restoreCloudSave = declareHandler({
  func: async (req, res) => {
    const { uuid: projectId } = req.params;
    if (!projectId) return res.send({ success: false, error: 'No project id provided' });
    await query(`UPDATE projects SET is_deleted = FALSE WHERE user_id = $1 AND uuid = $1`, [
      req.user.id,
      transformProjectId(projectId, req.user.id),
    ]);
    res.send({ success: true });
  },
});

export const remixProject = declareHandler({
  func: async (req, res) => {
    const { id: projectId } = req.body;
    if (!projectId) return res.send({ success: false, error: 'No project id provided' });
    const remixableProjectResult = await query(
      `
      SELECT id, project_state FROM projects WHERE uuid = $1 AND (remixable OR user_id = $2) AND superseded_by IS NULL LIMIT 1`,
      [transformProjectId(projectId, req.user.id), req.user.id]
    );
    if (remixableProjectResult.rows.length === 0) {
      return res.send({ error: 'No remixable project found' });
    }
    const { id: remixableProjectId, project_state: remixableProjectState } = remixableProjectResult.rows[0];
    const remixedProjectUuid = `projectState-${uuidv4()}`;
    const remixedProjectState = {
      ...remixableProjectState,
      id: remixedProjectUuid,
      name: `${remixableProjectState.name} (Remix)`,
    };
    await transaction(async (query) => {
      const insertQueryResult = await query(
        `INSERT INTO projects (user_id, project_state, uuid, remix_of_id) VALUES ($1, $2, $3, $4) RETURNING id`,
        [req.user.id, JSON.stringify(remixedProjectState), remixedProjectUuid, remixableProjectId]
      );
      await query(
        `INSERT INTO project_samples (project_id, sample_id) SELECT $1, sample_id FROM project_samples WHERE project_id = $2`,
        [insertQueryResult.rows[0].id, remixableProjectId]
      );
    });
    res.send({ project_state: remixedProjectState, uuid: remixedProjectUuid });
  },
});

export const setProjectRemixable = declareHandler({
  func: async (req, res) => {
    const { uuid: projectId } = req.params;
    const { remixable } = req.body;
    if (!projectId) return res.send({ success: false, error: 'No project id provided' });
    const updateResult = await query(
      `
      UPDATE projects SET remixable = $1 WHERE uuid = $2 AND user_id = $3 AND superseded_by IS NULL RETURNING remixable`,
      [Boolean(remixable), transformProjectId(projectId, req.user.id), req.user.id]
    );
    if (updateResult.rows.length === 0) {
      return res.send({ success: false, error: 'No project found' });
    } else {
      const updatedRemixable = updateResult.rows?.[0]?.remixable;
      return res.send({ success: true, remixable: updatedRemixable });
    }
  },
});

export const getProjectRemixable = declareHandler({
  func: async (req, res) => {
    const { uuid: projectId } = req.params;
    if (!projectId) return res.send({ success: false, error: 'No project id provided' });
    const queryResult = await query(
      `
      SELECT remixable FROM projects WHERE uuid = $1 AND user_id = $2 AND superseded_by IS NULL LIMIT 1`,
      [transformProjectId(projectId, req.user.id), req.user.id]
    );
    if (queryResult.rows.length === 0) {
      return res.send({ success: false, error: 'No project found' });
    } else {
      const resultRemixable = queryResult.rows?.[0]?.remixable;
      return res.send({ success: true, remixable: resultRemixable });
    }
  },
});

export const getRemixableProjectDetails = declareHandler({
  func: async (req, res) => {
    const { projectId } = req.params;
    if (!projectId) return res.send({ success: false, error: 'No project id provided' });

    const queryResult = await query(
      `
      SELECT uuid, project_state->>'name' AS name, user_id, users.display_name AS author
      FROM projects LEFT JOIN users ON projects.user_id = users.id
      WHERE uuid = $1 AND superseded_by IS NULL AND remixable = TRUE
      LIMIT 1
      `,
      [projectId]
    );

    // TODO: This doesn't work because when the user loads the page, it'll not have the credentials.
    const loggedInUserId = req.user?.id;
    if (queryResult.rows.length === 0) {
      return res.send({ success: false, error: 'No project found' });
    } else {
      return res.send({
        success: true,
        project: {
          uuid: queryResult.rows[0].uuid,
          name: queryResult.rows[0].name,
          author: queryResult.rows[0].author,
          isAuthor: `${queryResult.rows[0].user_id}` === `${loggedInUserId}`,
        },
      });
    }
  },
});
