const db = require('../db/models');
const RolesDBApi = require('../db/api/roles');
const processFile = require('../middlewares/upload');
const ValidationError = require('./notifications/errors/validation');
const csv = require('csv-parser');
const axios = require('axios');
const config = require('../config');
const stream = require('stream');

const WIDGET_SQL_MAX_LENGTH = 5000;
const WIDGET_SQL_MAX_ROWS = 1000;
const WIDGET_SQL_TIMEOUT_MS = 5000;

const validateWidgetSql = (sql) => {
  if (typeof sql !== 'string' || !sql.trim()) {
    throw new ValidationError('Widget query must be a non-empty SQL string');
  }

  if (sql.length > WIDGET_SQL_MAX_LENGTH) {
    throw new ValidationError(
      `Widget query is too long (max ${WIDGET_SQL_MAX_LENGTH} characters)`,
    );
  }

  const normalized = sql.trim().replace(/;+\s*$/, '');

  if (!/^(select|with)\b/i.test(normalized)) {
    throw new ValidationError('Widget query must be a SELECT statement');
  }

  if (normalized.includes(';')) {
    throw new ValidationError('Widget query must contain a single statement');
  }

  if (/--|\/\*/.test(normalized)) {
    throw new ValidationError('SQL comments are not allowed in widget queries');
  }

  if (/\b(pg_sleep|set_config|copy)\b/i.test(normalized)) {
    throw new ValidationError(
      'Restricted SQL function detected in widget query',
    );
  }

  return normalized;
};

const runSafeWidgetQuery = async (sql) => {
  const normalized = validateWidgetSql(sql);
  const wrappedSql = `SELECT * FROM (${normalized}) AS widget_query_result LIMIT ${WIDGET_SQL_MAX_ROWS}`;

  return db.sequelize.transaction(async (transaction) => {
    await db.sequelize.query(
      `SET LOCAL statement_timeout = ${WIDGET_SQL_TIMEOUT_MS}`,
      { transaction },
    );
    return db.sequelize.query(wrappedSql, { transaction });
  });
};

module.exports = class RolesService {
  static async create(data, currentUser) {
    const transaction = await db.sequelize.transaction();
    try {
      const createdRole = await RolesDBApi.create(data, {
        currentUser,
        transaction,
      });

      await transaction.commit();
      return createdRole;
    } catch (error) {
      await transaction.rollback();
      throw error;
    }
  }

  static async bulkImport(req, res) {
    const transaction = await db.sequelize.transaction();

    try {
      await processFile(req, res);
      const bufferStream = new stream.PassThrough();
      const results = [];

      await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream

      await new Promise((resolve, reject) => {
        bufferStream
          .pipe(csv())
          .on('data', (data) => results.push(data))
          .on('end', async () => {
            console.log('CSV results', results);
            resolve();
          })
          .on('error', (error) => reject(error));
      });

      await RolesDBApi.bulkImport(results, {
        transaction,
        ignoreDuplicates: true,
        validate: true,
        currentUser: req.currentUser,
      });

      await transaction.commit();
    } catch (error) {
      await transaction.rollback();
      throw error;
    }
  }

  static async update(data, id, currentUser) {
    const transaction = await db.sequelize.transaction();
    try {
      let roles = await RolesDBApi.findBy({ id }, { transaction });

      if (!roles) {
        throw new ValidationError('rolesNotFound');
      }

      const updatedRoles = await RolesDBApi.update(id, data, {
        currentUser,
        transaction,
      });

      await transaction.commit();
      return updatedRoles;
    } catch (error) {
      await transaction.rollback();
      throw error;
    }
  }

  static async deleteByIds(ids, currentUser) {
    const transaction = await db.sequelize.transaction();

    try {
      await RolesDBApi.deleteByIds(ids, {
        currentUser,
        transaction,
      });

      await transaction.commit();
    } catch (error) {
      await transaction.rollback();
      throw error;
    }
  }

  static async remove(id, currentUser) {
    const transaction = await db.sequelize.transaction();

    try {
      await RolesDBApi.remove(id, {
        currentUser,
        transaction,
      });

      await transaction.commit();
    } catch (error) {
      await transaction.rollback();
      throw error;
    }
  }

  static async addRoleInfo(roleId, userId, key, widgetId, currentUser) {
    const regexExpForUuid =
      /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi;
    const widgetIdIsUUID = regexExpForUuid.test(widgetId);

    const transaction = await db.sequelize.transaction();
    let role;
    if (roleId) {
      role = await RolesDBApi.findBy({ id: roleId }, { transaction });
    } else {
      role = await RolesDBApi.findBy({ name: 'User' }, { transaction });
    }

    if (!role) {
      throw new ValidationError('rolesNotFound');
    }

    try {
      let customization = {};
      try {
        customization = JSON.parse(role.role_customization || '{}');
      } catch (e) {
        console.log(e);
      }

      if (widgetIdIsUUID && Array.isArray(customization[key])) {
        const el = customization[key].find((e) => e === widgetId);
        !el ? customization[key].unshift(widgetId) : null;
      }

      if (widgetIdIsUUID && !customization[key]) {
        customization[key] = [widgetId];
      }

      const newRole = await RolesDBApi.update(
        role.id,
        {
          role_customization: JSON.stringify(customization),
          name: role.name,
          permissions: role.permissions,
        },
        {
          currentUser,
          transaction,
        },
      );

      await transaction.commit();

      return newRole;
    } catch (error) {
      await transaction.rollback();
      throw error;
    }
  }

  static async removeRoleInfoById(infoId, roleId, key, currentUser) {
    const transaction = await db.sequelize.transaction();

    let role;
    if (roleId) {
      role = await RolesDBApi.findBy({ id: roleId }, { transaction });
    } else {
      role = await RolesDBApi.findBy({ name: 'User' }, { transaction });
    }
    if (!role) {
      await transaction.rollback();
      throw new ValidationError('rolesNotFound');
    }

    let customization = {};
    try {
      customization = JSON.parse(role.role_customization || '{}');
    } catch (e) {
      console.log(e);
    }

    customization[key] = customization[key].filter((item) => item !== infoId);

    await axios.delete(
      `${config.flHost}/${config.project_uuid}/project_customization_widgets/${infoId}.json`,
    );
    try {
      const result = await RolesDBApi.update(
        role.id,
        {
          role_customization: JSON.stringify(customization),
          name: role.name,
          permissions: role.permissions,
        },
        {
          currentUser,
          transaction,
        },
      );

      await transaction.commit();
      return result;
    } catch (error) {
      await transaction.rollback();
      throw error;
    }
  }

  static async getRoleInfoByKey(key, roleId) {
    const transaction = await db.sequelize.transaction();

    let role;
    try {
      if (roleId) {
        role = await RolesDBApi.findBy({ id: roleId }, { transaction });
      } else {
        role = await RolesDBApi.findBy({ name: 'User' }, { transaction });
      }

      await transaction.commit();
    } catch (error) {
      await transaction.rollback();
      throw error;
    }

    if (!role) {
      throw new ValidationError('Role not found');
    }

    let customization = '{}';

    try {
      customization = JSON.parse(role.role_customization || '{}');
    } catch (e) {
      console.error('Failed to parse role customization JSON:', e);
      throw e;
    }

    if (key === 'widgets') {
      const widgets = customization[key] || [];
      const widgetArray = widgets.map((widget) => {
        return axios.get(
          `${config.flHost}/${config.project_uuid}/project_customization_widgets/${widget}.json`,
        );
      });
      const widgetResults = await Promise.allSettled(widgetArray);

      const fulfilledWidgets = widgetResults
        .filter((result) => result.status === 'fulfilled')
        .map((result) => result.value.data);

      const widgetsResults = [];

      if (Array.isArray(fulfilledWidgets)) {
        for (const widget of fulfilledWidgets) {
          const result = await runSafeWidgetQuery(widget.query);

          if (result[0] && result[0].length) {
            const key = Object.keys(result[0][0])[0];
            const value =
              widget.widget_type === 'scalar' ? result[0][0][key] : result[0];
            const widgetData = JSON.parse(widget.data);
            widgetsResults.push({ ...widget, ...widgetData, value });
          } else {
            widgetsResults.push({ ...widget, value: null });
          }
        }
      }
      return widgetsResults;
    }
    return customization[key];
  }
};
