import { clone } from '../utils/clone.js';
import { EntityRepository } from '../entity/EntityRepository.js';
import { UnderscoreNamingStrategy } from '../naming-strategy/UnderscoreNamingStrategy.js';
import { ExceptionConverter } from './ExceptionConverter.js';
import {
  ArrayType,
  BigIntType,
  BlobType,
  BooleanType,
  CharacterType,
  DateTimeType,
  DateType,
  DecimalType,
  DoubleType,
  EnumType,
  FloatType,
  IntegerType,
  IntervalType,
  JsonType,
  MediumIntType,
  SmallIntType,
  StringType,
  TextType,
  TimeType,
  TinyIntType,
  Type,
  Uint8ArrayType,
  UnknownType,
  UuidType,
} from '../types/index.js';
import { parseJsonSafe, Utils } from '../utils/Utils.js';
import { ReferenceKind } from '../enums.js';
import { Raw } from '../utils/RawQueryFragment.js';
/** Symbol used to tag cloned embeddable data for JSON serialization handling. */
export const JsonProperty = Symbol('JsonProperty');
/** Abstract base class providing database-specific behavior and SQL dialect differences. */
export class Platform {
  exceptionConverter = new ExceptionConverter();
  config;
  namingStrategy;
  timezone;
  /** Whether this driver uses pivot tables for M:N relations (SQL drivers do, MongoDB does not). */
  usesPivotTable() {
    return false;
  }
  /** Whether this driver supports database transactions. */
  supportsTransactions() {
    return !this.config.get('disableTransactions');
  }
  /** Whether the driver wraps operations in implicit transactions by default. */
  usesImplicitTransactions() {
    return true;
  }
  /** Returns the default naming strategy constructor for this platform. */
  getNamingStrategy() {
    return UnderscoreNamingStrategy;
  }
  /** Whether the driver supports RETURNING clause (e.g. PostgreSQL). */
  usesReturningStatement() {
    return false;
  }
  /** Whether the driver supports OUTPUT clause (e.g. MSSQL). */
  usesOutputStatement() {
    return false;
  }
  /** Whether DELETE statements require explicit CASCADE keyword. */
  usesCascadeStatement() {
    return false;
  }
  /** for postgres native enums */
  supportsNativeEnums() {
    return false;
  }
  /** for postgres text enums (default) */
  usesEnumCheckConstraints() {
    return false;
  }
  /** Whether this platform supports materialized views. */
  supportsMaterializedViews() {
    return false;
  }
  /** Returns the schema helper instance for this platform, or undefined if not supported. */
  getSchemaHelper() {
    return undefined;
  }
  /** Whether the platform automatically creates indexes on foreign key columns. */
  indexForeignKeys() {
    return false;
  }
  /**
   * Whether or not the driver supports retuning list of created PKs back when multi-inserting
   */
  usesBatchInserts() {
    return true;
  }
  /**
   * Whether or not the driver supports updating many records at once
   */
  usesBatchUpdates() {
    return true;
  }
  /** Whether the platform supports the DEFAULT keyword in INSERT statements. */
  usesDefaultKeyword() {
    return true;
  }
  /**
   * Normalizes primary key wrapper to scalar value (e.g. mongodb's ObjectId to string)
   */
  normalizePrimaryKey(data) {
    return data;
  }
  /**
   * Converts scalar primary key representation to native driver wrapper (e.g. string to mongodb's ObjectId)
   */
  denormalizePrimaryKey(data) {
    return data;
  }
  /**
   * Returns the SQL specific for the platform to get the current timestamp
   */
  getCurrentTimestampSQL(length) {
    return 'current_timestamp' + (length ? `(${length})` : '');
  }
  /** Returns the SQL type declaration for datetime columns. */
  getDateTimeTypeDeclarationSQL(column) {
    return 'datetime' + (column.length ? `(${column.length})` : '');
  }
  /** Returns the default fractional seconds precision for datetime columns. */
  getDefaultDateTimeLength() {
    return 0;
  }
  /** Returns the default length for varchar columns. */
  getDefaultVarcharLength() {
    return 255;
  }
  /** Returns the default length for char columns. */
  getDefaultCharLength() {
    return 1;
  }
  /** Returns the SQL type declaration for date columns. */
  getDateTypeDeclarationSQL(length) {
    return 'date' + (length ? `(${length})` : '');
  }
  /** Returns the SQL type declaration for time columns. */
  getTimeTypeDeclarationSQL(length) {
    return 'time' + (length ? `(${length})` : '');
  }
  /** Returns the SQL operator used for regular expression matching. */
  getRegExpOperator(val, flags) {
    return 'regexp';
  }
  /** Builds the SQL clause and parameters for a regular expression condition. */
  mapRegExpCondition(mappedKey, value) {
    const operator = this.getRegExpOperator(value.$re, value.$flags);
    const quotedKey = this.quoteIdentifier(mappedKey);
    return { sql: `${quotedKey} ${operator} ?`, params: [value.$re] };
  }
  /** Converts a JavaScript RegExp into a platform-specific regex representation. */
  getRegExpValue(val) {
    if (val.flags.includes('i')) {
      return { $re: `(?i)${val.source}` };
    }
    return { $re: val.source };
  }
  /** Whether the given operator is allowed at the top level of a query condition. */
  isAllowedTopLevelOperator(operator) {
    return operator === '$not';
  }
  /** Converts a version field value for comparison in optimistic locking queries. */
  convertVersionValue(value, prop) {
    return value;
  }
  /** Returns the default fractional seconds precision for version timestamp columns. */
  getDefaultVersionLength() {
    return 3;
  }
  /** Whether the platform supports tuple comparison in WHERE clauses. */
  allowsComparingTuples() {
    return true;
  }
  /** Whether the given property maps to a bigint database column. */
  isBigIntProperty(prop) {
    return prop.columnTypes?.[0] === 'bigint';
  }
  /** Returns the default schema name for this platform (e.g. "public" for PostgreSQL). */
  getDefaultSchemaName() {
    return undefined;
  }
  /** Returns the SQL type declaration for boolean columns. */
  getBooleanTypeDeclarationSQL() {
    return 'boolean';
  }
  /** Returns the SQL type declaration for integer columns. */
  getIntegerTypeDeclarationSQL(column) {
    return 'int';
  }
  getSmallIntTypeDeclarationSQL(column) {
    return 'smallint';
  }
  getMediumIntTypeDeclarationSQL(column) {
    return 'mediumint';
  }
  getTinyIntTypeDeclarationSQL(column) {
    return 'tinyint';
  }
  getBigIntTypeDeclarationSQL(column) {
    return 'bigint';
  }
  getCharTypeDeclarationSQL(column) {
    return `char(${column.length ?? this.getDefaultCharLength()})`;
  }
  getVarcharTypeDeclarationSQL(column) {
    return `varchar(${column.length ?? this.getDefaultVarcharLength()})`;
  }
  getIntervalTypeDeclarationSQL(column) {
    return 'interval' + (column.length ? `(${column.length})` : '');
  }
  getTextTypeDeclarationSQL(_column) {
    return 'text';
  }
  getEnumTypeDeclarationSQL(column) {
    if (column.items?.every(item => typeof item === 'string')) {
      return `enum('${column.items.join("','")}')`;
    }
    return this.getTinyIntTypeDeclarationSQL(column);
  }
  getFloatDeclarationSQL() {
    return 'float';
  }
  getDoubleDeclarationSQL() {
    return 'double';
  }
  getDecimalTypeDeclarationSQL(column) {
    const precision = column.precision ?? 10;
    const scale = column.scale ?? 0;
    return `numeric(${precision},${scale})`;
  }
  getUuidTypeDeclarationSQL(column) {
    column.length ??= 36;
    return this.getVarcharTypeDeclarationSQL(column);
  }
  /** Extracts the base type name from a full SQL type declaration (e.g. "varchar(255)" -> "varchar"). */
  extractSimpleType(type) {
    return /[^(), ]+/.exec(type.toLowerCase())[0];
  }
  /**
   * This should be used only to compare types, it can strip some information like the length.
   */
  normalizeColumnType(type, options) {
    return type.toLowerCase();
  }
  /** Returns the mapped Type instance for a given SQL/runtime type string. */
  getMappedType(type) {
    const mappedType = this.config.get('discovery').getMappedType?.(type, this);
    return mappedType ?? this.getDefaultMappedType(type);
  }
  /** Returns the default mapped Type for a given type string when no custom mapping is configured. */
  getDefaultMappedType(type) {
    if (type.endsWith('[]')) {
      return Type.getType(ArrayType);
    }
    switch (this.extractSimpleType(type)) {
      case 'character':
      case 'char':
        return Type.getType(CharacterType);
      case 'string':
      case 'varchar':
        return Type.getType(StringType);
      case 'interval':
        return Type.getType(IntervalType);
      case 'text':
        return Type.getType(TextType);
      case 'int':
      case 'number':
        return Type.getType(IntegerType);
      case 'bigint':
        return Type.getType(BigIntType);
      case 'smallint':
        return Type.getType(SmallIntType);
      case 'tinyint':
        return Type.getType(TinyIntType);
      case 'mediumint':
        return Type.getType(MediumIntType);
      case 'float':
        return Type.getType(FloatType);
      case 'double':
        return Type.getType(DoubleType);
      case 'integer':
        return Type.getType(IntegerType);
      case 'decimal':
      case 'numeric':
        return Type.getType(DecimalType);
      case 'boolean':
        return Type.getType(BooleanType);
      case 'blob':
      case 'buffer':
        return Type.getType(BlobType);
      case 'uint8array':
        return Type.getType(Uint8ArrayType);
      case 'uuid':
        return Type.getType(UuidType);
      case 'date':
        return Type.getType(DateType);
      case 'datetime':
      case 'timestamp':
        return Type.getType(DateTimeType);
      case 'time':
        return Type.getType(TimeType);
      case 'object':
      case 'json':
        return Type.getType(JsonType);
      case 'enum':
        return Type.getType(EnumType);
      default:
        return Type.getType(UnknownType);
    }
  }
  /** Whether the platform supports multiple cascade paths to the same table. */
  supportsMultipleCascadePaths() {
    return true;
  }
  /**
   * Returns true if the platform supports ON UPDATE foreign key rules.
   * Oracle doesn't support ON UPDATE rules.
   */
  supportsOnUpdate() {
    return true;
  }
  /** Whether the connection supports executing multiple SQL statements in a single call. */
  supportsMultipleStatements() {
    return this.config.get('multipleStatements');
  }
  /** Whether the platform supports the UNION WHERE optimization for multi-branch queries. */
  supportsUnionWhere() {
    return false;
  }
  /** Returns the SQL type declaration used for array storage. */
  getArrayDeclarationSQL() {
    return 'text';
  }
  /** Serializes a string array into its database storage format. */
  marshallArray(values) {
    return values.join(',');
  }
  /** Deserializes a database-stored array string back into a string array. */
  unmarshallArray(value) {
    if (value === '') {
      return [];
    }
    return value.split(',');
  }
  getBlobDeclarationSQL() {
    return 'blob';
  }
  getJsonDeclarationSQL() {
    return 'json';
  }
  getSearchJsonPropertySQL(path, type, aliased) {
    return path;
  }
  getSearchJsonPropertyKey(path, type, aliased, value) {
    return path.join('.');
  }
  processJsonCondition(o, value, path, alias) {
    if (Utils.isPlainObject(value) && !Object.keys(value).some(k => Utils.isOperator(k))) {
      Utils.keys(value).forEach(k => {
        this.processJsonCondition(o, value[k], [...path, k], alias);
      });
      return o;
    }
    if (path.length === 1) {
      o[path[0]] = value;
      return o;
    }
    const type = this.getJsonValueType(value);
    const k = this.getSearchJsonPropertyKey(path, type, alias, value);
    o[k] = value;
    return o;
  }
  getJsonValueType(value) {
    if (Array.isArray(value)) {
      return typeof value[0];
    }
    if (Utils.isPlainObject(value) && Object.keys(value).every(k => Utils.isOperator(k))) {
      return this.getJsonValueType(Object.values(value)[0]);
    }
    return typeof value;
  }
  /* v8 ignore next */
  getJsonIndexDefinition(index) {
    return index.columnNames;
  }
  getFullTextWhereClause(prop) {
    throw new Error('Full text searching is not supported by this driver.');
  }
  supportsCreatingFullTextIndex() {
    return false;
  }
  getFullTextIndexExpression(indexName, schemaName, tableName, columns) {
    throw new Error('Full text searching is not supported by this driver.');
  }
  /** Whether the driver automatically parses JSON columns into JS objects. */
  convertsJsonAutomatically() {
    return true;
  }
  /** Converts a JS value to its JSON database representation (typically JSON.stringify). */
  convertJsonToDatabaseValue(value, context) {
    return JSON.stringify(value);
  }
  /** Converts a database JSON value to its JS representation. */
  convertJsonToJSValue(value, context) {
    return parseJsonSafe(value);
  }
  convertDateToJSValue(value) {
    return value;
  }
  convertIntervalToJSValue(value) {
    return value;
  }
  convertIntervalToDatabaseValue(value) {
    return value;
  }
  usesAsKeyword() {
    return true;
  }
  /**
   * Determines how UUID values are compared in the change set tracking.
   * Return `'string'` for inline string comparison (fast), or `'any'` for deep comparison via type methods.
   */
  compareUuids() {
    return 'string';
  }
  convertUuidToJSValue(value) {
    return value;
  }
  convertUuidToDatabaseValue(value) {
    return value;
  }
  /** Parses a string or numeric value into a Date object. */
  parseDate(value) {
    const date = new Date(value);
    /* v8 ignore next */
    if (isNaN(date.getTime())) {
      return value;
    }
    return date;
  }
  /** Returns the default EntityRepository class used by this platform. */
  getRepositoryClass() {
    return EntityRepository;
  }
  /** Returns the default character set for this platform. */
  getDefaultCharset() {
    return 'utf8';
  }
  /** Returns the exception converter for translating native errors to driver exceptions. */
  getExceptionConverter() {
    return this.exceptionConverter;
  }
  /**
   * Allows registering extensions of the driver automatically (e.g. `SchemaGenerator` extension in SQL drivers).
   */
  lookupExtensions(orm) {
    // no extensions by default
  }
  /** @internal */
  init(orm) {
    this.lookupExtensions(orm);
  }
  /** Retrieves a registered extension (e.g. SchemaGenerator, Migrator), throwing if not found. */
  getExtension(extensionName, extensionKey, moduleName, em) {
    const extension = this.config.getExtension(extensionKey);
    if (extension) {
      return extension;
    }
    /* v8 ignore next */
    throw new Error(
      `${extensionName} extension not registered. Provide it in the ORM config, or use the async \`MikroORM.init()\` method to load extensions automatically.`,
    );
  }
  /* v8 ignore next: kept for type inference only */
  getSchemaGenerator(driver, em) {
    throw new Error(`${driver.constructor.name} does not support SchemaGenerator`);
  }
  /** Processes a date value before persisting, applying timezone or format conversions. */
  processDateProperty(value) {
    return value;
  }
  /** Wraps a table or column identifier with the platform-specific quote character. */
  quoteIdentifier(id, quote = '`') {
    const raw = Raw.getKnownFragment(id);
    if (raw) {
      return this.formatQuery(raw.sql, raw.params);
    }
    return `${quote}${id.toString().replace('.', `${quote}.${quote}`)}${quote}`;
  }
  /** Quotes a literal value for safe embedding in SQL. */
  quoteValue(value) {
    return value;
  }
  /* v8 ignore next */
  escape(value) {
    return value;
  }
  /** Replaces `?` placeholders in SQL with quoted parameter values. */
  formatQuery(sql, params) {
    if (params.length === 0) {
      return sql;
    }
    // fast string replace without regexps
    let j = 0;
    let pos = 0;
    let ret = '';
    if (sql.startsWith('?')) {
      if (sql[1] === '?') {
        ret += this.quoteIdentifier(params[j++]);
        pos = 2;
      } else {
        ret += this.quoteValue(params[j++]);
        pos = 1;
      }
    }
    while (pos < sql.length) {
      const idx = sql.indexOf('?', pos + 1);
      if (idx === -1) {
        ret += sql.substring(pos, sql.length);
        break;
      }
      if (sql.substring(idx - 1, idx + 1) === '\\?') {
        ret += sql.substring(pos, idx - 1) + '?';
        pos = idx + 1;
      } else if (sql.substring(idx, idx + 2) === '??') {
        ret += sql.substring(pos, idx) + this.quoteIdentifier(params[j++]);
        pos = idx + 2;
      } else {
        ret += sql.substring(pos, idx) + this.quoteValue(params[j++]);
        pos = idx + 1;
      }
    }
    return ret;
  }
  /** Deep-clones embeddable data and tags it for JSON serialization. */
  cloneEmbeddable(data) {
    const copy = clone(data);
    // tag the copy so we know it should be stringified when quoting (so we know how to treat JSON arrays)
    Object.defineProperty(copy, JsonProperty, { enumerable: false, value: true });
    return copy;
  }
  /** Initializes the platform with the ORM configuration. */
  setConfig(config) {
    this.config = config;
    this.namingStrategy = config.getNamingStrategy();
    if (this.config.get('forceUtcTimezone')) {
      this.timezone = 'Z';
    } else {
      this.timezone = this.config.get('timezone');
    }
  }
  /** Returns the current ORM configuration. */
  getConfig() {
    return this.config;
  }
  /** Returns the configured timezone, or undefined if not set. */
  getTimezone() {
    return this.timezone;
  }
  /** Whether the given property represents a numeric database column. */
  isNumericProperty(prop, ignoreCustomType = false) {
    const numericMappedType = prop.columnTypes?.[0] && this.isNumericColumn(this.getMappedType(prop.columnTypes[0]));
    return numericMappedType || prop.type === 'number' || this.isBigIntProperty(prop);
  }
  /** Whether the given mapped type represents a numeric column. */
  isNumericColumn(mappedType) {
    return [IntegerType, SmallIntType, BigIntType, TinyIntType].some(t => mappedType instanceof t);
  }
  /** Whether the platform supports unsigned integer columns. */
  supportsUnsigned() {
    return false;
  }
  /**
   * Returns the default name of index for the given columns
   */
  getIndexName(tableName, columns, type) {
    return this.namingStrategy.indexName(tableName, columns, type);
  }
  /** Returns the default primary key constraint name. */
  getDefaultPrimaryName(tableName, columns) {
    return 'primary';
  }
  /** Whether the platform supports custom names for primary key constraints. */
  supportsCustomPrimaryKeyNames() {
    return false;
  }
  /** Whether the given property key is included in the populate hint. */
  isPopulated(key, populate) {
    return populate === true || (populate !== false && populate.some(p => p.field === key || p.all));
  }
  /** Whether the given property should be included as a column in the SELECT query. */
  shouldHaveColumn(prop, populate, exclude, includeFormulas = true, ignoreInlineEmbeddables = true) {
    if (exclude?.includes(prop.name)) {
      return false;
    }
    if (exclude?.find(k => k.startsWith(`${prop.name}.`) && !this.isPopulated(prop.name, populate))) {
      return false;
    }
    if (prop.formula) {
      return includeFormulas && (!prop.lazy || this.isPopulated(prop.name, populate));
    }
    if (prop.persist === false) {
      return false;
    }
    if (prop.lazy && (populate === false || (populate !== true && !populate.some(p => p.field === prop.name)))) {
      return false;
    }
    if ([ReferenceKind.SCALAR, ReferenceKind.MANY_TO_ONE].includes(prop.kind)) {
      return true;
    }
    if (prop.kind === ReferenceKind.EMBEDDED) {
      return prop.object || ignoreInlineEmbeddables;
    }
    return prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner;
  }
  /**
   * Currently not supported due to how knex does complex sqlite diffing (always based on current schema)
   */
  /** Whether the platform supports generating down migrations. */
  supportsDownMigrations() {
    return true;
  }
  /** Whether the platform supports deferred unique constraints. */
  supportsDeferredUniqueConstraints() {
    return true;
  }
  /** Platform-specific validation of entity metadata. */
  validateMetadata(meta) {
    return;
  }
  /**
   * Generates a custom order by statement given a set of in order values, eg.
   * ORDER BY (CASE WHEN priority = 'low' THEN 1 WHEN priority = 'medium' THEN 2 ELSE NULL END)
   */
  generateCustomOrder(escapedColumn, values) {
    throw new Error('Not supported');
  }
  /**
   * Returns default client url for given driver (e.g. mongodb://127.0.0.1:27017 for mongodb)
   */
  getDefaultClientUrl() {
    return '';
  }
  /**
   * @internal
   */
  castColumn(prop) {
    return '';
  }
  /**
   * @internal
   */
  castJsonValue(prop) {
    return '';
  }
  /**
   * @internal
   */
  clone() {
    return this;
  }
  /** @ignore */
  /* v8 ignore next */
  [Symbol.for('nodejs.util.inspect.custom')]() {
    return `[${this.constructor.name}]`;
  }
}
