import {
  DecimalType,
  EntitySchema,
  RawQueryFragment,
  ReferenceKind,
  t,
  Type,
  UnknownType,
  Utils,
} from '@mikro-orm/core';
/**
 * @internal
 */
export class DatabaseTable {
  name;
  schema;
  #columns = {};
  #indexes = [];
  #checks = [];
  #foreignKeys = {};
  #platform;
  nativeEnums = {}; // for postgres
  comment;
  constructor(platform, name, schema) {
    this.name = name;
    this.schema = schema;
    this.#platform = platform;
  }
  getQuotedName() {
    return this.#platform.quoteIdentifier(this.getShortestName());
  }
  getColumns() {
    return Object.values(this.#columns);
  }
  getColumn(name) {
    return this.#columns[name];
  }
  removeColumn(name) {
    delete this.#columns[name];
  }
  getIndexes() {
    return Utils.removeDuplicates(this.#indexes);
  }
  getChecks() {
    return this.#checks;
  }
  /** @internal */
  setIndexes(indexes) {
    this.#indexes = indexes;
  }
  /** @internal */
  setChecks(checks) {
    this.#checks = checks;
  }
  /** @internal */
  setForeignKeys(fks) {
    this.#foreignKeys = fks;
  }
  init(cols, indexes = [], checks = [], pks, fks = {}, enums = {}) {
    this.#indexes = indexes;
    this.#checks = checks;
    this.#foreignKeys = fks;
    this.#columns = cols.reduce((o, v) => {
      const index = indexes.filter(i => i.columnNames[0] === v.name);
      v.primary = v.primary || pks.includes(v.name);
      v.unique = index.some(i => i.unique && !i.primary);
      const type = v.name in enums ? 'enum' : v.type;
      v.mappedType = this.#platform.getMappedType(type);
      v.default = v.default?.toString().startsWith('nextval(') ? null : v.default;
      v.enumItems ??= enums[v.name] || [];
      o[v.name] = v;
      return o;
    }, {});
  }
  addColumn(column) {
    this.#columns[column.name] = column;
  }
  addColumnFromProperty(prop, meta, config) {
    prop.fieldNames?.forEach((field, idx) => {
      const type = prop.enum ? 'enum' : prop.columnTypes[idx];
      const mappedType = this.#platform.getMappedType(type);
      if (mappedType instanceof DecimalType) {
        const match = /\w+\((\d+), ?(\d+)\)/.exec(prop.columnTypes[idx]);
        /* v8 ignore next */
        if (match) {
          prop.precision ??= +match[1];
          prop.scale ??= +match[2];
          prop.length = undefined;
        }
      }
      if (prop.length == null && prop.columnTypes[idx]) {
        prop.length = this.#platform.getSchemaHelper().inferLengthFromColumnType(prop.columnTypes[idx]);
        if (typeof mappedType.getDefaultLength !== 'undefined') {
          prop.length ??= mappedType.getDefaultLength(this.#platform);
        }
      }
      const primary = !meta.compositePK && prop.fieldNames.length === 1 && !!prop.primary;
      this.#columns[field] = {
        name: prop.fieldNames[idx],
        type: prop.columnTypes[idx],
        generated:
          prop.generated instanceof RawQueryFragment
            ? this.#platform.formatQuery(prop.generated.sql, prop.generated.params)
            : prop.generated,
        mappedType,
        unsigned: prop.unsigned && this.#platform.isNumericColumn(mappedType),
        autoincrement:
          prop.autoincrement ??
          (primary && prop.kind === ReferenceKind.SCALAR && this.#platform.isNumericColumn(mappedType)),
        primary,
        nullable: this.#columns[field]?.nullable ?? !!prop.nullable,
        nativeEnumName: prop.nativeEnumName,
        length: prop.length,
        precision: prop.precision,
        scale: prop.scale,
        default: prop.defaultRaw,
        enumItems: prop.nativeEnumName || prop.items?.every(i => typeof i === 'string') ? prop.items : undefined,
        comment: prop.comment,
        extra: prop.extra,
        ignoreSchemaChanges: prop.ignoreSchemaChanges,
      };
      this.#columns[field].unsigned ??= this.#columns[field].autoincrement;
      if (this.nativeEnums[type]) {
        this.#columns[field].enumItems ??= this.nativeEnums[type].items;
      }
      const defaultValue = this.#platform.getSchemaHelper().normalizeDefaultValue(prop.defaultRaw, prop.length);
      this.#columns[field].default = defaultValue;
    });
    if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.polymorphic) {
      const constraintName = this.getIndexName(prop.foreignKeyName ?? true, prop.fieldNames, 'foreign');
      let schema =
        prop.targetMeta.root.schema === '*'
          ? this.schema
          : (prop.targetMeta.root.schema ?? config.get('schema', this.#platform.getDefaultSchemaName()));
      if (prop.referencedTableName.includes('.')) {
        schema = undefined;
      }
      // For cross-schema FKs on MySQL/MariaDB (where schema = database), when the referenced
      // table has no explicit schema but the current table does, qualify with dbName so the
      // FK can resolve the referenced table in the correct database
      if (!schema && this.schema && !this.#platform.getDefaultSchemaName()) {
        schema = config.get('dbName');
      }
      if (prop.createForeignKeyConstraint) {
        this.#foreignKeys[constraintName] = {
          constraintName,
          columnNames: prop.fieldNames,
          localTableName: this.getShortestName(false),
          referencedColumnNames: prop.referencedColumnNames,
          referencedTableName: schema ? `${schema}.${prop.referencedTableName}` : prop.referencedTableName,
        };
        const schemaConfig = config.get('schemaGenerator');
        this.#foreignKeys[constraintName].deleteRule = prop.deleteRule ?? schemaConfig.defaultDeleteRule;
        this.#foreignKeys[constraintName].updateRule = prop.updateRule ?? schemaConfig.defaultUpdateRule;
        if (prop.deferMode) {
          this.#foreignKeys[constraintName].deferMode = prop.deferMode;
        }
      }
    }
    if (prop.index) {
      this.#indexes.push({
        columnNames: prop.fieldNames,
        composite: prop.fieldNames.length > 1,
        keyName: this.getIndexName(prop.index, prop.fieldNames, 'index'),
        constraint: false,
        primary: false,
        unique: false,
      });
    }
    if (prop.unique && !(prop.primary && !meta.compositePK)) {
      this.#indexes.push({
        columnNames: prop.fieldNames,
        composite: prop.fieldNames.length > 1,
        keyName: this.getIndexName(prop.unique, prop.fieldNames, 'unique'),
        constraint: !prop.fieldNames.some(d => d.includes('.')),
        primary: false,
        unique: true,
        deferMode: prop.deferMode,
      });
    }
  }
  getIndexName(value, columnNames, type) {
    if (typeof value === 'string') {
      return value;
    }
    return this.#platform.getIndexName(this.name, columnNames, type);
  }
  getEntityDeclaration(namingStrategy, schemaHelper, scalarPropertiesForRelations) {
    const { fksOnColumnProps, fksOnStandaloneProps, columnFks, fkIndexes, nullableForeignKeys, skippedColumnNames } =
      this.foreignKeysToProps(namingStrategy, scalarPropertiesForRelations);
    const name = namingStrategy.getEntityName(this.name, this.schema);
    const schema = new EntitySchema({ name, collection: this.name, schema: this.schema, comment: this.comment });
    const compositeFkIndexes = {};
    const compositeFkUniques = {};
    const potentiallyUnmappedIndexes = this.#indexes.filter(
      index =>
        !index.primary && // Skip primary index. Whether it's in use by scalar column or FK, it's already mapped.
        // Non-trivial non-composite indexes will be declared at the entity's metadata, though later outputted in the property
        (index.columnNames.length > 1 || // All composite indexes are to be mapped to entity decorators or FK props.
          skippedColumnNames.includes(index.columnNames[0]) || // Non-composite indexes for skipped columns are to be mapped as entity decorators.
          index.deferMode ||
          index.expression ||
          !(index.columnNames[0] in columnFks)) && // Trivial non-composite indexes for scalar props are to be mapped to the column.
        // ignore indexes that don't have all column names (this can happen in sqlite where there is no way to infer this for expressions)
        !(index.columnNames.some(col => !col) && !index.expression),
    );
    // Helper to map column name to property name
    const columnToPropertyName = colName => this.getPropertyName(namingStrategy, colName);
    for (const index of potentiallyUnmappedIndexes) {
      // Build the index/unique options object with advanced options
      const ret = {
        name: index.keyName,
        deferMode: index.deferMode,
        expression: index.expression,
        // Advanced index options - convert column names to property names
        columns: index.columns?.map(col => ({
          ...col,
          name: columnToPropertyName(col.name),
        })),
        include: index.include?.map(colName => columnToPropertyName(colName)),
        fillFactor: index.fillFactor,
        disabled: index.disabled,
      };
      // Index-only options (not valid for Unique)
      if (!index.unique) {
        if (index.type) {
          // Convert index type - IndexDef.type can be string or object, IndexOptions.type is just string
          ret.type = typeof index.type === 'string' ? index.type : index.type.indexType;
        }
        if (index.invisible) {
          ret.invisible = index.invisible;
        }
        if (index.clustered) {
          ret.clustered = index.clustered;
        }
      }
      // An index is trivial if it has no special options that require entity-level declaration
      const hasAdvancedOptions =
        index.columns?.length ||
        index.include?.length ||
        index.fillFactor ||
        index.type ||
        index.invisible ||
        index.disabled ||
        index.clustered;
      const isTrivial = !index.deferMode && !index.expression && !hasAdvancedOptions;
      if (isTrivial) {
        // Index is for FK. Map to the FK prop and move on.
        const fkForIndex = fkIndexes.get(index);
        if (fkForIndex && !fkForIndex.fk.columnNames.some(col => !index.columnNames.includes(col))) {
          ret.properties = [this.getPropertyName(namingStrategy, fkForIndex.baseName, fkForIndex.fk)];
          const map = index.unique ? compositeFkUniques : compositeFkIndexes;
          if (typeof map[ret.properties[0]] === 'undefined') {
            map[ret.properties[0]] = index;
            continue;
          }
        }
      }
      const properties =
        ret.properties ??
        this.getIndexProperties(index, columnFks, fksOnColumnProps, fksOnStandaloneProps, namingStrategy);
      // If there is a column that cannot be unambiguously mapped to a prop, render an expression.
      if (typeof properties === 'undefined') {
        ret.expression ??= schemaHelper.getCreateIndexSQL(this.name, index);
      } else {
        ret.properties ??= properties;
        // If the index is for one property that is not a FK prop, map to the column prop and move on.
        if (properties.length === 1 && isTrivial && !fksOnStandaloneProps.has(properties[0])) {
          const map = index.unique ? compositeFkUniques : compositeFkIndexes;
          // Only map one trivial index. If the same column is indexed many times over, output
          if (typeof map[properties[0]] === 'undefined') {
            map[properties[0]] = index;
            continue;
          }
        }
      }
      // Composite indexes that aren't exclusively mapped to FK props get an entity decorator.
      if (index.unique) {
        schema.addUnique(ret);
        continue;
      }
      schema.addIndex(ret);
    }
    const addedStandaloneFkPropsBasedOnColumn = new Set();
    const nonSkippedColumns = this.getColumns().filter(column => !skippedColumnNames.includes(column.name));
    for (const column of nonSkippedColumns) {
      const columnName = column.name;
      const standaloneFkPropBasedOnColumn = fksOnStandaloneProps.get(columnName);
      if (standaloneFkPropBasedOnColumn && !fksOnColumnProps.get(columnName)) {
        addedStandaloneFkPropsBasedOnColumn.add(columnName);
        const { fkIndex, currentFk } = standaloneFkPropBasedOnColumn;
        const prop = this.getForeignKeyDeclaration(
          currentFk,
          namingStrategy,
          schemaHelper,
          fkIndex,
          nullableForeignKeys.has(currentFk),
          columnName,
          fksOnColumnProps,
        );
        schema.addProperty(prop.name, prop.type, prop);
      }
      const prop = this.getPropertyDeclaration(
        column,
        namingStrategy,
        schemaHelper,
        compositeFkIndexes,
        compositeFkUniques,
        columnFks,
        fksOnColumnProps.get(columnName),
      );
      schema.addProperty(prop.name, prop.type, prop);
    }
    for (const [propBaseName, { fkIndex, currentFk }] of fksOnStandaloneProps.entries()) {
      if (addedStandaloneFkPropsBasedOnColumn.has(propBaseName)) {
        continue;
      }
      const prop = this.getForeignKeyDeclaration(
        currentFk,
        namingStrategy,
        schemaHelper,
        fkIndex,
        nullableForeignKeys.has(currentFk),
        propBaseName,
        fksOnColumnProps,
      );
      schema.addProperty(prop.name, prop.type, prop);
    }
    const meta = schema.init().meta;
    const oneToOneCandidateProperties = meta.relations.filter(
      prop => prop.primary && prop.kind === ReferenceKind.MANY_TO_ONE,
    );
    if (
      oneToOneCandidateProperties.length === 1 &&
      oneToOneCandidateProperties[0].fieldNames.length ===
        new Set(meta.getPrimaryProps().flatMap(prop => prop.fieldNames)).size
    ) {
      oneToOneCandidateProperties[0].kind = ReferenceKind.ONE_TO_ONE;
    }
    return meta;
  }
  foreignKeysToProps(namingStrategy, scalarPropertiesForRelations) {
    const fks = Object.values(this.getForeignKeys());
    const fksOnColumnProps = new Map();
    const fksOnStandaloneProps = new Map();
    const columnFks = {};
    const fkIndexes = new Map();
    const nullableForeignKeys = new Set();
    const standaloneFksBasedOnColumnNames = new Map();
    for (const currentFk of fks) {
      const fkIndex = this.findFkIndex(currentFk);
      if (
        currentFk.columnNames.length === 1 &&
        !fks.some(
          fk => fk !== currentFk && fk.columnNames.length === 1 && currentFk.columnNames[0] === fk.columnNames[0],
        )
      ) {
        // Non-composite FK is the only possible one for a column. Render the column with it.
        const columnName = currentFk.columnNames[0];
        columnFks[columnName] ??= [];
        columnFks[columnName].push(currentFk);
        if (this.getColumn(columnName)?.nullable) {
          nullableForeignKeys.add(currentFk);
        }
        if (scalarPropertiesForRelations === 'always') {
          const baseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName);
          standaloneFksBasedOnColumnNames.set(baseName, currentFk);
          fksOnStandaloneProps.set(baseName, { fkIndex, currentFk });
          if (fkIndex) {
            fkIndexes.set(fkIndex, { fk: currentFk, baseName });
          }
        } else {
          fksOnColumnProps.set(columnName, currentFk);
          if (fkIndex) {
            fkIndexes.set(fkIndex, { fk: currentFk, baseName: columnName });
          }
        }
        continue;
      }
      const specificColumnNames = [];
      const nullableColumnsInFk = [];
      for (const columnName of currentFk.columnNames) {
        columnFks[columnName] ??= [];
        columnFks[columnName].push(currentFk);
        if (!fks.some(fk => fk !== currentFk && fk.columnNames.includes(columnName))) {
          specificColumnNames.push(columnName);
        }
        if (this.getColumn(columnName)?.nullable) {
          nullableColumnsInFk.push(columnName);
        }
      }
      if (nullableColumnsInFk.length > 0) {
        nullableForeignKeys.add(currentFk);
      }
      if (
        specificColumnNames.length === 1 &&
        (nullableColumnsInFk.length === currentFk.columnNames.length ||
          nullableColumnsInFk.length === 0 ||
          (nullableColumnsInFk.length === 1 && nullableColumnsInFk[0] === specificColumnNames[0]))
      ) {
        // Composite FK has exactly one column which is not used in any other FK.
        // The FK also doesn't have a mix of nullable and non-nullable columns,
        // or its only nullable column is this very one.
        // It is safe to just render this FK attached to the specific column.
        const columnName = specificColumnNames[0];
        if (scalarPropertiesForRelations === 'always') {
          const baseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName);
          standaloneFksBasedOnColumnNames.set(baseName, currentFk);
          fksOnStandaloneProps.set(baseName, { fkIndex, currentFk });
          if (fkIndex) {
            fkIndexes.set(fkIndex, { fk: currentFk, baseName });
          }
        } else {
          fksOnColumnProps.set(columnName, currentFk);
          if (fkIndex) {
            fkIndexes.set(fkIndex, { fk: currentFk, baseName: columnName });
          }
        }
        continue;
      }
      if (specificColumnNames.length === currentFk.columnNames.length) {
        // All columns involved with this FK are only covered by this one FK.
        if (nullableColumnsInFk.length <= 1) {
          // Also, this FK is either not nullable, or has only one nullable column.
          // It is safe to name the FK after the nullable column, or any non-nullable one (the first one is picked).
          const columnName = nullableColumnsInFk.at(0) ?? currentFk.columnNames[0];
          if (scalarPropertiesForRelations === 'always') {
            const baseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName);
            standaloneFksBasedOnColumnNames.set(baseName, currentFk);
            fksOnStandaloneProps.set(baseName, { fkIndex, currentFk });
            if (fkIndex) {
              fkIndexes.set(fkIndex, { fk: currentFk, baseName });
            }
          } else {
            fksOnColumnProps.set(columnName, currentFk);
            if (fkIndex) {
              fkIndexes.set(fkIndex, { fk: currentFk, baseName: columnName });
            }
          }
          continue;
        }
        // If the first nullable column's name with FK is different from the name without FK,
        // name a standalone prop after the column, but treat the column prop itself as not having FK.
        const columnName = nullableColumnsInFk[0];
        const baseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName);
        standaloneFksBasedOnColumnNames.set(baseName, currentFk);
        fksOnStandaloneProps.set(baseName, { fkIndex, currentFk });
        if (fkIndex) {
          fkIndexes.set(fkIndex, { fk: currentFk, baseName });
        }
        continue;
      }
      // FK is not unambiguously mappable to a column. Pick another name for a standalone FK prop.
      const baseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks);
      fksOnStandaloneProps.set(baseName, { fkIndex, currentFk });
      if (fkIndex) {
        fkIndexes.set(fkIndex, { fk: currentFk, baseName });
      }
    }
    const columnsInFks = Object.keys(columnFks);
    const skippingHandlers = {
      // Never generate scalar props for composite keys,
      // i.e. always skip columns if they are covered by foreign keys.
      never: column => columnsInFks.includes(column.name) && !fksOnColumnProps.has(column.name),
      // Always generate scalar props for composite keys,
      // i.e. do not skip columns, even if they are covered by foreign keys.
      always: column => false,
      // Smart scalar props generation.
      // Skips columns if they are covered by foreign keys.
      // But also does not skip if the column is not nullable, and yet all involved FKs are nullable,
      // or if one or more FKs involved has multiple nullable columns.
      smart: column => {
        return (
          columnsInFks.includes(column.name) &&
          !fksOnColumnProps.has(column.name) &&
          (column.nullable
            ? columnFks[column.name].some(
                fk =>
                  !fk.columnNames.some(
                    fkColumnName => fkColumnName !== column.name && this.getColumn(fkColumnName)?.nullable,
                  ),
              )
            : columnFks[column.name].some(fk => !nullableForeignKeys.has(fk)))
        );
      },
    };
    const skippedColumnNames = this.getColumns()
      .filter(skippingHandlers[scalarPropertiesForRelations])
      .map(column => column.name);
    // Check standalone FKs named after columns for potential conflicts among themselves.
    // This typically happens when two standalone FKs named after a column resolve to the same prop name
    // because the respective columns include the referenced table in the name.
    // Depending on naming strategy and actual names, it may also originate from other scenarios.
    // We do our best to de-duplicate them here.
    const safePropNames = new Set();
    const unsafePropNames = new Map();
    for (const [unsafeBaseName, currentFk] of standaloneFksBasedOnColumnNames) {
      const propName = this.getPropertyName(namingStrategy, unsafeBaseName, currentFk);
      if (safePropNames.has(propName)) {
        if (!unsafePropNames.has(propName)) {
          unsafePropNames.set(propName, []);
        }
        unsafePropNames.get(propName).push({ unsafeBaseName, currentFk });
        continue;
      }
      safePropNames.add(propName);
    }
    for (const [unsafePropName, affectedBaseNames] of unsafePropNames) {
      safePropNames.delete(unsafePropName);
      for (const { unsafeBaseName, currentFk } of affectedBaseNames) {
        const newBaseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks);
        fksOnStandaloneProps.delete(unsafeBaseName);
        let fkIndex;
        for (const [indexDef, fkIndexDesc] of fkIndexes) {
          if (fkIndexDesc.fk !== currentFk) {
            continue;
          }
          fkIndexDesc.baseName = newBaseName;
          fkIndex = indexDef;
          break;
        }
        fksOnStandaloneProps.set(newBaseName, { fkIndex, currentFk });
      }
    }
    return { fksOnColumnProps, fksOnStandaloneProps, columnFks, fkIndexes, nullableForeignKeys, skippedColumnNames };
  }
  findFkIndex(currentFk) {
    const fkColumnsLength = currentFk.columnNames.length;
    const possibleIndexes = this.#indexes.filter(index => {
      return (
        index.columnNames.length === fkColumnsLength &&
        !currentFk.columnNames.some((columnName, i) => index.columnNames[i] !== columnName)
      );
    });
    possibleIndexes.sort((a, b) => {
      if (a.primary !== b.primary) {
        return a.primary ? -1 : 1;
      }
      if (a.unique !== b.unique) {
        return a.unique ? -1 : 1;
      }
      return a.keyName.localeCompare(b.keyName);
    });
    return possibleIndexes.at(0);
  }
  getIndexProperties(index, columnFks, fksOnColumnProps, fksOnStandaloneProps, namingStrategy) {
    const propBaseNames = new Set();
    const columnNames = index.columnNames;
    const l = columnNames.length;
    if (columnNames.some(col => !col)) {
      return;
    }
    for (let i = 0; i < l; ++i) {
      const columnName = columnNames[i];
      // The column is not involved with FKs.
      if (!(columnName in columnFks)) {
        // If there is no such column, the "name" is actually an expression.
        if (!this.hasColumn(columnName)) {
          return;
        }
        // It has a prop named after it.
        // Add it and move on.
        propBaseNames.add(columnName);
        continue;
      }
      // If the prop named after the column has a FK and the FK's columns are a subset of this index,
      // include this prop and move on.
      const columnPropFk = fksOnColumnProps.get(columnName);
      if (columnPropFk && !columnPropFk.columnNames.some(fkColumnName => !columnNames.includes(fkColumnName))) {
        propBaseNames.add(columnName);
        continue;
      }
      // If there is at least one standalone FK featuring this column,
      // and all of its columns are a subset of this index,
      // include that FK, and consider mapping of this column to a prop a success.
      let propAdded = false;
      for (const [propName, { currentFk: fk }] of fksOnStandaloneProps) {
        if (!columnFks[columnName].includes(fk)) {
          continue;
        }
        if (!fk.columnNames.some(fkColumnName => !columnNames.includes(fkColumnName))) {
          propBaseNames.add(propName);
          propAdded = true;
        }
      }
      if (propAdded) {
        continue;
      }
      // If we have reached this point, it means the column is not mappable to a prop name.
      // Break the whole prop creation.
      return;
    }
    return Array.from(propBaseNames).map(baseName =>
      this.getPropertyName(namingStrategy, baseName, fksOnColumnProps.get(baseName)),
    );
  }
  getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName) {
    if (
      columnName &&
      this.getPropertyName(namingStrategy, columnName, currentFk) !== this.getPropertyName(namingStrategy, columnName)
    ) {
      // The eligible scalar column name is different from the name of the FK prop of the same column.
      // Both can be safely rendered.
      // Use the column name as a base for the FK prop.
      return columnName;
    }
    // Strip schema prefix from referenced table name (e.g., "public.fr_usuario" -> "fr_usuario")
    const getTableName = fullName => {
      const parts = fullName.split('.');
      return parts[parts.length - 1];
    };
    const referencedTableName = getTableName(currentFk.referencedTableName);
    // Check for conflicts using stripped table names (handles cross-schema FKs to same-named tables)
    const hasConflictingFk = fks.some(
      fk => fk !== currentFk && getTableName(fk.referencedTableName) === referencedTableName,
    );
    if (!hasConflictingFk && !this.getColumn(referencedTableName)) {
      // FK is the only one in this table that references a table with this name.
      // The name of the referenced table is not shared with a column in this table,
      // so it is safe to output prop name based on the referenced entity.
      return referencedTableName;
    }
    // Any ambiguous FK is rendered with a name based on the FK constraint name
    let finalPropBaseName = currentFk.constraintName;
    while (this.getColumn(finalPropBaseName)) {
      // In the unlikely event that the FK constraint name is shared by a column name, generate a name by
      // continuously prefixing with "fk_", until a non-existent column is hit.
      // The worst case scenario is a very long name with several repeated "fk_"
      // that is not really a valid DB identifier but a valid JS variable name.
      finalPropBaseName = `fk_${finalPropBaseName}`;
    }
    return finalPropBaseName;
  }
  /**
   * The shortest name is stripped of the default namespace. All other namespaced elements are returned as full-qualified names.
   */
  getShortestName(skipDefaultSchema = true) {
    const defaultSchema = this.#platform.getDefaultSchemaName();
    if (
      !this.schema ||
      this.name.startsWith(defaultSchema + '.') ||
      (this.schema === defaultSchema && skipDefaultSchema)
    ) {
      return this.name;
    }
    return `${this.schema}.${this.name}`;
  }
  getForeignKeys() {
    return this.#foreignKeys;
  }
  hasColumn(columnName) {
    return columnName in this.#columns;
  }
  getIndex(indexName) {
    return this.#indexes.find(i => i.keyName === indexName);
  }
  hasIndex(indexName) {
    return !!this.getIndex(indexName);
  }
  getCheck(checkName) {
    return this.#checks.find(i => i.name === checkName);
  }
  hasCheck(checkName) {
    return !!this.getCheck(checkName);
  }
  getPrimaryKey() {
    return this.#indexes.find(i => i.primary);
  }
  hasPrimaryKey() {
    return !!this.getPrimaryKey();
  }
  getForeignKeyDeclaration(fk, namingStrategy, schemaHelper, fkIndex, nullable, propNameBase, fksOnColumnProps) {
    const prop = this.getPropertyName(namingStrategy, propNameBase, fk);
    const kind = fkIndex?.unique && !fkIndex.primary ? this.getReferenceKind(fk, fkIndex) : this.getReferenceKind(fk);
    const runtimeType = this.getPropertyTypeForForeignKey(namingStrategy, fk);
    const fkOptions = {};
    fkOptions.fieldNames = fk.columnNames;
    fkOptions.referencedTableName = fk.referencedTableName;
    fkOptions.referencedColumnNames = fk.referencedColumnNames;
    fkOptions.updateRule = fk.updateRule?.toLowerCase();
    fkOptions.deleteRule = fk.deleteRule?.toLowerCase();
    fkOptions.deferMode = fk.deferMode;
    fkOptions.columnTypes = fk.columnNames.map(c => this.getColumn(c).type);
    const columnOptions = {};
    if (fk.columnNames.length === 1) {
      const column = this.getColumn(fk.columnNames[0]);
      const defaultRaw = this.getPropertyDefaultValue(schemaHelper, column, column.type, true);
      const defaultTs = this.getPropertyDefaultValue(schemaHelper, column, column.type);
      columnOptions.default = defaultRaw !== defaultTs || defaultRaw === '' ? defaultTs : undefined;
      columnOptions.defaultRaw = column.nullable && defaultRaw === 'null' ? undefined : defaultRaw;
      columnOptions.optional = typeof column.generated !== 'undefined' || defaultRaw !== 'null';
      columnOptions.generated = column.generated;
      columnOptions.nullable = column.nullable;
      columnOptions.primary = column.primary;
      columnOptions.length = column.length;
      columnOptions.precision = column.precision;
      columnOptions.scale = column.scale;
      columnOptions.extra = column.extra;
      columnOptions.comment = column.comment;
      columnOptions.enum = !!column.enumItems?.length;
      columnOptions.items = column.enumItems;
    }
    return {
      name: prop,
      type: runtimeType,
      runtimeType,
      kind,
      ...columnOptions,
      nullable,
      primary:
        fkIndex?.primary || !fk.columnNames.some(columnName => !this.getPrimaryKey()?.columnNames.includes(columnName)),
      index: !fkIndex?.unique ? fkIndex?.keyName : undefined,
      unique: fkIndex?.unique && !fkIndex.primary ? fkIndex.keyName : undefined,
      ...fkOptions,
    };
  }
  getPropertyDeclaration(column, namingStrategy, schemaHelper, compositeFkIndexes, compositeFkUniques, columnFks, fk) {
    const prop = this.getPropertyName(namingStrategy, column.name, fk);
    const persist = !(column.name in columnFks && typeof fk === 'undefined');
    const index =
      compositeFkIndexes[prop] ||
      this.#indexes.find(idx => idx.columnNames[0] === column.name && !idx.composite && !idx.unique && !idx.primary);
    const unique =
      compositeFkUniques[prop] ||
      this.#indexes.find(idx => idx.columnNames[0] === column.name && !idx.composite && idx.unique && !idx.primary);
    const kind = this.getReferenceKind(fk, unique);
    const runtimeType = this.getPropertyTypeForColumn(namingStrategy, column, fk);
    const type = fk
      ? runtimeType
      : (Utils.keys(t).find(k => {
          const typeInCoreMap = this.#platform.getMappedType(k);
          return (
            (typeInCoreMap !== Type.getType(UnknownType) || k === 'unknown') && typeInCoreMap === column.mappedType
          );
        }) ?? runtimeType);
    const ignoreSchemaChanges =
      type === 'unknown' && column.length ? (column.extra ? ['type', 'extra'] : ['type']) : undefined;
    const defaultRaw = this.getPropertyDefaultValue(schemaHelper, column, runtimeType, true);
    const defaultParsed = this.getPropertyDefaultValue(schemaHelper, column, runtimeType);
    const defaultTs = defaultRaw !== defaultParsed || defaultParsed === '' ? defaultParsed : undefined;
    const fkOptions = {};
    if (fk) {
      fkOptions.fieldNames = fk.columnNames;
      fkOptions.referencedTableName = fk.referencedTableName;
      fkOptions.referencedColumnNames = fk.referencedColumnNames;
      fkOptions.updateRule = fk.updateRule?.toLowerCase();
      fkOptions.deleteRule = fk.deleteRule?.toLowerCase();
      fkOptions.deferMode = fk.deferMode;
      fkOptions.columnTypes = fk.columnNames.map(col => this.getColumn(col).type);
    }
    const ret = {
      name: prop,
      type,
      runtimeType,
      kind,
      ignoreSchemaChanges,
      generated: column.generated,
      optional: defaultRaw !== 'null' || defaultTs != null || typeof column.generated !== 'undefined',
      columnType: column.type,
      default: defaultTs,
      defaultRaw: column.nullable && defaultRaw === 'null' ? undefined : defaultRaw,
      nullable: column.nullable,
      primary: column.primary && persist,
      autoincrement: column.autoincrement,
      fieldName: column.name,
      unsigned: column.unsigned,
      length: column.length,
      precision: column.precision,
      scale: column.scale,
      extra: column.extra,
      comment: column.comment,
      index: index ? index.keyName : undefined,
      unique: unique ? unique.keyName : undefined,
      enum: !!column.enumItems?.length,
      items: column.enumItems,
      persist,
      ...fkOptions,
    };
    const nativeEnumName = Object.keys(this.nativeEnums).find(name => name === column.type);
    if (nativeEnumName) {
      ret.nativeEnumName = nativeEnumName;
    }
    return ret;
  }
  getReferenceKind(fk, unique) {
    if (fk && unique) {
      return ReferenceKind.ONE_TO_ONE;
    }
    if (fk) {
      return ReferenceKind.MANY_TO_ONE;
    }
    return ReferenceKind.SCALAR;
  }
  getPropertyName(namingStrategy, baseName, fk) {
    let field = baseName;
    if (fk) {
      const idx = fk.columnNames.indexOf(baseName);
      let replacedFieldName = field.replace(new RegExp(`_${fk.referencedColumnNames[idx]}$`), '');
      if (replacedFieldName === field) {
        replacedFieldName = field.replace(new RegExp(`_${namingStrategy.referenceColumnName()}$`), '');
      }
      field = replacedFieldName;
    }
    if (field.startsWith('_')) {
      return field;
    }
    return namingStrategy.columnNameToProperty(field);
  }
  getPropertyTypeForForeignKey(namingStrategy, fk) {
    const parts = fk.referencedTableName.split('.', 2);
    return namingStrategy.getEntityName(...parts.reverse());
  }
  getPropertyTypeForColumn(namingStrategy, column, fk) {
    if (fk) {
      return this.getPropertyTypeForForeignKey(namingStrategy, fk);
    }
    const enumMode = this.#platform.getConfig().get('entityGenerator').enumMode;
    // If this column is using an enum.
    if (column.enumItems?.length) {
      const name = column.nativeEnumName ?? column.name;
      const tableName = column.nativeEnumName ? undefined : this.name;
      if (enumMode === 'ts-enum') {
        // We will create a new enum name for this type and set it as the property type as well.
        return namingStrategy.getEnumClassName(name, tableName, this.schema);
      }
      // With other enum strategies, we need to use the type name.
      return namingStrategy.getEnumTypeName(name, tableName, this.schema);
    }
    return column.mappedType?.runtimeType ?? 'unknown';
  }
  getPropertyDefaultValue(schemaHelper, column, propType, raw = false) {
    const defaultValue = column.default ?? 'null';
    const val = schemaHelper.normalizeDefaultValue(defaultValue, column.length);
    if (val === 'null') {
      return raw ? 'null' : column.nullable ? null : undefined;
    }
    if (propType === 'boolean' && !raw) {
      return !['0', 'false', 'f', 'n', 'no', 'off'].includes('' + column.default);
    }
    if (propType === 'number' && !raw) {
      return +defaultValue;
    }
    // unquote string defaults if `raw = false`
    const match = /^'(.*)'$/.exec('' + val);
    if (!raw && match) {
      return match[1];
    }
    return '' + val;
  }
  processIndexExpression(indexName, expression, meta) {
    if (expression instanceof Function) {
      const qualifiedName = this.schema ? `${this.schema}.${this.name}` : this.name;
      const table = {
        name: this.name,
        schema: this.schema,
        qualifiedName,
        toString: () => qualifiedName,
      };
      const columns = meta.createSchemaColumnMappingObject();
      const exp = expression(columns, table, indexName);
      return exp instanceof RawQueryFragment ? this.#platform.formatQuery(exp.sql, exp.params) : exp;
    }
    return expression;
  }
  addIndex(meta, index, type) {
    // If columns are specified but properties are not, derive properties from column names
    if (
      index.columns?.length &&
      !index.expression &&
      (!index.properties || Utils.asArray(index.properties).length === 0)
    ) {
      index = { ...index, properties: index.columns.map(c => c.name) };
    }
    const properties = Utils.unique(
      Utils.flatten(
        Utils.asArray(index.properties).map(prop => {
          const parts = prop.split('.');
          const root = parts[0];
          if (meta.properties[prop]) {
            if (meta.properties[prop].embeddedPath) {
              return [meta.properties[prop].embeddedPath.join('.')];
            }
            return meta.properties[prop].fieldNames;
          }
          const rootProp = meta.properties[root];
          // inline embedded property index, we need to find the field name of the child property
          if (rootProp?.embeddable && !rootProp.object && parts.length > 1) {
            const expand = (p, i) => {
              if (parts.length === i) {
                return p.fieldNames[0];
              }
              return expand(p.embeddedProps[parts[i]], i + 1);
            };
            return [expand(rootProp, 1)];
          }
          // json index, we need to rename the column only
          if (rootProp) {
            return [prop.replace(root, rootProp.fieldNames[0])];
          }
          /* v8 ignore next */
          return [prop];
        }),
      ),
    );
    if (properties.length === 0 && !index.expression) {
      return;
    }
    const name = this.getIndexName(index.name, properties, type);
    // Process include columns (map property names to field names)
    const includeColumns = index.include
      ? Utils.unique(
          Utils.flatten(
            Utils.asArray(index.include).map(prop => {
              if (meta.properties[prop]) {
                return meta.properties[prop].fieldNames;
              }
              /* v8 ignore next */
              return [prop];
            }),
          ),
        )
      : undefined;
    // Process columns with advanced options (map property names to field names)
    const columns = index.columns?.map(col => {
      const fieldName = meta.properties[col.name]?.fieldNames[0] ?? col.name;
      return {
        name: fieldName,
        sort: col.sort?.toUpperCase(),
        nulls: col.nulls?.toUpperCase(),
        length: col.length,
        collation: col.collation,
      };
    });
    // Validate that column options reference fields in the index properties
    if (columns?.length && properties.length > 0) {
      for (const col of columns) {
        if (!properties.includes(col.name)) {
          throw new Error(
            `Index '${name}' on entity '${meta.className}': column option references field '${col.name}' which is not in the index properties`,
          );
        }
      }
    }
    // Validate fillFactor range
    if (index.fillFactor != null && (index.fillFactor < 0 || index.fillFactor > 100)) {
      throw new Error(
        `fillFactor must be between 0 and 100, got ${index.fillFactor} for index '${name}' on entity '${meta.className}'`,
      );
    }
    this.#indexes.push({
      keyName: name,
      columnNames: properties,
      composite: properties.length > 1,
      // JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them
      constraint: type !== 'index' && !properties.some(d => d.includes('.')),
      primary: type === 'primary',
      unique: type !== 'index',
      type: index.type,
      expression: this.processIndexExpression(name, index.expression, meta),
      options: index.options,
      deferMode: index.deferMode,
      columns,
      include: includeColumns,
      fillFactor: index.fillFactor,
      invisible: index.invisible,
      disabled: index.disabled,
      clustered: index.clustered,
    });
  }
  addCheck(check) {
    this.#checks.push(check);
  }
  toJSON() {
    const columns = this.#columns;
    const columnsMapped = Utils.keys(columns).reduce((o, col) => {
      const c = columns[col];
      const normalized = {
        name: c.name,
        type: c.type,
        unsigned: !!c.unsigned,
        autoincrement: !!c.autoincrement,
        primary: !!c.primary,
        nullable: !!c.nullable,
        unique: !!c.unique,
        length: c.length ?? null,
        precision: c.precision ?? null,
        scale: c.scale ?? null,
        default: c.default ?? null,
        comment: c.comment ?? null,
        enumItems: c.enumItems ?? [],
        mappedType: Utils.keys(t).find(k => t[k] === c.mappedType.constructor),
      };
      if (c.generated) {
        normalized.generated = c.generated;
      }
      if (c.nativeEnumName) {
        normalized.nativeEnumName = c.nativeEnumName;
      }
      if (c.extra) {
        normalized.extra = c.extra;
      }
      if (c.ignoreSchemaChanges) {
        normalized.ignoreSchemaChanges = c.ignoreSchemaChanges;
      }
      if (c.defaultConstraint) {
        normalized.defaultConstraint = c.defaultConstraint;
      }
      o[col] = normalized;
      return o;
    }, {});
    return {
      name: this.name,
      schema: this.schema,
      columns: columnsMapped,
      indexes: this.#indexes,
      checks: this.#checks,
      foreignKeys: this.#foreignKeys,
      nativeEnums: this.nativeEnums,
      comment: this.comment,
    };
  }
}
