'use strict';

const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require('../db.config')[env];
const { getRequestContext } = require('../../requestContext');
const { normalizeSubdomain, validateSubdomainOrThrow } = require('../../tenantSubdomain');
const db = {};

let sequelize;
console.log(env);
if (config.use_env_variable) {
  sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
  sequelize = new Sequelize(config.database, config.username, config.password, config);
}

const Op = Sequelize.Op;

function addWhereCondition(options, condition) {
  if (!condition) {
    return;
  }

  if (!options.where) {
    options.where = condition;
    return;
  }

  options.where = {
    [Op.and]: [options.where, condition],
  };
}

function getOrganizationScopeCondition(model, context) {
  if (!context.enforceOrganizationScope || context.globalAccess) {
    return null;
  }

  if (model?.name === 'organizations') {
    if (!context.currentOrg) {
      return Sequelize.literal('1 = 0');
    }

    return {
      id: context.currentOrg,
    };
  }

  if (!model?.rawAttributes?.organizationsId) {
    return null;
  }

  if (!context.currentOrg) {
    return Sequelize.literal('1 = 0');
  }

  return {
    organizationsId: context.currentOrg,
  };
}

function applyOrganizationScope(model, options = {}) {
  const context = getRequestContext();
  const condition = getOrganizationScopeCondition(model, context);

  if (!condition) {
    return options;
  }

  addWhereCondition(options, condition);
  return options;
}

function enforceOrganizationWriteScope(model, instance) {
  const context = getRequestContext();

  if (!context.enforceOrganizationScope || context.globalAccess) {
    return;
  }

  if (model?.rawAttributes?.organizationsId) {
    if (!context.currentOrg) {
      const error = new Error('Authenticated requests must be scoped to an organization.');
      error.code = 403;
      throw error;
    }

    instance.set('organizationsId', context.currentOrg);
  }
}

sequelize.afterPoolAcquire(async (connection) => {
  const context = getRequestContext();
  const currentOrg = context.currentOrg || '';
  const globalAccess = context.globalAccess ? 'true' : 'false';

  await connection.query(
    'select set_config($1, $2, false), set_config($3, $4, false)',
    ['app.current_org', currentOrg, 'app.global_access', globalAccess],
  );
});

fs
  .readdirSync(__dirname)
  .filter((file) => {
    return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js';
  })
  .forEach((file) => {
    const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
    db[model.name] = model;
  });

Object.keys(db).forEach((modelName) => {
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});

Object.values(db).forEach((model) => {
  if (!model?.addHook) {
    return;
  }

  model.addHook('beforeFind', (options) => applyOrganizationScope(model, options));
  model.addHook('beforeCount', (options) => applyOrganizationScope(model, options));
  model.addHook('beforeCreate', (instance) => enforceOrganizationWriteScope(model, instance));
  model.addHook('beforeUpdate', (instance) => enforceOrganizationWriteScope(model, instance));
  model.addHook('beforeBulkCreate', (instances) => {
    instances.forEach((instance) => enforceOrganizationWriteScope(model, instance));
  });
});

if (db.tenants?.addHook) {
  db.tenants.addHook('beforeValidate', (tenant) => {
    if (tenant.subdomain === undefined || tenant.subdomain === null) {
      return;
    }

    const normalized = normalizeSubdomain(tenant.subdomain);

    if (!normalized) {
      tenant.set('subdomain', normalized);
      return;
    }

    tenant.set('subdomain', validateSubdomainOrThrow(normalized));
  });
}

db.sequelize = sequelize;
db.Sequelize = Sequelize;

module.exports = db;
