/**
 * This file is part of the NocoBase (R) project.
 * Copyright (c) 2020-2024 NocoBase Co., Ltd.
 * Authors: NocoBase Team.
 *
 * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
 * For more information, please refer to: https://www.nocobase.com/agreement.
 */

import { InheritedCollection } from './inherited-collection';
import lodash from 'lodash';

export class InheritedSyncRunner {
  static async syncInheritModel(model: any, options: any) {
    const { transaction } = options;
    options.hooks = options.hooks === undefined ? true : !!options.hooks;

    const inheritedCollection = model.collection as InheritedCollection;
    const db = inheritedCollection.context.database;

    const dialect = db.sequelize.getDialect();

    const queryInterface = db.sequelize.getQueryInterface();

    if (dialect != 'postgres') {
      throw new Error('Inherit model is only supported on postgres');
    }

    const parents = inheritedCollection.parents;

    if (!parents) {
      throw new Error(
        `Inherit model ${inheritedCollection.name} can't be created without parents, parents option is ${lodash
          .castArray(inheritedCollection.options.inherits)
          .join(', ')}`,
      );
    }

    for (const parent of parents) {
      if (Object.keys(parent.model.rawAttributes).length === 0) {
        throw new Error(
          `can't inherit from collection ${parent.options.name} because it has no attributes, please define at least one attribute in parent collection`,
        );
      }
    }
    const tableName = inheritedCollection.getTableNameWithSchema();
    const attributes = model.tableAttributes;

    const childAttributes = lodash.pickBy(attributes, (value) => {
      return !value.inherit;
    });

    if (
      !(await inheritedCollection.existsInDb({
        transaction,
      }))
    ) {
      let maxSequenceVal = 0;
      let maxSequenceName;

      // find max sequence
      if (childAttributes.id && childAttributes.id.autoIncrement) {
        for (const parent of parents) {
          const sequenceNameResult = await queryInterface.sequelize.query(
            `SELECT column_default
             FROM information_schema.columns
             WHERE table_name = '${parent.model.tableName}'
               and table_schema = '${parent.collectionSchema()}'
               and "column_name" = 'id';`,
            {
              transaction,
            },
          );

          if (!sequenceNameResult[0].length) {
            continue;
          }

          const columnDefault = sequenceNameResult[0][0]['column_default'];

          if (!columnDefault) {
            throw new Error(`Can't find sequence name of parent collection ${parent.options.name}`);
          }

          const regex = new RegExp(/nextval\('(.*)'::regclass\)/);
          const match = regex.exec(columnDefault);

          const sequenceName = match[1];
          const sequenceCurrentValResult = await queryInterface.sequelize.query(
            `select last_value
             from ${sequenceName}`,
            {
              transaction,
            },
          );

          const sequenceCurrentVal = parseInt(sequenceCurrentValResult[0][0]['last_value']);

          if (sequenceCurrentVal > maxSequenceVal) {
            maxSequenceName = sequenceName;
            maxSequenceVal = sequenceCurrentVal;
          }
        }
      }

      await this.createTable(tableName, childAttributes, options, model, parents);

      // if we have max sequence, set it to child table
      if (maxSequenceName) {
        const parentsDeep = Array.from(db.inheritanceMap.getParents(inheritedCollection.name)).map((parent) =>
          db.getCollection(parent).getTableNameWithSchema(),
        );

        const sequenceTables = [...parentsDeep, tableName];

        for (const sequenceTable of sequenceTables) {
          const tableName = sequenceTable.tableName;
          const schemaName = sequenceTable.schema;

          const idColumnSql = `SELECT column_name
                               FROM information_schema.columns
                               WHERE table_name = '${tableName}'
                                 and column_name = 'id'
                                 and table_schema = '${schemaName}';
          `;

          const idColumnQuery = await queryInterface.sequelize.query(idColumnSql, {
            transaction,
          });

          if (idColumnQuery[0].length == 0) {
            continue;
          }

          await queryInterface.sequelize.query(
            `alter table ${db.utils.quoteTable(sequenceTable)}
              alter column id set default nextval('${maxSequenceName}')`,
            {
              transaction,
            },
          );
        }
      }
    }

    if (options.alter) {
      const columns = await queryInterface.describeTable(tableName, options);

      for (const attribute in childAttributes) {
        const columnName = childAttributes[attribute].field;

        if (!columns[columnName]) {
          await queryInterface.addColumn(tableName, columnName, childAttributes[columnName], options);
        }
      }
    }

    if (options.hooks) {
      await model.runHooks('afterSync', {
        ...options,
        modelName: model.name,
        transaction,
      });
    }
  }

  static async createTable(tableName, attributes, options, model, parents) {
    let sql = '';

    options = { ...options };

    if (options && options.uniqueKeys) {
      lodash.forOwn(options.uniqueKeys, (uniqueKey) => {
        if (uniqueKey.customIndex === undefined) {
          uniqueKey.customIndex = true;
        }
      });
    }

    if (model) {
      options.uniqueKeys = options.uniqueKeys || model.uniqueKeys;
    }

    const queryGenerator = model.queryGenerator;

    attributes = lodash.mapValues(attributes, (attribute) => model.sequelize.normalizeAttribute(attribute));

    attributes = queryGenerator.attributesToSQL(attributes, { table: tableName, context: 'createTable' });

    sql = `${queryGenerator.createTableQuery(tableName, attributes, options)}`.replace(
      ';',
      ` INHERITS (${parents
        .map((t) => {
          return t.getTableNameWithSchema();
        })
        .join(', ')});`,
    );

    return await model.sequelize.query(sql, options);
  }
}
