import { ReferenceKind, isRaw } from '@mikro-orm/core';
import { DatabaseTable } from './DatabaseTable.js';
/**
 * @internal
 */
export class DatabaseSchema {
  name;
  #tables = [];
  #views = [];
  #namespaces = new Set();
  #nativeEnums = {}; // for postgres
  #platform;
  constructor(platform, name) {
    this.name = name;
    this.#platform = platform;
  }
  addTable(name, schema, comment) {
    const namespaceName = schema ?? this.name;
    const table = new DatabaseTable(this.#platform, name, namespaceName);
    table.nativeEnums = this.#nativeEnums;
    table.comment = comment;
    this.#tables.push(table);
    if (namespaceName != null) {
      this.#namespaces.add(namespaceName);
    }
    return table;
  }
  getTables() {
    return this.#tables;
  }
  /** @internal */
  setTables(tables) {
    this.#tables = tables;
  }
  /** @internal */
  setNamespaces(namespaces) {
    this.#namespaces = namespaces;
  }
  getTable(name) {
    return this.#tables.find(t => t.name === name || `${t.schema}.${t.name}` === name);
  }
  hasTable(name) {
    return !!this.getTable(name);
  }
  addView(name, schema, definition, materialized, withData) {
    const namespaceName = schema ?? this.name;
    const view = { name, schema: namespaceName, definition, materialized, withData };
    this.#views.push(view);
    if (namespaceName != null) {
      this.#namespaces.add(namespaceName);
    }
    return view;
  }
  getViews() {
    return this.#views;
  }
  /** @internal */
  setViews(views) {
    this.#views = views;
  }
  getView(name) {
    return this.#views.find(v => v.name === name || `${v.schema}.${v.name}` === name);
  }
  hasView(name) {
    return !!this.getView(name);
  }
  setNativeEnums(nativeEnums) {
    this.#nativeEnums = nativeEnums;
    for (const nativeEnum of Object.values(nativeEnums)) {
      if (nativeEnum.schema && nativeEnum.schema !== '*') {
        this.#namespaces.add(nativeEnum.schema);
      }
    }
  }
  getNativeEnums() {
    return this.#nativeEnums;
  }
  getNativeEnum(name) {
    return this.#nativeEnums[name];
  }
  hasNamespace(namespace) {
    return this.#namespaces.has(namespace);
  }
  hasNativeEnum(name) {
    return name in this.#nativeEnums;
  }
  getNamespaces() {
    return [...this.#namespaces];
  }
  static async create(connection, platform, config, schemaName, schemas, takeTables, skipTables, skipViews) {
    const schema = new DatabaseSchema(platform, schemaName ?? config.get('schema') ?? platform.getDefaultSchemaName());
    const allTables = await platform.getSchemaHelper().getAllTables(connection, schemas);
    const parts = config.get('migrations').tableName.split('.');
    const migrationsTableName = parts[1] ?? parts[0];
    const migrationsSchemaName = parts.length > 1 ? parts[0] : config.get('schema', platform.getDefaultSchemaName());
    const tables = allTables.filter(
      t =>
        this.isTableNameAllowed(t.table_name, takeTables, skipTables) &&
        (t.table_name !== migrationsTableName || (t.schema_name && t.schema_name !== migrationsSchemaName)),
    );
    await platform
      .getSchemaHelper()
      .loadInformationSchema(schema, connection, tables, schemas && schemas.length > 0 ? schemas : undefined);
    // Load views from database
    await platform.getSchemaHelper().loadViews(schema, connection);
    // Load materialized views (PostgreSQL only)
    if (platform.supportsMaterializedViews()) {
      await platform.getSchemaHelper().loadMaterializedViews(schema, connection, schemaName);
    }
    // Filter out skipped views
    if (skipViews && skipViews.length > 0) {
      schema.#views = schema.#views.filter(v => this.isNameAllowed(v.name, skipViews));
    }
    return schema;
  }
  static fromMetadata(metadata, platform, config, schemaName, em) {
    const schema = new DatabaseSchema(platform, schemaName ?? config.get('schema'));
    const nativeEnums = {};
    const skipColumns = config.get('schemaGenerator').skipColumns || {};
    for (const meta of metadata) {
      // Skip view entities when collecting native enums
      if (meta.view) {
        continue;
      }
      for (const prop of meta.props) {
        if (prop.nativeEnumName) {
          let key = prop.nativeEnumName;
          let enumName = prop.nativeEnumName;
          let enumSchema = meta.schema ?? schema.name;
          if (key.includes('.')) {
            const [explicitSchema, ...parts] = prop.nativeEnumName.split('.');
            enumName = parts.join('.');
            key = enumName;
            enumSchema = explicitSchema;
          }
          if (enumSchema && enumSchema !== '*' && enumSchema !== platform.getDefaultSchemaName()) {
            key = enumSchema + '.' + key;
          }
          nativeEnums[key] = {
            name: enumName,
            schema: enumSchema,
            items: prop.items?.map(val => '' + val) ?? [],
          };
        }
      }
    }
    schema.setNativeEnums(nativeEnums);
    for (const meta of metadata) {
      // Handle view entities separately
      if (meta.view) {
        const viewDefinition = this.getViewDefinition(meta, em, platform);
        if (viewDefinition) {
          schema.addView(
            meta.collection,
            this.getSchemaName(meta, config, schemaName),
            viewDefinition,
            meta.materialized,
            meta.withData,
          );
        }
        continue;
      }
      const table = schema.addTable(meta.collection, this.getSchemaName(meta, config, schemaName));
      table.comment = meta.comment;
      // For TPT child entities, only use ownProps (properties defined in this entity only)
      // For all other entities (including TPT root), use all props
      const propsToProcess =
        meta.inheritanceType === 'tpt' && meta.tptParent && meta.ownProps ? meta.ownProps : meta.props;
      for (const prop of propsToProcess) {
        if (!this.shouldHaveColumn(meta, prop, skipColumns)) {
          continue;
        }
        table.addColumnFromProperty(prop, meta, config);
      }
      // For TPT child entities, always include the PK columns (they form the FK to parent)
      if (meta.inheritanceType === 'tpt' && meta.tptParent) {
        const pkProps = meta.primaryKeys.map(pk => meta.properties[pk]);
        for (const pkProp of pkProps) {
          // Only add if not already added (it might be in ownProps if defined in this entity)
          if (!propsToProcess.includes(pkProp)) {
            table.addColumnFromProperty(pkProp, meta, config);
          }
          // Child PK must not be autoincrement — it references the parent PK via FK
          for (const field of pkProp.fieldNames) {
            const col = table.getColumn(field);
            if (col) {
              col.autoincrement = false;
            }
          }
        }
        // Add FK from child PK to parent PK with ON DELETE CASCADE
        this.addTPTForeignKey(table, meta, config, platform);
      }
      meta.indexes.forEach(index => table.addIndex(meta, index, 'index'));
      meta.uniques.forEach(index => table.addIndex(meta, index, 'unique'));
      // For TPT child entities, the PK is also defined here
      const pkPropsForIndex =
        meta.inheritanceType === 'tpt' && meta.tptParent
          ? meta.primaryKeys.map(pk => meta.properties[pk])
          : meta.props.filter(prop => prop.primary);
      table.addIndex(meta, { properties: pkPropsForIndex.map(prop => prop.name) }, 'primary');
      for (const check of meta.checks) {
        const columnName = check.property ? meta.properties[check.property].fieldNames[0] : undefined;
        const expression = isRaw(check.expression)
          ? platform.formatQuery(check.expression.sql, check.expression.params)
          : check.expression;
        table.addCheck({
          name: check.name,
          expression,
          definition: `check (${expression})`,
          columnName,
        });
      }
    }
    return schema;
  }
  static getViewDefinition(meta, em, platform) {
    if (typeof meta.expression === 'string') {
      return meta.expression;
    }
    // Expression is a function, need to evaluate it
    /* v8 ignore next */
    if (!em) {
      return undefined;
    }
    const result = meta.expression(em, {}, {});
    // Async expressions are not supported for view entities
    if (result && typeof result.then === 'function') {
      throw new Error(
        `View entity ${meta.className} expression returned a Promise. Async expressions are not supported for view entities.`,
      );
    }
    /* v8 ignore next */
    if (typeof result === 'string') {
      return result;
    }
    /* v8 ignore next */
    if (isRaw(result)) {
      return platform.formatQuery(result.sql, result.params);
    }
    // Check if it's a QueryBuilder (has getFormattedQuery method)
    if (result && typeof result.getFormattedQuery === 'function') {
      return result.getFormattedQuery();
    }
    /* v8 ignore next - fallback for unknown result types */
    return undefined;
  }
  static getSchemaName(meta, config, schema) {
    return (meta.schema === '*' ? schema : meta.schema) ?? config.get('schema');
  }
  /**
   * Add a foreign key from a TPT child entity's PK to its parent entity's PK.
   * This FK uses ON DELETE CASCADE to ensure child rows are deleted when parent is deleted.
   */
  static addTPTForeignKey(table, meta, config, platform) {
    const parent = meta.tptParent;
    const pkColumnNames = meta.primaryKeys.flatMap(pk => meta.properties[pk].fieldNames);
    const parentPkColumnNames = parent.primaryKeys.flatMap(pk => parent.properties[pk].fieldNames);
    // Determine the parent table name with schema
    const parentSchema =
      parent.schema === '*' ? undefined : (parent.schema ?? config.get('schema', platform.getDefaultSchemaName()));
    const parentTableName = parentSchema ? `${parentSchema}.${parent.tableName}` : parent.tableName;
    // Create FK constraint name
    const constraintName = platform.getIndexName(table.name, pkColumnNames, 'foreign');
    // Add the foreign key to the table
    const fks = table.getForeignKeys();
    fks[constraintName] = {
      constraintName,
      columnNames: pkColumnNames,
      localTableName: table.getShortestName(false),
      referencedColumnNames: parentPkColumnNames,
      referencedTableName: parentTableName,
      deleteRule: 'cascade', // TPT always uses cascade delete
      updateRule: 'cascade', // TPT always uses cascade update
    };
  }
  static matchName(name, nameToMatch) {
    return typeof nameToMatch === 'string'
      ? name.toLocaleLowerCase() === nameToMatch.toLocaleLowerCase()
      : nameToMatch.test(name);
  }
  static isNameAllowed(name, skipNames) {
    return !(skipNames?.some(pattern => this.matchName(name, pattern)) ?? false);
  }
  static isTableNameAllowed(tableName, takeTables, skipTables) {
    return (
      (takeTables?.some(tableNameToMatch => this.matchName(tableName, tableNameToMatch)) ?? true) &&
      this.isNameAllowed(tableName, skipTables)
    );
  }
  static shouldHaveColumn(meta, prop, skipColumns) {
    if (prop.persist === false || (prop.columnTypes?.length ?? 0) === 0) {
      return false;
    }
    // Check if column should be skipped
    if (skipColumns) {
      const tableName = meta.tableName;
      const tableSchema = meta.schema;
      const fullTableName = tableSchema ? `${tableSchema}.${tableName}` : tableName;
      // Check for skipColumns by table name or fully qualified table name
      const columnsToSkip = skipColumns[tableName] || skipColumns[fullTableName];
      if (columnsToSkip) {
        for (const fieldName of prop.fieldNames) {
          if (columnsToSkip.some(pattern => this.matchName(fieldName, pattern))) {
            return false;
          }
        }
      }
    }
    if (prop.kind === ReferenceKind.EMBEDDED && prop.object) {
      return true;
    }
    const getRootProperty = prop => (prop.embedded ? getRootProperty(meta.properties[prop.embedded[0]]) : prop);
    const rootProp = getRootProperty(prop);
    if (rootProp.kind === ReferenceKind.EMBEDDED) {
      return prop === rootProp || !rootProp.object;
    }
    return (
      [ReferenceKind.SCALAR, ReferenceKind.MANY_TO_ONE].includes(prop.kind) ||
      (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner)
    );
  }
  toJSON() {
    return {
      name: this.name,
      namespaces: [...this.#namespaces],
      tables: this.#tables,
      views: this.#views,
      nativeEnums: this.#nativeEnums,
    };
  }
  prune(schema, wildcardSchemaTables) {
    const hasWildcardSchema = wildcardSchemaTables.length > 0;
    this.#tables = this.#tables.filter(table => {
      return (
        (!schema && !hasWildcardSchema) || // no schema specified and we don't have any multi-schema entity
        table.schema === schema || // specified schema matches the table's one
        (!schema && !wildcardSchemaTables.includes(table.name))
      ); // no schema specified and the table has fixed one provided
    });
    this.#views = this.#views.filter(view => {
      /* v8 ignore next */
      return (
        (!schema && !hasWildcardSchema) ||
        view.schema === schema ||
        (!schema && !wildcardSchemaTables.includes(view.name))
      );
    });
    // remove namespaces of ignored tables and views
    for (const ns of this.#namespaces) {
      if (
        !this.#tables.some(t => t.schema === ns) &&
        !this.#views.some(v => v.schema === ns) &&
        !Object.values(this.#nativeEnums).some(e => e.schema === ns)
      ) {
        this.#namespaces.delete(ns);
      }
    }
  }
}
