import { DeferMode, EnumType, Type, Utils } from '@mikro-orm/core';
import { SchemaHelper } from '../../schema/SchemaHelper.js';
/** PostGIS system views that should be automatically ignored */
const POSTGIS_VIEWS = ['geography_columns', 'geometry_columns'];
export class PostgreSqlSchemaHelper extends SchemaHelper {
  static DEFAULT_VALUES = {
    'now()': ['now()', 'current_timestamp'],
    'current_timestamp(?)': ['current_timestamp(?)'],
    "('now'::text)::timestamp(?) with time zone": ['current_timestamp(?)'],
    "('now'::text)::timestamp(?) without time zone": ['current_timestamp(?)'],
    'null::character varying': ['null'],
    'null::timestamp with time zone': ['null'],
    'null::timestamp without time zone': ['null'],
  };
  getSchemaBeginning(charset, disableForeignKeys) {
    if (disableForeignKeys) {
      return `set names '${charset}';\n${this.disableForeignKeysSQL()}\n\n`;
    }
    return `set names '${charset}';\n\n`;
  }
  getCreateDatabaseSQL(name) {
    return `create database ${this.quote(name)}`;
  }
  getListTablesSQL() {
    return (
      `select table_name, table_schema as schema_name, ` +
      `(select pg_catalog.obj_description(c.oid) from pg_catalog.pg_class c
          where c.oid = (select ('"' || table_schema || '"."' || table_name || '"')::regclass::oid) and c.relname = table_name) as table_comment ` +
      `from information_schema.tables ` +
      `where ${this.getIgnoredNamespacesConditionSQL('table_schema')} ` +
      `and table_name != 'geometry_columns' and table_name != 'spatial_ref_sys' and table_type != 'VIEW' ` +
      `and table_name not in (select inhrelid::regclass::text from pg_inherits) ` +
      `order by table_name`
    );
  }
  getIgnoredViewsCondition() {
    return POSTGIS_VIEWS.map(v => `table_name != '${v}'`).join(' and ');
  }
  getListViewsSQL() {
    return (
      `select table_name as view_name, table_schema as schema_name, view_definition ` +
      `from information_schema.views ` +
      `where ${this.getIgnoredNamespacesConditionSQL('table_schema')} ` +
      `and ${this.getIgnoredViewsCondition()} ` +
      `order by table_name`
    );
  }
  async loadViews(schema, connection) {
    const views = await connection.execute(this.getListViewsSQL());
    for (const view of views) {
      const definition = view.view_definition?.trim().replace(/;$/, '') ?? '';
      if (definition) {
        schema.addView(view.view_name, view.schema_name, definition);
      }
    }
  }
  getListMaterializedViewsSQL() {
    return (
      `select matviewname as view_name, schemaname as schema_name, definition as view_definition ` +
      `from pg_matviews ` +
      `where ${this.getIgnoredNamespacesConditionSQL('schemaname')} ` +
      `order by matviewname`
    );
  }
  async loadMaterializedViews(schema, connection, schemaName) {
    const views = await connection.execute(this.getListMaterializedViewsSQL());
    for (const view of views) {
      const definition = view.view_definition?.trim().replace(/;$/, '') ?? '';
      if (definition) {
        schema.addView(view.view_name, view.schema_name, definition, true);
      }
    }
  }
  createMaterializedView(name, schema, definition, withData = true) {
    const viewName = this.quote(this.getTableName(name, schema));
    const dataClause = withData ? ' with data' : ' with no data';
    return `create materialized view ${viewName} as ${definition}${dataClause}`;
  }
  dropMaterializedViewIfExists(name, schema) {
    return `drop materialized view if exists ${this.quote(this.getTableName(name, schema))} cascade`;
  }
  refreshMaterializedView(name, schema, concurrently = false) {
    const concurrent = concurrently ? ' concurrently' : '';
    return `refresh materialized view${concurrent} ${this.quote(this.getTableName(name, schema))}`;
  }
  async getNamespaces(connection) {
    const sql =
      `select schema_name from information_schema.schemata ` +
      `where ${this.getIgnoredNamespacesConditionSQL()} ` +
      `order by schema_name`;
    const res = await connection.execute(sql);
    return res.map(row => row.schema_name);
  }
  getIgnoredNamespacesConditionSQL(column = 'schema_name') {
    const ignored = [
      'information_schema',
      'tiger',
      'topology',
      /* v8 ignore next */
      ...(this.platform.getConfig().get('schemaGenerator').ignoreSchema ?? []),
    ]
      .map(s => this.platform.quoteValue(s))
      .join(', ');
    const ignoredPrefixes = ['pg_', 'crdb_', '_timescaledb_'].map(p => `"${column}" not like '${p}%'`).join(' and ');
    return `${ignoredPrefixes} and "${column}" not in (${ignored})`;
  }
  async loadInformationSchema(schema, connection, tables, schemas) {
    schemas ??= tables.length === 0 ? [schema.name] : tables.map(t => t.schema_name);
    const nativeEnums = await this.getNativeEnumDefinitions(connection, schemas);
    schema.setNativeEnums(nativeEnums);
    if (tables.length === 0) {
      return;
    }
    const tablesBySchema = this.getTablesGroupedBySchemas(tables);
    const columns = await this.getAllColumns(connection, tablesBySchema, nativeEnums);
    const indexes = await this.getAllIndexes(connection, tables);
    const checks = await this.getAllChecks(connection, tablesBySchema);
    const fks = await this.getAllForeignKeys(connection, tablesBySchema);
    for (const t of tables) {
      const key = this.getTableKey(t);
      const table = schema.addTable(t.table_name, t.schema_name, t.table_comment);
      const pks = await this.getPrimaryKeys(connection, indexes[key], table.name, table.schema);
      const enums = this.getEnumDefinitions(checks[key] ?? []);
      if (columns[key]) {
        table.init(columns[key], indexes[key], checks[key], pks, fks[key], enums);
      }
    }
  }
  async getAllIndexes(connection, tables) {
    const sql = this.getIndexesSQL(tables);
    const unquote = str => str.replace(/['"`]/g, '');
    const allIndexes = await connection.execute(sql);
    const ret = {};
    for (const index of allIndexes) {
      const key = this.getTableKey(index);
      // Extract INCLUDE columns from expression first, to filter them from key columns
      const includeMatch = index.expression?.match(/include\s*\(([^)]+)\)/i);
      const includeColumns = includeMatch ? includeMatch[1].split(',').map(col => unquote(col.trim())) : [];
      // Filter out INCLUDE columns from the column definitions to get only key columns
      const keyColumnDefs = index.index_def.filter(col => !includeColumns.includes(unquote(col)));
      // Parse sort order and NULLS ordering from the full expression
      // pg_get_indexdef individual columns don't include sort modifiers, so we parse from full expression
      const columns = this.parseIndexColumnsFromExpression(index.expression, keyColumnDefs, unquote);
      const columnNames = columns.map(col => col.name);
      const hasAdvancedColumnOptions = columns.some(col => col.sort || col.nulls || col.collation);
      const indexDef = {
        columnNames,
        composite: columnNames.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: index.contype === 'u',
        keyName: index.constraint_name,
        unique: index.unique,
        primary: index.primary,
      };
      // Add columns array if there are advanced options
      if (hasAdvancedColumnOptions) {
        indexDef.columns = columns;
      }
      if (index.condeferrable) {
        indexDef.deferMode = index.condeferred ? DeferMode.INITIALLY_DEFERRED : DeferMode.INITIALLY_IMMEDIATE;
      }
      if (index.index_def.some(col => /[(): ,"'`]/.exec(col)) || index.expression?.match(/ where /i)) {
        indexDef.expression = index.expression;
      }
      if (index.deferrable) {
        indexDef.deferMode = index.initially_deferred ? DeferMode.INITIALLY_DEFERRED : DeferMode.INITIALLY_IMMEDIATE;
      }
      // Extract fillFactor from reloptions
      if (index.reloptions) {
        const fillFactorMatch = index.reloptions.find(opt => opt.startsWith('fillfactor='));
        if (fillFactorMatch) {
          indexDef.fillFactor = parseInt(fillFactorMatch.split('=')[1], 10);
        }
      }
      // Add INCLUDE columns (already extracted above)
      if (includeColumns.length > 0) {
        indexDef.include = includeColumns;
      }
      // Add index type if not btree (the default)
      if (index.index_type && index.index_type !== 'btree') {
        indexDef.type = index.index_type;
      }
      ret[key] ??= [];
      ret[key].push(indexDef);
    }
    return ret;
  }
  /**
   * Parses column definitions from the full CREATE INDEX expression.
   * Since pg_get_indexdef(oid, col_num, true) doesn't include sort modifiers,
   * we extract them from the full expression instead.
   *
   * We use columnDefs (from individual pg_get_indexdef calls) as the source
   * of column names, and find their modifiers in the expression.
   */
  parseIndexColumnsFromExpression(expression, columnDefs, unquote) {
    // Extract just the column list from the expression (between first parens after USING)
    // Pattern: ... USING method (...columns...) [INCLUDE (...)] [WHERE ...]
    // Note: pg_get_indexdef always returns a valid expression with USING clause
    const usingMatch = /using\s+\w+\s*\(/i.exec(expression);
    const startIdx = usingMatch.index + usingMatch[0].length - 1; // Position of opening (
    const columnsStr = this.extractParenthesizedContent(expression, startIdx);
    // Use the column names from columnDefs and find their modifiers in the expression
    return columnDefs.map(colDef => {
      const name = unquote(colDef);
      const result = { name };
      // Find this column in the expression and extract modifiers
      // Create a pattern that matches the column name (quoted or unquoted) followed by modifiers
      const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
      const colPattern = new RegExp(`"?${escapedName}"?\\s*([^,)]*?)(?:,|$)`, 'i');
      const colMatch = columnsStr.match(colPattern);
      if (colMatch) {
        const modifiers = colMatch[1];
        // Extract sort order (PostgreSQL omits ASC in output as it's the default)
        if (/\bdesc\b/i.test(modifiers)) {
          result.sort = 'DESC';
        }
        // Extract NULLS ordering
        const nullsMatch = /nulls\s+(first|last)/i.exec(modifiers);
        if (nullsMatch) {
          result.nulls = nullsMatch[1].toUpperCase();
        }
        // Extract collation
        const collateMatch = /collate\s+"?([^"\s,)]+)"?/i.exec(modifiers);
        if (collateMatch) {
          result.collation = collateMatch[1];
        }
      }
      return result;
    });
  }
  /**
   * Extracts the content inside parentheses starting at the given position.
   * Handles nested parentheses correctly.
   */
  extractParenthesizedContent(str, startIdx) {
    let depth = 0;
    const start = startIdx + 1;
    for (let i = startIdx; i < str.length; i++) {
      if (str[i] === '(') {
        depth++;
      } else if (str[i] === ')') {
        depth--;
        if (depth === 0) {
          return str.slice(start, i);
        }
      }
    }
    /* v8 ignore next - pg_get_indexdef always returns balanced parentheses */
    return '';
  }
  async getAllColumns(connection, tablesBySchemas, nativeEnums) {
    const sql = `select table_schema as schema_name, table_name, column_name,
      column_default,
      is_nullable,
      udt_name,
      udt_schema,
      coalesce(datetime_precision, character_maximum_length) length,
      atttypmod custom_length,
      numeric_precision,
      numeric_scale,
      data_type,
      is_identity,
      identity_generation,
      generation_expression,
      pg_catalog.col_description(pgc.oid, cols.ordinal_position::int) column_comment
      from information_schema.columns cols
      join pg_class pgc on cols.table_name = pgc.relname
      join pg_attribute pga on pgc.oid = pga.attrelid and cols.column_name = pga.attname
      where (${[...tablesBySchemas.entries()].map(([schema, tables]) => `(table_schema = ${this.platform.quoteValue(schema)} and table_name in (${tables.map(t => this.platform.quoteValue(t.table_name)).join(',')}))`).join(' or ')})
      order by ordinal_position`;
    const allColumns = await connection.execute(sql);
    const str = val => (val != null ? '' + val : val);
    const ret = {};
    for (const col of allColumns) {
      const mappedType = connection.getPlatform().getMappedType(col.data_type);
      const increments =
        (col.column_default?.includes('nextval') || col.is_identity === 'YES') &&
        connection.getPlatform().isNumericColumn(mappedType);
      const key = this.getTableKey(col);
      ret[key] ??= [];
      let type = col.data_type.toLowerCase() === 'array' ? col.udt_name.replace(/^_(.*)$/, '$1[]') : col.udt_name;
      if (
        col.data_type === 'USER-DEFINED' &&
        col.udt_schema &&
        col.udt_schema !== this.platform.getDefaultSchemaName()
      ) {
        type = `${col.udt_schema}.${type}`;
      }
      if (type === 'bpchar') {
        type = 'char';
      }
      if (type === 'vector' && col.length == null && col.custom_length != null && col.custom_length !== -1) {
        col.length = col.custom_length;
      }
      if (col.length != null && !type.endsWith(`(${col.length})`) && !['text', 'date'].includes(type)) {
        type += `(${col.length})`;
      }
      if (type === 'numeric' && col.numeric_precision != null && col.numeric_scale != null) {
        type += `(${col.numeric_precision},${col.numeric_scale})`;
      }
      const length = this.inferLengthFromColumnType(type) === -1 ? -1 : col.length;
      const column = {
        name: col.column_name,
        type,
        mappedType,
        length,
        precision: col.numeric_precision,
        scale: col.numeric_scale,
        nullable: col.is_nullable === 'YES',
        default: str(this.normalizeDefaultValue(col.column_default, col.length)),
        unsigned: increments,
        autoincrement: increments,
        generated:
          col.is_identity === 'YES'
            ? col.identity_generation === 'BY DEFAULT'
              ? 'by default as identity'
              : 'identity'
            : col.generation_expression
              ? col.generation_expression + ' stored'
              : undefined,
        comment: col.column_comment,
      };
      if (nativeEnums?.[column.type]) {
        column.mappedType = Type.getType(EnumType);
        column.nativeEnumName = column.type;
        column.enumItems = nativeEnums[column.type]?.items;
      }
      ret[key].push(column);
    }
    return ret;
  }
  async getAllChecks(connection, tablesBySchemas) {
    const sql = this.getChecksSQL(tablesBySchemas);
    const allChecks = await connection.execute(sql);
    const ret = {};
    const seen = new Set();
    for (const check of allChecks) {
      const key = this.getTableKey(check);
      const dedupeKey = `${key}:${check.name}`;
      if (seen.has(dedupeKey)) {
        continue;
      }
      seen.add(dedupeKey);
      ret[key] ??= [];
      const m = /^check \(\((.*)\)\)$/is.exec(check.expression);
      const def = m?.[1].replace(/\((.*?)\)::\w+/g, '$1');
      ret[key].push({
        name: check.name,
        columnName: check.column_name,
        definition: check.expression,
        expression: def,
      });
    }
    return ret;
  }
  async getAllForeignKeys(connection, tablesBySchemas) {
    const sql = `select nsp1.nspname schema_name, cls1.relname table_name, nsp2.nspname referenced_schema_name,
      cls2.relname referenced_table_name, a.attname column_name, af.attname referenced_column_name, conname constraint_name,
      confupdtype update_rule, confdeltype delete_rule, array_position(con.conkey,a.attnum) as ord, condeferrable, condeferred,
      pg_get_constraintdef(con.oid) as constraint_def
      from pg_attribute a
      join pg_constraint con on con.conrelid = a.attrelid AND a.attnum = ANY (con.conkey)
      join pg_attribute af on af.attnum = con.confkey[array_position(con.conkey,a.attnum)] AND af.attrelid = con.confrelid
      join pg_namespace nsp1 on nsp1.oid = con.connamespace
      join pg_class cls1 on cls1.oid = con.conrelid
      join pg_class cls2 on cls2.oid = confrelid
      join pg_namespace nsp2 on nsp2.oid = cls2.relnamespace
      where (${[...tablesBySchemas.entries()].map(([schema, tables]) => `(cls1.relname in (${tables.map(t => this.platform.quoteValue(t.table_name)).join(',')}) and nsp1.nspname = ${this.platform.quoteValue(schema)})`).join(' or ')})
      and confrelid > 0
      order by nsp1.nspname, cls1.relname, constraint_name, ord`;
    const allFks = await connection.execute(sql);
    const ret = {};
    function mapReferentialIntegrity(value, def) {
      const match = ['n', 'd'].includes(value) && /ON DELETE (SET (NULL|DEFAULT) \(.*?\))/.exec(def);
      if (match) {
        return match[1];
      }
      /* v8 ignore next */
      switch (value) {
        case 'r':
          return 'RESTRICT';
        case 'c':
          return 'CASCADE';
        case 'n':
          return 'SET NULL';
        case 'd':
          return 'SET DEFAULT';
        case 'a':
        default:
          return 'NO ACTION';
      }
    }
    for (const fk of allFks) {
      fk.update_rule = mapReferentialIntegrity(fk.update_rule, fk.constraint_def);
      fk.delete_rule = mapReferentialIntegrity(fk.delete_rule, fk.constraint_def);
      if (fk.condeferrable) {
        fk.defer_mode = fk.condeferred ? DeferMode.INITIALLY_DEFERRED : DeferMode.INITIALLY_IMMEDIATE;
      }
      const key = this.getTableKey(fk);
      ret[key] ??= [];
      ret[key].push(fk);
    }
    Object.keys(ret).forEach(key => {
      const [schemaName, tableName] = key.split('.');
      ret[key] = this.mapForeignKeys(ret[key], tableName, schemaName);
    });
    return ret;
  }
  async getNativeEnumDefinitions(connection, schemas) {
    const uniqueSchemas = Utils.unique(schemas);
    const res = await connection.execute(
      `select t.typname as enum_name, n.nspname as schema_name, array_agg(e.enumlabel order by e.enumsortorder) as enum_value
        from pg_type t
        join pg_enum e on t.oid = e.enumtypid
        join pg_catalog.pg_namespace n on n.oid = t.typnamespace
        where n.nspname in (${Array(uniqueSchemas.length).fill('?').join(', ')})
        group by t.typname, n.nspname`,
      uniqueSchemas,
    );
    return res.reduce((o, row) => {
      let name = row.enum_name;
      if (row.schema_name && row.schema_name !== this.platform.getDefaultSchemaName()) {
        name = row.schema_name + '.' + name;
      }
      let items = row.enum_value;
      if (!Array.isArray(items)) {
        items = this.platform.unmarshallArray(row.enum_value);
      }
      o[name] = {
        name: row.enum_name,
        schema: row.schema_name,
        items,
      };
      return o;
    }, {});
  }
  getCreateNativeEnumSQL(name, values, schema) {
    if (schema && schema !== this.platform.getDefaultSchemaName()) {
      name = schema + '.' + name;
    }
    return `create type ${this.quote(name)} as enum (${values.map(value => this.platform.quoteValue(value)).join(', ')})`;
  }
  getDropNativeEnumSQL(name, schema) {
    if (schema && schema !== this.platform.getDefaultSchemaName()) {
      name = schema + '.' + name;
    }
    return `drop type ${this.quote(name)}`;
  }
  getAlterNativeEnumSQL(name, schema, value, items, oldItems) {
    if (schema && schema !== this.platform.getDefaultSchemaName()) {
      name = schema + '.' + name;
    }
    let suffix = '';
    if (items && value && oldItems) {
      const position = items.indexOf(value);
      if (position > 0) {
        suffix = ` after ${this.platform.quoteValue(items[position - 1])}`;
      } else if (items.length > 1 && oldItems.length > 0) {
        suffix = ` before ${this.platform.quoteValue(oldItems[0])}`;
      }
    }
    return `alter type ${this.quote(name)} add value if not exists ${this.platform.quoteValue(value)}${suffix}`;
  }
  getEnumDefinitions(checks) {
    return checks.reduce((o, item) => {
      // check constraints are defined as one of:
      // `CHECK ((type = ANY (ARRAY['local'::text, 'global'::text])))`
      // `CHECK (("columnName" = ANY (ARRAY['local'::text, 'global'::text])))`
      // `CHECK (((enum_test)::text = ANY ((ARRAY['a'::character varying, 'b'::character varying, 'c'::character varying])::text[])))`
      // `CHECK ((("enumTest")::text = ANY ((ARRAY['a'::character varying, 'b'::character varying, 'c'::character varying])::text[])))`
      // `CHECK ((type = 'a'::text))`
      const m1 =
        item.definition?.match(/check \(\(\("?(\w+)"?\)::/i) || item.definition?.match(/check \(\("?(\w+)"? = /i);
      const m2 = item.definition?.match(/\(array\[(.*)]\)/i) || item.definition?.match(/ = (.*)\)/i);
      if (item.columnName && m1 && m2) {
        const m3 = m2[1].match(/('[^']*'::text)/g);
        let items;
        /* v8 ignore next */
        if (m3) {
          items = m3.map(item => /^\(?'(.*)'/.exec(item.trim())?.[1]);
        } else {
          items = m2[1].split(',').map(item => /^\(?'(.*)'/.exec(item.trim())?.[1]);
        }
        items = items.filter(item => item !== undefined);
        if (items.length > 0) {
          o[item.columnName] = items;
          item.expression = `${this.quote(item.columnName)} in ('${items.join("', '")}')`;
          item.definition = `check (${item.expression})`;
        }
      }
      return o;
    }, {});
  }
  createTableColumn(column, table) {
    const pk = table.getPrimaryKey();
    const compositePK = pk?.composite;
    const primaryKey = !this.hasNonDefaultPrimaryKeyName(table);
    const col = [this.quote(column.name)];
    if (column.autoincrement && !column.generated && !compositePK) {
      col.push(column.mappedType.getColumnType({ autoincrement: true }, this.platform));
    } else {
      let columnType = column.type;
      if (column.nativeEnumName) {
        const parts = column.type.split('.');
        if (parts.length === 2 && parts[0] === '*') {
          columnType = `${table.schema}.${parts[1]}`;
        }
        if (columnType.endsWith('[]')) {
          columnType = this.quote(columnType.substring(0, columnType.length - 2)) + '[]';
        } else {
          columnType = this.quote(columnType);
        }
      }
      if (column.generated === 'by default as identity') {
        columnType += ` generated ${column.generated}`;
      } else if (column.generated) {
        columnType += ` generated always as ${column.generated}`;
      }
      col.push(columnType);
      Utils.runIfNotEmpty(() => col.push('null'), column.nullable);
      Utils.runIfNotEmpty(() => col.push('not null'), !column.nullable);
    }
    if (column.autoincrement && !compositePK) {
      Utils.runIfNotEmpty(() => col.push('primary key'), primaryKey && column.primary);
    }
    const useDefault = column.default != null && column.default !== 'null' && !column.autoincrement;
    Utils.runIfNotEmpty(() => col.push(`default ${column.default}`), useDefault);
    return col.join(' ');
  }
  getPreAlterTable(tableDiff, safe) {
    const ret = [];
    const parts = tableDiff.name.split('.');
    const tableName = parts.pop();
    const schemaName = parts.pop();
    /* v8 ignore next */
    const name =
      (schemaName && schemaName !== this.platform.getDefaultSchemaName() ? schemaName + '.' : '') + tableName;
    const quotedName = this.quote(name);
    // detect that the column was an enum before and remove the check constraint in such case here
    const changedEnums = Object.values(tableDiff.changedColumns).filter(
      col => col.fromColumn.mappedType instanceof EnumType,
    );
    for (const col of changedEnums) {
      if (!col.fromColumn.nativeEnumName && col.column.nativeEnumName && col.fromColumn.default) {
        ret.push(`alter table ${quotedName} alter column "${col.column.name}" drop default`);
      }
      if (col.fromColumn.nativeEnumName && !col.column.nativeEnumName && col.fromColumn.default) {
        ret.push(`alter table ${quotedName} alter column "${col.column.name}" drop default`);
      }
    }
    // changing uuid column type requires to cast it to text first
    const uuids = Object.values(tableDiff.changedColumns).filter(
      col => col.changedProperties.has('type') && col.fromColumn.type === 'uuid',
    );
    for (const col of uuids) {
      ret.push(
        `alter table ${quotedName} alter column "${col.column.name}" type text using ("${col.column.name}"::text)`,
      );
    }
    for (const { column } of Object.values(tableDiff.changedColumns).filter(diff =>
      diff.changedProperties.has('autoincrement'),
    )) {
      if (!column.autoincrement && column.default == null) {
        ret.push(`alter table ${quotedName} alter column ${this.quote(column.name)} drop default`);
      }
    }
    return ret;
  }
  castColumn(name, type) {
    if (type === 'uuid') {
      type = 'text::uuid';
    }
    return ` using (${this.quote(name)}::${type})`;
  }
  dropForeignKey(tableName, constraintName) {
    return `alter table ${this.quote(tableName)} drop constraint ${this.quote(constraintName)}`;
  }
  getPostAlterTable(tableDiff, safe) {
    const ret = [];
    const parts = tableDiff.name.split('.');
    const tableName = parts.pop();
    const schemaName = parts.pop();
    /* v8 ignore next */
    const name =
      (schemaName && schemaName !== this.platform.getDefaultSchemaName() ? schemaName + '.' : '') + tableName;
    const quotedName = this.quote(name);
    // detect that the column was an enum before and remove the check constraint in such a case here
    const changedEnums = Object.values(tableDiff.changedColumns).filter(
      col => col.fromColumn.mappedType instanceof EnumType,
    );
    for (const col of changedEnums) {
      if (!col.fromColumn.nativeEnumName && col.column.nativeEnumName && col.column.default) {
        ret.push(`alter table ${quotedName} alter column "${col.column.name}" set default ${col.column.default}`);
      }
      if (col.fromColumn.nativeEnumName && !col.column.nativeEnumName && col.column.default) {
        ret.push(`alter table ${quotedName} alter column "${col.column.name}" set default ${col.column.default}`);
      }
    }
    for (const { column } of Object.values(tableDiff.changedColumns).filter(diff =>
      diff.changedProperties.has('autoincrement'),
    )) {
      ret.push(...this.getAlterColumnAutoincrement(tableName, column, schemaName));
    }
    return ret;
  }
  getAlterColumnAutoincrement(tableName, column, schemaName) {
    const ret = [];
    /* v8 ignore next */
    const name =
      (schemaName && schemaName !== this.platform.getDefaultSchemaName() ? schemaName + '.' : '') + tableName;
    if (column.autoincrement) {
      const seqName = this.platform.getIndexName(tableName, [column.name], 'sequence');
      ret.push(`create sequence if not exists ${this.quote(seqName)}`);
      ret.push(`select setval('${seqName}', (select max(${this.quote(column.name)}) from ${this.quote(name)}))`);
      ret.push(
        `alter table ${this.quote(name)} alter column ${this.quote(column.name)} set default nextval('${seqName}')`,
      );
    }
    return ret;
  }
  getChangeColumnCommentSQL(tableName, to, schemaName) {
    const name = this.quote(
      (schemaName && schemaName !== this.platform.getDefaultSchemaName() ? schemaName + '.' : '') + tableName,
    );
    const value = to.comment ? this.platform.quoteValue(to.comment) : 'null';
    return `comment on column ${name}.${this.quote(to.name)} is ${value}`;
  }
  alterTableComment(table, comment) {
    return `comment on table ${table.getQuotedName()} is ${this.platform.quoteValue(comment ?? '')}`;
  }
  normalizeDefaultValue(defaultValue, length) {
    if (!defaultValue || typeof defaultValue !== 'string') {
      return super.normalizeDefaultValue(defaultValue, length, PostgreSqlSchemaHelper.DEFAULT_VALUES);
    }
    const match = /^'(.*)'::(.*)$/.exec(defaultValue);
    if (match) {
      if (match[2] === 'integer') {
        return +match[1];
      }
      return `'${match[1]}'`;
    }
    return super.normalizeDefaultValue(defaultValue, length, PostgreSqlSchemaHelper.DEFAULT_VALUES);
  }
  appendComments(table) {
    const sql = [];
    if (table.comment) {
      const comment = this.platform.quoteValue(this.processComment(table.comment));
      sql.push(`comment on table ${table.getQuotedName()} is ${comment}`);
    }
    for (const column of table.getColumns()) {
      if (column.comment) {
        const comment = this.platform.quoteValue(this.processComment(column.comment));
        sql.push(`comment on column ${table.getQuotedName()}.${this.quote(column.name)} is ${comment}`);
      }
    }
    return sql;
  }
  getDatabaseExistsSQL(name) {
    return `select 1 from pg_database where datname = '${name}'`;
  }
  getDatabaseNotExistsError(dbName) {
    return `database ${this.quote(dbName)} does not exist`;
  }
  getManagementDbName() {
    return this.platform.getConfig().get('schemaGenerator', {}).managementDbName ?? 'postgres';
  }
  disableForeignKeysSQL() {
    return `set session_replication_role = 'replica';`;
  }
  enableForeignKeysSQL() {
    return `set session_replication_role = 'origin';`;
  }
  getRenameIndexSQL(tableName, index, oldIndexName) {
    oldIndexName = this.quote(oldIndexName);
    const keyName = this.quote(index.keyName);
    return [`alter index ${oldIndexName} rename to ${keyName}`];
  }
  dropIndex(table, index, oldIndexName = index.keyName) {
    if (index.primary || (index.unique && index.constraint)) {
      return `alter table ${this.quote(table)} drop constraint ${this.quote(oldIndexName)}`;
    }
    return `drop index ${this.quote(oldIndexName)}`;
  }
  /**
   * Build the column list for a PostgreSQL index.
   */
  getIndexColumns(index) {
    if (index.columns?.length) {
      return index.columns
        .map(col => {
          let colDef = this.quote(col.name);
          // PostgreSQL supports collation with double quotes
          if (col.collation) {
            colDef += ` collate ${this.quote(col.collation)}`;
          }
          // PostgreSQL supports sort order
          if (col.sort) {
            colDef += ` ${col.sort}`;
          }
          // PostgreSQL supports NULLS FIRST/LAST
          if (col.nulls) {
            colDef += ` nulls ${col.nulls}`;
          }
          return colDef;
        })
        .join(', ');
    }
    return index.columnNames.map(c => this.quote(c)).join(', ');
  }
  /**
   * PostgreSQL-specific index options like fill factor.
   */
  getCreateIndexSuffix(index) {
    const withOptions = [];
    if (index.fillFactor != null) {
      withOptions.push(`fillfactor = ${index.fillFactor}`);
    }
    if (withOptions.length > 0) {
      return ` with (${withOptions.join(', ')})`;
    }
    return super.getCreateIndexSuffix(index);
  }
  getIndexesSQL(tables) {
    return `select indrelid::regclass as table_name, ns.nspname as schema_name, relname as constraint_name, idx.indisunique as unique, idx.indisprimary as primary, contype, condeferrable, condeferred,
      array(
        select pg_get_indexdef(idx.indexrelid, k + 1, true)
        from generate_subscripts(idx.indkey, 1) as k
        order by k
      ) as index_def,
      pg_get_indexdef(idx.indexrelid) as expression,
      c.condeferrable as deferrable,
      c.condeferred as initially_deferred,
      i.reloptions,
      am.amname as index_type
      from pg_index idx
      join pg_class as i on i.oid = idx.indexrelid
      join pg_namespace as ns on i.relnamespace = ns.oid
      join pg_am as am on am.oid = i.relam
      left join pg_constraint as c on c.conname = i.relname
      where indrelid in (${tables.map(t => `${this.platform.quoteValue(`${this.quote(t.schema_name)}.${this.quote(t.table_name)}`)}::regclass`).join(', ')})
      order by relname`;
  }
  getChecksSQL(tablesBySchemas) {
    return `select ccu.table_name as table_name, ccu.table_schema as schema_name, pgc.conname as name, conrelid::regclass as table_from, ccu.column_name as column_name, pg_get_constraintdef(pgc.oid) as expression
      from pg_constraint pgc
      join pg_namespace nsp on nsp.oid = pgc.connamespace
      join pg_class cls on pgc.conrelid = cls.oid
      join information_schema.constraint_column_usage ccu on pgc.conname = ccu.constraint_name and nsp.nspname = ccu.constraint_schema and cls.relname = ccu.table_name
      where contype = 'c' and (${[...tablesBySchemas.entries()].map(([schema, tables]) => `ccu.table_name in (${tables.map(t => this.platform.quoteValue(t.table_name)).join(',')}) and ccu.table_schema = ${this.platform.quoteValue(schema)}`).join(' or ')})
      order by pgc.conname`;
  }
  inferLengthFromColumnType(type) {
    const match = /^(\w+(?:\s+\w+)*)\s*(?:\(\s*(\d+)\s*\)|$)/.exec(type);
    if (!match) {
      return;
    }
    if (!match[2]) {
      switch (match[1]) {
        case 'character varying':
        case 'varchar':
        case 'bpchar':
        case 'char':
        case 'character':
          return -1;
        case 'interval':
        case 'time':
        case 'timestamp':
        case 'timestamptz':
          return this.platform.getDefaultDateTimeLength();
      }
      return;
    }
    return +match[2];
  }
}
