'use strict';

const _ = require('lodash/fp');

const RESERVED_TABLE_NAMES = ['strapi_migrations', 'strapi_database_schema'];

const statuses = {
  CHANGED: 'CHANGED',
  UNCHANGED: 'UNCHANGED',
};

// NOTE:We could move the schema to use maps of tables & columns instead of arrays to make it easier to diff
// => this will make the creation a bit more complicated (ordering, Object.values(tables | columns)) -> not a big pbl

const helpers = {
  hasTable(schema, tableName) {
    return schema.tables.findIndex(table => table.name === tableName) !== -1;
  },
  findTable(schema, tableName) {
    return schema.tables.find(table => table.name === tableName);
  },

  hasColumn(table, columnName) {
    return table.columns.findIndex(column => column.name === columnName) !== -1;
  },
  findColumn(table, columnName) {
    return table.columns.find(column => column.name === columnName);
  },

  hasIndex(table, columnName) {
    return table.indexes.findIndex(column => column.name === columnName) !== -1;
  },
  findIndex(table, columnName) {
    return table.indexes.find(column => column.name === columnName);
  },

  hasForeignKey(table, columnName) {
    return table.foreignKeys.findIndex(column => column.name === columnName) !== -1;
  },
  findForeignKey(table, columnName) {
    return table.foreignKeys.find(column => column.name === columnName);
  },
};

module.exports = db => {
  const hasChangedStatus = diff => diff.status === statuses.CHANGED;

  /**
   * Compares two indexes info
   * @param {Object} oldIndex - index info read from DB
   * @param {Object} index - newly generate index info
   */
  const diffIndexes = (oldIndex, index) => {
    const changes = [];

    if (_.difference(oldIndex.columns, index.columns).length > 0) {
      changes.push('columns');
    }

    if (_.toLower(oldIndex.type) !== _.toLower(index.type)) {
      changes.push('type');
    }

    return {
      status: changes.length > 0 ? statuses.CHANGED : statuses.UNCHANGED,
      diff: {
        name: index.name,
        object: index,
      },
    };
  };

  /**
   * Compares two foreign keys info
   * @param {Object} oldForeignKey - foreignKey info read from DB
   * @param {Object} foreignKey - newly generate foreignKey info
   */
  const diffForeignKeys = (oldForeignKey, foreignKey) => {
    const changes = [];

    if (_.difference(oldForeignKey.columns, foreignKey.columns).length > 0) {
      changes.push('columns');
    }

    if (_.difference(oldForeignKey.referencedColumns, foreignKey.referencedColumns).length > 0) {
      changes.push('referencedColumns');
    }

    if (oldForeignKey.referencedTable !== foreignKey.referencedTable) {
      changes.push('referencedTable');
    }

    if (_.isNil(oldForeignKey.onDelete) || _.toUpper(oldForeignKey.onDelete) === 'NO ACTION') {
      if (!_.isNil(foreignKey.onDelete) && _.toUpper(oldForeignKey.onDelete) !== 'NO ACTION') {
        changes.push('onDelete');
      }
    } else if (_.toUpper(oldForeignKey.onDelete) !== _.toUpper(foreignKey.onDelete)) {
      changes.push('onDelete');
    }

    if (_.isNil(oldForeignKey.onUpdate) || _.toUpper(oldForeignKey.onUpdate) === 'NO ACTION') {
      if (!_.isNil(foreignKey.onUpdate) && _.toUpper(oldForeignKey.onUpdate) !== 'NO ACTION') {
        changes.push('onUpdate');
      }
    } else if (_.toUpper(oldForeignKey.onUpdate) !== _.toUpper(foreignKey.onUpdate)) {
      changes.push('onUpdate');
    }

    return {
      status: changes.length > 0 ? statuses.CHANGED : statuses.UNCHANGED,
      diff: {
        name: foreignKey.name,
        object: foreignKey,
      },
    };
  };

  const diffDefault = (oldColumn, column) => {
    const oldDefaultTo = oldColumn.defaultTo;
    const defaultTo = column.defaultTo;

    if (oldDefaultTo === null || _.toLower(oldDefaultTo) === 'null') {
      return _.isNil(defaultTo) || _.toLower(defaultTo) === 'null';
    }

    return (
      _.toLower(oldDefaultTo) === _.toLower(column.defaultTo) ||
      _.toLower(oldDefaultTo) === _.toLower(`'${column.defaultTo}'`)
    );
  };

  /**
   * Compares two columns info
   * @param {Object} oldColumn - column info read from DB
   * @param {Object} column - newly generate column info
   */
  const diffColumns = (oldColumn, column) => {
    const changes = [];

    const isIgnoredType = ['increments'].includes(column.type);
    const oldType = oldColumn.type;
    const type = db.dialect.getSqlType(column.type);

    if (oldType !== type && !isIgnoredType) {
      changes.push('type');
    }

    // NOTE: compare args at some point and split them into specific properties instead

    if (oldColumn.notNullable !== column.notNullable) {
      changes.push('notNullable');
    }

    const hasSameDefault = diffDefault(oldColumn, column);
    if (!hasSameDefault) {
      changes.push('defaultTo');
    }

    if (oldColumn.unsigned !== column.unsigned && db.dialect.supportsUnsigned()) {
      changes.push('unsigned');
    }

    return {
      status: changes.length > 0 ? statuses.CHANGED : statuses.UNCHANGED,
      diff: {
        name: column.name,
        object: column,
      },
    };
  };

  const diffTableColumns = (srcTable, destTable) => {
    const addedColumns = [];
    const updatedColumns = [];
    const unchangedColumns = [];
    const removedColumns = [];

    for (const destColumn of destTable.columns) {
      if (!helpers.hasColumn(srcTable, destColumn.name)) {
        addedColumns.push(destColumn);
        continue;
      }

      const srcColumn = helpers.findColumn(srcTable, destColumn.name);
      const { status, diff } = diffColumns(srcColumn, destColumn);

      if (status === statuses.CHANGED) {
        updatedColumns.push(diff);
      } else {
        unchangedColumns.push(srcColumn);
      }
    }

    for (const srcColumn of srcTable.columns) {
      if (!helpers.hasColumn(destTable, srcColumn.name)) {
        removedColumns.push(srcColumn);
      }
    }

    const hasChanged = [addedColumns, updatedColumns, removedColumns].some(arr => arr.length > 0);

    return {
      status: hasChanged ? statuses.CHANGED : statuses.UNCHANGED,
      diff: {
        added: addedColumns,
        updated: updatedColumns,
        unchanged: unchangedColumns,
        removed: removedColumns,
      },
    };
  };

  const diffTableIndexes = (srcTable, destTable) => {
    const addedIndexes = [];
    const updatedIndexes = [];
    const unchangedIndexes = [];
    const removedIndexes = [];

    for (const destIndex of destTable.indexes) {
      if (helpers.hasIndex(srcTable, destIndex.name)) {
        const srcIndex = helpers.findIndex(srcTable, destIndex.name);
        const { status, diff } = diffIndexes(srcIndex, destIndex);

        if (status === statuses.CHANGED) {
          updatedIndexes.push(diff);
        } else {
          unchangedIndexes.push(srcIndex);
        }
      } else {
        addedIndexes.push(destIndex);
      }
    }

    for (const srcIndex of srcTable.indexes) {
      if (!helpers.hasIndex(destTable, srcIndex.name)) {
        removedIndexes.push(srcIndex);
      }
    }

    const hasChanged = [addedIndexes, updatedIndexes, removedIndexes].some(arr => arr.length > 0);

    return {
      status: hasChanged ? statuses.CHANGED : statuses.UNCHANGED,
      diff: {
        added: addedIndexes,
        updated: updatedIndexes,
        unchanged: unchangedIndexes,
        removed: removedIndexes,
      },
    };
  };

  const diffTableForeignKeys = (srcTable, destTable) => {
    const addedForeignKeys = [];
    const updatedForeignKeys = [];
    const unchangedForeignKeys = [];
    const removedForeignKeys = [];

    if (!db.dialect.usesForeignKeys()) {
      return {
        status: statuses.UNCHANGED,
        diff: {
          added: addedForeignKeys,
          updated: updatedForeignKeys,
          unchanged: unchangedForeignKeys,
          removed: removedForeignKeys,
        },
      };
    }

    for (const destForeignKey of destTable.foreignKeys) {
      if (helpers.hasForeignKey(srcTable, destForeignKey.name)) {
        const srcForeignKey = helpers.findForeignKey(srcTable, destForeignKey.name);
        const { status, diff } = diffForeignKeys(srcForeignKey, destForeignKey);

        if (status === statuses.CHANGED) {
          updatedForeignKeys.push(diff);
        } else {
          unchangedForeignKeys.push(srcForeignKey);
        }
      } else {
        addedForeignKeys.push(destForeignKey);
      }
    }

    for (const srcForeignKey of srcTable.foreignKeys) {
      if (!helpers.hasForeignKey(destTable, srcForeignKey.name)) {
        removedForeignKeys.push(srcForeignKey);
      }
    }

    const hasChanged = [addedForeignKeys, updatedForeignKeys, removedForeignKeys].some(
      arr => arr.length > 0
    );

    return {
      status: hasChanged ? statuses.CHANGED : statuses.UNCHANGED,
      diff: {
        added: addedForeignKeys,
        updated: updatedForeignKeys,
        unchanged: unchangedForeignKeys,
        removed: removedForeignKeys,
      },
    };
  };

  const diffTables = (srcTable, destTable) => {
    const columnsDiff = diffTableColumns(srcTable, destTable);
    const indexesDiff = diffTableIndexes(srcTable, destTable);
    const foreignKeysDiff = diffTableForeignKeys(srcTable, destTable);

    const hasChanged = [columnsDiff, indexesDiff, foreignKeysDiff].some(hasChangedStatus);

    return {
      status: hasChanged ? statuses.CHANGED : statuses.UNCHANGED,
      diff: {
        name: srcTable.name,
        indexes: indexesDiff.diff,
        foreignKeys: foreignKeysDiff.diff,
        columns: columnsDiff.diff,
      },
    };
  };

  const diffSchemas = (srcSchema, destSchema) => {
    const addedTables = [];
    const updatedTables = [];
    const unchangedTables = [];
    const removedTables = [];

    for (const destTable of destSchema.tables) {
      if (helpers.hasTable(srcSchema, destTable.name)) {
        const srcTable = helpers.findTable(srcSchema, destTable.name);

        const { status, diff } = diffTables(srcTable, destTable);

        if (status === statuses.CHANGED) {
          updatedTables.push(diff);
        } else {
          unchangedTables.push(srcTable);
        }
      } else {
        addedTables.push(destTable);
      }
    }

    for (const srcTable of srcSchema.tables) {
      if (
        !helpers.hasTable(destSchema, srcTable.name) &&
        !RESERVED_TABLE_NAMES.includes(srcTable.name)
      ) {
        removedTables.push(srcTable);
      }
    }

    const hasChanged = [addedTables, updatedTables, removedTables].some(arr => arr.length > 0);

    return {
      status: hasChanged ? statuses.CHANGED : statuses.UNCHANGED,
      diff: {
        tables: {
          added: addedTables,
          updated: updatedTables,
          unchanged: unchangedTables,
          removed: removedTables,
        },
      },
    };
  };

  return {
    diff: diffSchemas,
  };
};
