/**
 * Copyright (c) 2020-present, Goldman Sachs
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import type { Entity } from '@finos/legend-storage';
import type { TreeData, TreeNodeData } from '@finos/legend-art';
import {
  type GeneratorFn,
  type Writable,
  assertErrorThrown,
  LogEvent,
  addUniqueEntry,
  guaranteeNonNullable,
  isNonNullable,
  filterByType,
  ActionState,
  at,
  guaranteeType,
  assertNonEmptyString,
  assertTrue,
  UnsupportedOperationError,
} from '@finos/legend-shared';
import {
  observable,
  action,
  makeObservable,
  flow,
  flowResult,
  computed,
} from 'mobx';
import { LEGEND_STUDIO_APP_EVENT } from '../../../../../__lib__/LegendStudioEvent.js';
import type { EditorStore } from '../../../EditorStore.js';
import {
  type RawLambda,
  type PureModel,
  type Runtime,
  type ExecutionResultWithMetadata,
  TDSExecutionResult,
  getColumn,
  PrimitiveType,
  PRIMITIVE_TYPE,
  TDSRow,
  type Schema,
  Table,
  RelationalDatabaseConnection,
  DatabaseBuilderInput,
  DatabasePattern,
  TargetDatabase,
  Column,
  Database,
  resolvePackagePathAndElementName,
  getSchema,
  getNullableSchema,
  getNullableTable,
  isStubbed_PackageableElement,
  isValidFullPath,
  PackageableElementExplicitReference,
  getTable,
  Mapping,
  EngineRuntime,
  StoreConnections,
  IdentifiedConnection,
  getOrCreateGraphPackage,
  extractElementNameFromPath,
  extractPackagePathFromPath,
} from '@finos/legend-graph';
import { GraphEditFormModeState } from '../../../GraphEditFormModeState.js';
import { connection_setStore } from '../../../../graph-modifier/DSL_Mapping_GraphModifierHelper.js';
import { getTDSColumnDerivedProperyFromType } from '@finos/legend-query-builder';
import { getPrimitiveTypeFromRelationalType } from '../../../utils/MockDataUtils.js';

const GENERATED_PACKAGE = 'generated';
const TDS_LIMIT = 1000;

const buildTableToTDSQueryGrammar = (table: Table): string => {
  const tableName = table.name;
  const schemaName = table.schema.name;
  const db = table.schema._OWNER.path;
  return `|${db}->tableReference(
    '${schemaName}',
    '${tableName}'
  )->tableToTDS()->take(${TDS_LIMIT})`;
};

const buildTableToTDSQueryNonNumericWithColumnGrammar = (
  column: Column,
): string => {
  const table = guaranteeType(column.owner, Table);
  const tableName = table.name;
  const colName = column.name;
  const schemaName = table.schema.name;
  const db = table.schema._OWNER.path;
  const PREVIEW_COLUMN_NAME = 'Count Value';
  const columnGetter = getTDSColumnDerivedProperyFromType(
    getPrimitiveTypeFromRelationalType(column.type) ?? PrimitiveType.STRING,
  );
  return `|${db}->tableReference(
    '${schemaName}',
    '${tableName}'
  )->tableToTDS()->restrict(
    ['${colName}']
  )->groupBy(
    ['${colName}'],
    '${PREVIEW_COLUMN_NAME}'->agg(
      row|$row.${columnGetter}('${colName}'),
      y|$y->count()
    )
  )->sort(
    [
      desc('${colName}'),
      asc('${PREVIEW_COLUMN_NAME}')
    ]
  )->take(${TDS_LIMIT})`;
};

const buildTableToTDSQueryNumericWithColumnGrammar = (
  column: Column,
): string => {
  const table = guaranteeType(column.owner, Table);
  const tableName = table.name;
  const colName = column.name;
  const schemaName = table.schema.name;
  const db = table.schema._OWNER.path;
  const columnGetter = getTDSColumnDerivedProperyFromType(
    getPrimitiveTypeFromRelationalType(column.type) ?? PrimitiveType.STRING,
  );
  return `|${db}->tableReference(
    '${schemaName}',
    '${tableName}'
  )->tableToTDS()->restrict(
    ['${colName}']
  )->groupBy(
    [],
    [
      'Count'->agg(
      row|$row.${columnGetter}('${colName}'),
      x|$x->count()
    ),
      'Distinct Count'->agg(
      row|$row.${columnGetter}('${colName}'),
      x|$x->distinct()->count()
    ),
      'Sum'->agg(
      row|$row.${columnGetter}('${colName}'),
      x|$x->sum()
    ),
      'Min'->agg(
      row|$row.${columnGetter}('${colName}'),
      x|$x->min()
    ),
      'Max'->agg(
      row|$row.${columnGetter}('${colName}'),
      x|$x->max()
    ),
      'Average'->agg(
      row|$row.${columnGetter}('${colName}'),
      x|$x->average()
    ),
      'Std Dev (Population)'->agg(
      row|$row.${columnGetter}('${colName}'),
      x|$x->stdDevPopulation()
    ),
      'Std Dev (Sample)'->agg(
      row|$row.${columnGetter}('${colName}'),
      x|$x->stdDevSample()
    )
    ]
  )`;
};

const buildTableToTDSQueryColumnQuery = (column: Column): [string, boolean] => {
  const type =
    getPrimitiveTypeFromRelationalType(column.type) ?? PrimitiveType.STRING;
  const numerics = [
    PRIMITIVE_TYPE.NUMBER,
    PRIMITIVE_TYPE.INTEGER,
    PRIMITIVE_TYPE.DECIMAL,
    PRIMITIVE_TYPE.FLOAT,
  ];
  if (numerics.includes(type.path as PRIMITIVE_TYPE)) {
    return [buildTableToTDSQueryNumericWithColumnGrammar(column), true];
  }

  return [buildTableToTDSQueryNonNumericWithColumnGrammar(column), false];
};

// 1. mapping
// 2. connection
// 3. runtime

const buildTDSModel = (
  graph: PureModel,
  connection: RelationalDatabaseConnection,
  db: Database,
): {
  mapping: Mapping;
  runtime: Runtime;
} => {
  // mapping
  const mappingName = 'EmptyMapping';
  const _mapping = new Mapping(mappingName);
  graph.addElement(_mapping, GENERATED_PACKAGE);
  const engineRuntime = new EngineRuntime();
  engineRuntime.mappings = [
    PackageableElementExplicitReference.create(_mapping),
  ];
  const _storeConnection = new StoreConnections(
    PackageableElementExplicitReference.create(db),
  );
  // copy over new connection
  const newconnection = new RelationalDatabaseConnection(
    PackageableElementExplicitReference.create(db),
    connection.type,
    connection.datasourceSpecification,
    connection.authenticationStrategy,
  );
  newconnection.localMode = connection.localMode;
  newconnection.timeZone = connection.timeZone;
  _storeConnection.storeConnections = [
    new IdentifiedConnection('connection1', newconnection),
  ];
  engineRuntime.connections = [_storeConnection];
  return {
    runtime: engineRuntime,
    mapping: _mapping,
  };
};

export abstract class DatabaseSchemaExplorerTreeNodeData
  implements TreeNodeData
{
  isOpen?: boolean | undefined;
  id: string;
  label: string;
  parentId?: string | undefined;
  childrenIds?: string[] | undefined;
  isChecked = false;

  constructor(id: string, label: string, parentId: string | undefined) {
    makeObservable(this, {
      isChecked: observable,
      setChecked: action,
    });

    this.id = id;
    this.label = label;
    this.parentId = parentId;
  }

  setChecked(val: boolean): void {
    this.isChecked = val;
  }
}

export class DatabaseSchemaExplorerTreeSchemaNodeData extends DatabaseSchemaExplorerTreeNodeData {
  schema: Schema;

  constructor(id: string, schema: Schema) {
    super(id, schema.name, undefined);
    this.schema = schema;
  }
}

export class DatabaseSchemaExplorerTreeTableNodeData extends DatabaseSchemaExplorerTreeNodeData {
  override parentId: string;
  owner: Schema;
  table: Table;

  constructor(id: string, parentId: string, owner: Schema, table: Table) {
    super(id, table.name, parentId);
    this.parentId = parentId;
    this.owner = owner;
    this.table = table;
  }
}

export class DatabaseSchemaExplorerTreeColumnNodeData extends DatabaseSchemaExplorerTreeNodeData {
  override parentId: string;
  owner: Table;
  column: Column;

  constructor(id: string, parentId: string, owner: Table, column: Column) {
    super(id, column.name, parentId);
    this.parentId = parentId;
    this.owner = owner;
    this.column = column;
  }
}

export interface DatabaseExplorerTreeData
  extends TreeData<DatabaseSchemaExplorerTreeNodeData> {
  database: Database;
}

export const DEFAULT_DATABASE_PATH = 'store::MyDatabase';

export class DatabaseSchemaExplorerState {
  readonly editorStore: EditorStore;
  readonly connection: RelationalDatabaseConnection;
  database: Database;
  targetDatabasePath: string;
  makeTargetDatabasePathEditable?: boolean;

  isGeneratingDatabase = false;
  isUpdatingDatabase = false;
  treeData?: DatabaseExplorerTreeData | undefined;
  previewer: TDSExecutionResult | undefined;
  previewDataState = ActionState.create();

  constructor(
    editorStore: EditorStore,
    connection: RelationalDatabaseConnection,
  ) {
    makeObservable(this, {
      isGeneratingDatabase: observable,
      isUpdatingDatabase: observable,
      database: observable,
      treeData: observable,
      targetDatabasePath: observable,
      previewer: observable,
      previewDataState: observable,
      makeTargetDatabasePathEditable: observable,
      isCreatingNewDatabase: computed,
      resolveDatabasePackageAndName: computed,
      setTreeData: action,
      setTargetDatabasePath: action,
      setMakeTargetDatabasePathEditable: action,
      onNodeSelect: flow,
      fetchDatabaseMetadata: flow,
      fetchSchemaMetadata: flow,
      fetchTableMetadata: flow,
      generateDatabase: flow,
      updateDatabase: flow,
      updateDatabaseAndGraph: flow,
      previewData: flow,
    });

    this.connection = connection;
    this.database = guaranteeType(connection.store.value, Database);
    this.editorStore = editorStore;
    this.targetDatabasePath = DEFAULT_DATABASE_PATH;
  }

  get isCreatingNewDatabase(): boolean {
    return isStubbed_PackageableElement(this.connection.store.value);
  }

  setMakeTargetDatabasePathEditable(val: boolean): void {
    this.makeTargetDatabasePathEditable = val;
  }

  get resolveDatabasePackageAndName(): [string, string] {
    if (!this.isCreatingNewDatabase && !this.makeTargetDatabasePathEditable) {
      return [
        guaranteeNonNullable(this.database.package).path,
        this.database.name,
      ];
    }
    assertNonEmptyString(this.targetDatabasePath, 'Must specify database path');
    assertTrue(
      isValidFullPath(this.targetDatabasePath),
      'Invalid database path',
    );
    return resolvePackagePathAndElementName(
      this.targetDatabasePath,
      this.targetDatabasePath,
    );
  }

  setTargetDatabasePath(val: string): void {
    this.targetDatabasePath = val;
  }

  setTreeData(builderTreeData?: DatabaseExplorerTreeData): void {
    this.treeData = builderTreeData;
  }

  *onNodeSelect(
    node: DatabaseSchemaExplorerTreeNodeData,
    treeData: DatabaseExplorerTreeData,
  ): GeneratorFn<void> {
    if (
      node instanceof DatabaseSchemaExplorerTreeSchemaNodeData &&
      !node.childrenIds
    ) {
      yield flowResult(this.fetchSchemaMetadata(node, treeData));
    } else if (
      node instanceof DatabaseSchemaExplorerTreeTableNodeData &&
      !node.childrenIds
    ) {
      yield flowResult(this.fetchTableMetadata(node, treeData));
    }
    node.isOpen = !node.isOpen;
    this.setTreeData({ ...treeData });
  }

  getChildNodes(
    node: DatabaseSchemaExplorerTreeNodeData,
    treeData: DatabaseExplorerTreeData,
  ): DatabaseSchemaExplorerTreeNodeData[] | undefined {
    return node.childrenIds
      ?.map((childNode) => treeData.nodes.get(childNode))
      .filter(isNonNullable);
  }

  toggleCheckedNode(
    node: DatabaseSchemaExplorerTreeNodeData,
    treeData: DatabaseExplorerTreeData,
  ): void {
    node.setChecked(!node.isChecked);
    if (node instanceof DatabaseSchemaExplorerTreeSchemaNodeData) {
      this.getChildNodes(node, treeData)?.forEach((childNode) => {
        childNode.setChecked(node.isChecked);
      });
    } else if (node instanceof DatabaseSchemaExplorerTreeTableNodeData) {
      if (node.parentId) {
        const parent = treeData.nodes.get(node.parentId);
        if (
          parent &&
          this.getChildNodes(parent, treeData)?.every(
            (e) => e.isChecked === node.isChecked,
          )
        ) {
          parent.setChecked(node.isChecked);
        }
      }
    }

    // TODO: support toggling check for columns
    this.setTreeData({ ...treeData });
  }

  *fetchDatabaseMetadata(): GeneratorFn<void> {
    try {
      this.isGeneratingDatabase = true;
      const databaseBuilderInput = new DatabaseBuilderInput(this.connection);
      const [packagePath, name] = this.resolveDatabasePackageAndName;
      databaseBuilderInput.targetDatabase = new TargetDatabase(
        packagePath,
        name,
      );
      databaseBuilderInput.config.maxTables = undefined;
      databaseBuilderInput.config.enrichTables = false;
      databaseBuilderInput.config.patterns = [
        new DatabasePattern(undefined, undefined),
      ];
      const database = (yield this.buildIntermediateDatabase(
        databaseBuilderInput,
      )) as Database;

      const rootIds: string[] = [];
      const nodes = new Map<string, DatabaseSchemaExplorerTreeNodeData>();
      database.schemas
        .toSorted((schemaA, schemaB) =>
          schemaA.name.localeCompare(schemaB.name),
        )
        .forEach((schema) => {
          const schemaId = schema.name;
          rootIds.push(schemaId);
          const schemaNode = new DatabaseSchemaExplorerTreeSchemaNodeData(
            schemaId,
            schema,
          );
          nodes.set(schemaId, schemaNode);

          schemaNode.setChecked(
            Boolean(
              this.database.schemas.find(
                (cSchema) => cSchema.name === schema.name,
              ),
            ),
          );
        });
      const treeData = { rootIds, nodes, database };
      this.setTreeData(treeData);
    } catch (error) {
      assertErrorThrown(error);
      this.editorStore.applicationStore.logService.error(
        LogEvent.create(LEGEND_STUDIO_APP_EVENT.DATABASE_BUILDER_FAILURE),
        error,
      );
      this.editorStore.applicationStore.notificationService.notifyError(error);
    } finally {
      this.isGeneratingDatabase = false;
    }
  }

  *fetchSchemaMetadata(
    schemaNode: DatabaseSchemaExplorerTreeSchemaNodeData,
    treeData: DatabaseExplorerTreeData,
  ): GeneratorFn<void> {
    try {
      this.isGeneratingDatabase = true;

      const schema = schemaNode.schema;
      const databaseBuilderInput = new DatabaseBuilderInput(this.connection);
      const [packagePath, name] = this.resolveDatabasePackageAndName;
      databaseBuilderInput.targetDatabase = new TargetDatabase(
        packagePath,
        name,
      );
      databaseBuilderInput.config.maxTables = undefined;
      databaseBuilderInput.config.enrichTables = true;
      databaseBuilderInput.config.patterns = [
        new DatabasePattern(schema.name, undefined),
      ];
      const database = (yield this.buildIntermediateDatabase(
        databaseBuilderInput,
      )) as Database;

      const tables = getSchema(database, schema.name).tables;
      const childrenIds = schemaNode.childrenIds ?? [];
      schema.tables = tables;
      tables
        .toSorted((tableA, tableB) => tableA.name.localeCompare(tableB.name))
        .forEach((table) => {
          table.schema = schema;
          const tableId = `${schema.name}.${table.name}`;
          const tableNode = new DatabaseSchemaExplorerTreeTableNodeData(
            tableId,
            schemaNode.id,
            schema,
            table,
          );
          treeData.nodes.set(tableId, tableNode);
          addUniqueEntry(childrenIds, tableId);

          const matchingSchema = getNullableSchema(this.database, schema.name);
          tableNode.setChecked(
            Boolean(
              matchingSchema
                ? getNullableTable(matchingSchema, table.name)
                : undefined,
            ),
          );
        });
      schemaNode.childrenIds = childrenIds;
      this.setTreeData({ ...treeData });
    } catch (error) {
      assertErrorThrown(error);
      this.editorStore.applicationStore.logService.error(
        LogEvent.create(LEGEND_STUDIO_APP_EVENT.DATABASE_BUILDER_FAILURE),
        error,
      );
      this.editorStore.applicationStore.notificationService.notifyError(error);
    } finally {
      this.isGeneratingDatabase = false;
    }
  }

  *fetchTableMetadata(
    tableNode: DatabaseSchemaExplorerTreeTableNodeData,
    treeData: DatabaseExplorerTreeData,
  ): GeneratorFn<void> {
    try {
      this.isGeneratingDatabase = true;

      const databaseBuilderInput = new DatabaseBuilderInput(this.connection);
      const [packagePath, name] = this.resolveDatabasePackageAndName;
      databaseBuilderInput.targetDatabase = new TargetDatabase(
        packagePath,
        name,
      );
      const table = tableNode.table;
      const config = databaseBuilderInput.config;
      config.maxTables = undefined;
      config.enrichTables = true;
      config.enrichColumns = true;
      config.enrichPrimaryKeys = true;
      config.patterns = [new DatabasePattern(table.schema.name, table.name)];
      const database = (yield this.buildIntermediateDatabase(
        databaseBuilderInput,
      )) as Database;

      const enrichedTable = database.schemas
        .find((s) => table.schema.name === s.name)
        ?.tables.find((t) => t.name === table.name);
      if (enrichedTable) {
        table.primaryKey = enrichedTable.primaryKey;
        const columns = enrichedTable.columns.filter(filterByType(Column));
        tableNode.table.columns = columns;
        tableNode.childrenIds?.forEach((childId) =>
          treeData.nodes.delete(childId),
        );
        tableNode.childrenIds = undefined;
        const childrenIds: string[] = [];
        const tableId = tableNode.id;
        columns
          .toSorted((colA, colB) => colA.name.localeCompare(colB.name))
          .forEach((column) => {
            const columnId = `${tableId}.${column.name}`;
            const columnNode = new DatabaseSchemaExplorerTreeColumnNodeData(
              columnId,
              tableId,
              table,
              column,
            );
            column.owner = tableNode.table;
            treeData.nodes.set(columnId, columnNode);
            addUniqueEntry(childrenIds, columnId);
          });
        tableNode.childrenIds = childrenIds;
      }
    } catch (error) {
      assertErrorThrown(error);
      this.editorStore.applicationStore.logService.error(
        LogEvent.create(LEGEND_STUDIO_APP_EVENT.DATABASE_BUILDER_FAILURE),
        error,
      );
      this.editorStore.applicationStore.notificationService.notifyError(error);
    } finally {
      this.isGeneratingDatabase = false;
    }
  }

  private async buildIntermediateDatabase(
    databaseBuilderInput: DatabaseBuilderInput,
  ): Promise<Database> {
    const entities =
      await this.editorStore.graphManagerState.graphManager.buildDatabase(
        databaseBuilderInput,
      );
    const graph = this.editorStore.graphManagerState.createNewGraph();
    await this.editorStore.graphManagerState.graphManager.buildGraph(
      graph,
      entities,
      ActionState.create(),
    );
    return at(
      graph.ownDatabases,
      0,
      'Expected one database to be generated from input',
    );
  }

  *previewData(node: DatabaseSchemaExplorerTreeNodeData): GeneratorFn<void> {
    try {
      this.previewer = undefined;
      this.previewDataState.inProgress();
      let column: Column | undefined;
      let table: Table | undefined;
      if (node instanceof DatabaseSchemaExplorerTreeTableNodeData) {
        table = node.table;
      } else if (node instanceof DatabaseSchemaExplorerTreeColumnNodeData) {
        table = guaranteeType(node.column.owner, Table);
        column = node.column;
      } else {
        throw new UnsupportedOperationError(
          'Preview data only supported for column and table',
        );
      }
      const schemaName = table.schema.name;
      const tableName = table.name;
      const dummyPackage = 'generation';
      const dummyName = 'myDB';
      const dummyDbPath = `${dummyPackage}::${dummyName}`;
      const databaseBuilderInput = new DatabaseBuilderInput(this.connection);
      databaseBuilderInput.targetDatabase = new TargetDatabase(
        dummyPackage,
        dummyName,
      );
      const config = databaseBuilderInput.config;
      config.maxTables = undefined;
      config.enrichTables = true;
      config.enrichColumns = true;
      config.enrichPrimaryKeys = true;
      config.patterns.push(new DatabasePattern(table.schema.name, table.name));
      const entities =
        (yield this.editorStore.graphManagerState.graphManager.buildDatabase(
          databaseBuilderInput,
        )) as Entity[];
      assertTrue(entities.length === 1);
      const dbEntity = guaranteeNonNullable(entities[0]);
      const emptyGraph = this.editorStore.graphManagerState.createNewGraph();
      (yield this.editorStore.graphManagerState.graphManager.buildGraph(
        emptyGraph,
        [dbEntity],
        ActionState.create(),
      )) as Entity[];
      const generatedDb = emptyGraph.getDatabase(dummyDbPath);
      const resolvedTable = getTable(
        getSchema(generatedDb, schemaName),
        tableName,
      );
      let queryGrammar: string;
      let resolveResult = false;
      if (column) {
        const resolvedColumn = getColumn(resolvedTable, column.name);
        const grammarResult = buildTableToTDSQueryColumnQuery(resolvedColumn);
        queryGrammar = grammarResult[0];
        resolveResult = grammarResult[1];
      } else {
        queryGrammar = buildTableToTDSQueryGrammar(resolvedTable);
      }
      const rawLambda =
        (yield this.editorStore.graphManagerState.graphManager.pureCodeToLambda(
          queryGrammar,
          'QUERY',
        )) as RawLambda;
      const { mapping, runtime } = buildTDSModel(
        emptyGraph,
        this.connection,
        generatedDb,
      );
      const execPlan = (
        (yield this.editorStore.graphManagerState.graphManager.runQuery(
          rawLambda,
          mapping,
          runtime,
          emptyGraph,
        )) as ExecutionResultWithMetadata
      ).executionResult;
      let tdsResult = guaranteeType(
        execPlan,
        TDSExecutionResult,
        'Execution from `tabletoTDS` expected to be TDS',
      );
      if (resolveResult) {
        const newResult = new TDSExecutionResult();
        newResult.result.columns = ['Aggregation', 'Value'];
        newResult.result.rows = tdsResult.result.columns.map((col, idx) => {
          const _row = new TDSRow();
          _row.values = [
            col,
            guaranteeNonNullable(
              guaranteeNonNullable(tdsResult.result.rows[0]).values[idx],
            ),
          ];
          return _row;
        });
        tdsResult = newResult;
      }
      this.previewer = tdsResult;
    } catch (error) {
      assertErrorThrown(error);
      this.editorStore.applicationStore.notificationService.notifyError(
        `Unable to preview data: ${error.message}`,
      );
    } finally {
      this.previewDataState.complete();
    }
  }

  *generateDatabase(): GeneratorFn<Entity> {
    try {
      this.isGeneratingDatabase = true;

      const treeData = guaranteeNonNullable(this.treeData);
      const databaseBuilderInput = new DatabaseBuilderInput(this.connection);
      const [packagePath, name] = this.resolveDatabasePackageAndName;
      databaseBuilderInput.targetDatabase = new TargetDatabase(
        packagePath,
        name,
      );
      const config = databaseBuilderInput.config;
      config.maxTables = undefined;
      config.enrichTables = true;
      config.enrichColumns = true;
      config.enrichPrimaryKeys = true;
      treeData.rootIds
        .map((e) => treeData.nodes.get(e))
        .filter(isNonNullable)
        .forEach((schemaNode) => {
          if (schemaNode instanceof DatabaseSchemaExplorerTreeSchemaNodeData) {
            const tableNodes = this.getChildNodes(schemaNode, treeData);
            const allChecked = tableNodes?.every((t) => t.isChecked === true);
            if (
              allChecked ||
              (schemaNode.isChecked && !schemaNode.childrenIds)
            ) {
              config.patterns.push(
                new DatabasePattern(schemaNode.schema.name, undefined),
              );
            } else {
              tableNodes?.forEach((t) => {
                if (
                  t instanceof DatabaseSchemaExplorerTreeTableNodeData &&
                  t.isChecked
                ) {
                  config.patterns.push(
                    new DatabasePattern(schemaNode.schema.name, t.table.name),
                  );
                }
              });
            }
          }
        });
      const entities =
        (yield this.editorStore.graphManagerState.graphManager.buildDatabase(
          databaseBuilderInput,
        )) as Entity[];
      return at(entities, 0, 'Expected a database to be generated');
    } finally {
      this.isGeneratingDatabase = false;
    }
  }

  // this method just updates database
  *updateDatabase(forceRename?: boolean): GeneratorFn<Database> {
    this.isUpdatingDatabase = true;
    const graph = this.editorStore.graphManagerState.createNewGraph();
    (yield this.editorStore.graphManagerState.graphManager.buildGraph(
      graph,
      [(yield flowResult(this.generateDatabase())) as Entity],
      ActionState.create(),
    )) as Entity[];
    const database = at(
      graph.ownDatabases,
      0,
      'Expected one database to be generated from input',
    );
    // remove undefined schemas
    const schemas = Array.from(
      guaranteeNonNullable(this.treeData).nodes.values(),
    )
      .map((schemaNode) => {
        if (schemaNode instanceof DatabaseSchemaExplorerTreeSchemaNodeData) {
          return schemaNode.schema;
        }
        return undefined;
      })
      .filter(isNonNullable);

    // update this.database packge and name
    if (forceRename || this.database.name === '' || !this.database.package) {
      this.database.package = getOrCreateGraphPackage(
        graph,
        extractPackagePathFromPath(this.targetDatabasePath),
        undefined,
      );
      this.database.name = extractElementNameFromPath(this.targetDatabasePath);
    }
    // update schemas
    this.database.schemas = this.database.schemas.filter((schema) => {
      if (
        schemas.find((item) => item.name === schema.name) &&
        !database.schemas.find((s) => s.name === schema.name)
      ) {
        return false;
      }
      return true;
    });
    // update existing schemas
    database.schemas.forEach((schema) => {
      (schema as Writable<Schema>)._OWNER = this.database;
      const currentSchemaIndex = this.database.schemas.findIndex(
        (item) => item.name === schema.name,
      );
      if (currentSchemaIndex !== -1) {
        this.database.schemas[currentSchemaIndex] = schema;
      } else {
        this.database.schemas.push(schema);
      }
    });
    this.isUpdatingDatabase = false;
    return database;
  }

  // this method updates database and add database to the graph
  *updateDatabaseAndGraph(): GeneratorFn<void> {
    if (!this.treeData) {
      return;
    }
    try {
      const createDatabase =
        this.isCreatingNewDatabase &&
        !this.editorStore.graphManagerState.graph.databases.includes(
          this.database,
        );
      this.isUpdatingDatabase = true;
      const database = (yield flowResult(this.updateDatabase())) as Database;
      if (createDatabase) {
        connection_setStore(
          this.connection,
          PackageableElementExplicitReference.create(database),
        );
        const packagePath = guaranteeNonNullable(
          database.package?.name,
          'Database package is missing',
        );
        yield flowResult(
          this.editorStore.graphEditorMode.addElement(
            database,
            packagePath,
            false,
          ),
        );
      }
      this.editorStore.applicationStore.notificationService.notifySuccess(
        `Database successfully updated`,
      );
      yield flowResult(
        this.editorStore
          .getGraphEditorMode(GraphEditFormModeState)
          .globalCompile({
            message: `Can't compile graph after editing database. Redirecting you to text mode`,
          }),
      );
    } catch (error) {
      assertErrorThrown(error);
      this.editorStore.applicationStore.logService.error(
        LogEvent.create(LEGEND_STUDIO_APP_EVENT.DATABASE_BUILDER_FAILURE),
        error,
      );
      this.editorStore.applicationStore.notificationService.notifyError(error);
    } finally {
      this.isUpdatingDatabase = false;
    }
  }
}
