import { ReferenceKind, isRaw } from '@mikro-orm/core';
import {
  AliasNode,
  ColumnNode,
  ColumnUpdateNode,
  OperationNodeTransformer,
  PrimitiveValueListNode,
  ReferenceNode,
  SchemableIdentifierNode,
  TableNode,
  ValueListNode,
  ValueNode,
  ValuesNode,
} from 'kysely';
export class MikroTransformer extends OperationNodeTransformer {
  /**
   * Context stack to support nested queries (subqueries, CTEs)
   * Each level of query scope has its own Map of table aliases/names to EntityMetadata
   * Top of stack (highest index) is the current scope
   */
  #contextStack = [];
  /**
   * Subquery alias map: maps subquery/CTE alias to its source table metadata
   * Used to resolve columns from subqueries/CTEs to their original table definitions
   */
  #subqueryAliasMap = new Map();
  #metadata;
  #platform;
  /**
   * Global map of all entities involved in the query.
   * Populated during AST transformation and used for result transformation.
   */
  #entityMap = new Map();
  #em;
  #options;
  constructor(em, options = {}) {
    super();
    this.#em = em;
    this.#options = options;
    this.#metadata = em.getMetadata();
    this.#platform = em.getDriver().getPlatform();
  }
  reset() {
    this.#subqueryAliasMap.clear();
    this.#entityMap.clear();
  }
  getOutputEntityMap() {
    return this.#entityMap;
  }
  /** @internal */
  getContextStack() {
    return this.#contextStack;
  }
  /** @internal */
  getSubqueryAliasMap() {
    return this.#subqueryAliasMap;
  }
  transformSelectQuery(node, queryId) {
    // Push a new context for this query scope (starts with inherited parent context)
    const currentContext = new Map();
    this.#contextStack.push(currentContext);
    try {
      // Process WITH clause (CTEs) first - they define names available in this scope
      if (node.with) {
        this.processWithNode(node.with, currentContext);
      }
      // Process FROM clause - main tables in this scope
      if (node.from?.froms) {
        for (const from of node.from.froms) {
          this.processFromItem(from, currentContext);
        }
      }
      // Process JOINs - additional tables joined into this scope
      if (node.joins) {
        for (const join of node.joins) {
          this.processJoinNode(join, currentContext);
        }
      }
      return super.transformSelectQuery(node, queryId);
    } finally {
      // Pop the context when exiting this query scope
      this.#contextStack.pop();
    }
  }
  transformInsertQuery(node, queryId) {
    const currentContext = new Map();
    this.#contextStack.push(currentContext);
    try {
      let entityMeta;
      if (node.into) {
        const tableName = this.getTableName(node.into);
        if (tableName) {
          const meta = this.findEntityMetadata(tableName);
          if (meta) {
            entityMeta = meta;
            currentContext.set(meta.tableName, meta);
            this.#entityMap.set(meta.tableName, meta);
          }
        }
      }
      const nodeWithHooks =
        this.#options.processOnCreateHooks && entityMeta ? this.processOnCreateHooks(node, entityMeta) : node;
      const nodeWithConvertedValues =
        this.#options.convertValues && entityMeta ? this.processInsertValues(nodeWithHooks, entityMeta) : nodeWithHooks;
      // Handle ON CONFLICT clause
      let finalNode = nodeWithConvertedValues;
      if (node.onConflict?.updates && entityMeta) {
        // Create a temporary UpdateQueryNode to reuse processOnUpdateHooks and processUpdateValues
        // We only care about the updates part
        const tempUpdateNode = {
          kind: 'UpdateQueryNode',
          table: node.into, // Dummy table
          updates: node.onConflict.updates,
        };
        const updatesWithHooks = this.#options.processOnUpdateHooks
          ? this.processOnUpdateHooks(tempUpdateNode, entityMeta).updates
          : node.onConflict.updates;
        const tempUpdateNodeWithHooks = {
          ...tempUpdateNode,
          updates: updatesWithHooks,
        };
        const updatesWithConvertedValues = this.#options.convertValues
          ? this.processUpdateValues(tempUpdateNodeWithHooks, entityMeta).updates
          : updatesWithHooks;
        if (updatesWithConvertedValues && updatesWithConvertedValues !== node.onConflict.updates) {
          // Construct the new OnConflictNode with updated values
          finalNode = {
            ...finalNode,
            onConflict: {
              ...node.onConflict,
              updates: updatesWithConvertedValues,
            },
          };
        }
      }
      return super.transformInsertQuery(finalNode, queryId);
    } finally {
      this.#contextStack.pop();
    }
  }
  transformUpdateQuery(node, queryId) {
    const currentContext = new Map();
    this.#contextStack.push(currentContext);
    try {
      let entityMeta;
      if (node.table && TableNode.is(node.table)) {
        const tableName = this.getTableName(node.table);
        if (tableName) {
          const meta = this.findEntityMetadata(tableName);
          if (meta) {
            entityMeta = meta;
            currentContext.set(meta.tableName, meta);
            this.#entityMap.set(meta.tableName, meta);
          }
        }
      }
      // Process FROM clause in UPDATE queries (for UPDATE with JOIN)
      if (node.from) {
        for (const fromItem of node.from.froms) {
          this.processFromItem(fromItem, currentContext);
        }
      }
      // Also process JOINs in UPDATE queries
      if (node.joins) {
        for (const join of node.joins) {
          this.processJoinNode(join, currentContext);
        }
      }
      const nodeWithHooks =
        this.#options.processOnUpdateHooks && entityMeta ? this.processOnUpdateHooks(node, entityMeta) : node;
      const nodeWithConvertedValues =
        this.#options.convertValues && entityMeta ? this.processUpdateValues(nodeWithHooks, entityMeta) : nodeWithHooks;
      return super.transformUpdateQuery(nodeWithConvertedValues, queryId);
    } finally {
      this.#contextStack.pop();
    }
  }
  transformDeleteQuery(node, queryId) {
    const currentContext = new Map();
    this.#contextStack.push(currentContext);
    try {
      const froms = node.from?.froms;
      if (froms && froms.length > 0) {
        const firstFrom = froms[0];
        if (TableNode.is(firstFrom)) {
          const tableName = this.getTableName(firstFrom);
          if (tableName) {
            const meta = this.findEntityMetadata(tableName);
            if (meta) {
              currentContext.set(meta.tableName, meta);
              this.#entityMap.set(meta.tableName, meta);
            }
          }
        }
      }
      // Also process JOINs in DELETE queries
      if (node.joins) {
        for (const join of node.joins) {
          this.processJoinNode(join, currentContext);
        }
      }
      return super.transformDeleteQuery(node, queryId);
    } finally {
      this.#contextStack.pop();
    }
  }
  transformMergeQuery(node, queryId) {
    const currentContext = new Map();
    this.#contextStack.push(currentContext);
    try {
      return super.transformMergeQuery(node, queryId);
    } finally {
      this.#contextStack.pop();
    }
  }
  transformIdentifier(node, queryId) {
    node = super.transformIdentifier(node, queryId);
    const parent = this.nodeStack[this.nodeStack.length - 2];
    // Transform table names when tableNamingStrategy is 'entity'
    if (this.#options.tableNamingStrategy === 'entity' && parent && SchemableIdentifierNode.is(parent)) {
      const meta = this.findEntityMetadata(node.name);
      if (meta) {
        return {
          ...node,
          name: meta.tableName,
        };
      }
    }
    // Transform column names when columnNamingStrategy is 'property'
    // Support ColumnNode, ColumnUpdateNode, and ReferenceNode (for JOIN conditions)
    if (
      this.#options.columnNamingStrategy === 'property' &&
      parent &&
      (ColumnNode.is(parent) || ColumnUpdateNode.is(parent) || ReferenceNode.is(parent))
    ) {
      const ownerMeta = this.findOwnerEntityInContext();
      if (ownerMeta) {
        const prop = ownerMeta.properties[node.name];
        const fieldName = prop?.fieldNames?.[0];
        if (fieldName) {
          return {
            ...node,
            name: fieldName,
          };
        }
      }
    }
    return node;
  }
  /**
   * Find owner entity metadata for the current identifier in the context stack.
   * Supports both aliased and non-aliased table references.
   * Searches up the context stack to support correlated subqueries.
   * Also checks subquery/CTE aliases to resolve to their source tables.
   */
  findOwnerEntityInContext() {
    // Check if current column has a table reference (e.g., u.firstName)
    const reference = this.nodeStack.find(it => ReferenceNode.is(it));
    if (reference?.table && TableNode.is(reference.table)) {
      const tableName = this.getTableName(reference.table);
      if (tableName) {
        // First, check in subquery alias map (for CTE/subquery columns)
        if (this.#subqueryAliasMap.has(tableName)) {
          return this.#subqueryAliasMap.get(tableName);
        }
        // Find entity metadata to get the actual table name
        // Context uses table names (meta.tableName) as keys, not entity names
        const entityMeta = this.findEntityMetadata(tableName);
        if (entityMeta) {
          // Search in context stack using the actual table name
          const meta = this.lookupInContextStack(entityMeta.tableName);
          if (meta) {
            return meta;
          }
          // Also try with the entity name (for cases where context uses entity name)
          const metaByEntityName = this.lookupInContextStack(tableName);
          if (metaByEntityName) {
            return metaByEntityName;
          }
        } else {
          // If entity metadata not found, try direct lookup (for CTE/subquery cases)
          const meta = this.lookupInContextStack(tableName);
          if (meta) {
            return meta;
          }
        }
      }
    }
    // If no explicit table reference, use the first entity in current context
    if (this.#contextStack.length > 0) {
      const currentContext = this.#contextStack[this.#contextStack.length - 1];
      for (const [alias, meta] of currentContext.entries()) {
        if (meta) {
          return meta;
        }
        // If the context value is undefined but the alias is in subqueryAliasMap,
        // use the mapped metadata (for CTE/subquery cases)
        if (!meta && this.#subqueryAliasMap.has(alias)) {
          const mappedMeta = this.#subqueryAliasMap.get(alias);
          if (mappedMeta) {
            return mappedMeta;
          }
        }
      }
    }
    return undefined;
  }
  processOnCreateHooks(node, meta) {
    if (!node.columns || !node.values || !ValuesNode.is(node.values)) {
      return node;
    }
    const existingProps = new Set();
    for (const col of node.columns) {
      const prop = this.findProperty(meta, this.normalizeColumnName(col.column));
      if (prop) {
        existingProps.add(prop.name);
      }
    }
    const missingProps = meta.props.filter(prop => prop.onCreate && !existingProps.has(prop.name));
    if (missingProps.length === 0) {
      return node;
    }
    const newColumns = [...node.columns];
    for (const prop of missingProps) {
      newColumns.push(ColumnNode.create(prop.name));
    }
    const newRows = node.values.values.map(row => {
      const valuesToAdd = missingProps.map(prop => {
        const val = prop.onCreate(undefined, this.#em);
        return val;
      });
      if (ValueListNode.is(row)) {
        const newValues = [...row.values, ...valuesToAdd.map(v => ValueNode.create(v))];
        return ValueListNode.create(newValues);
      }
      if (PrimitiveValueListNode.is(row)) {
        const newValues = [...row.values, ...valuesToAdd];
        return PrimitiveValueListNode.create(newValues);
      }
      return row;
    });
    return {
      ...node,
      columns: Object.freeze(newColumns),
      values: ValuesNode.create(newRows),
    };
  }
  processOnUpdateHooks(node, meta) {
    if (!node.updates) {
      return node;
    }
    const existingProps = new Set();
    for (const update of node.updates) {
      if (ColumnNode.is(update.column)) {
        const prop = this.findProperty(meta, this.normalizeColumnName(update.column.column));
        if (prop) {
          existingProps.add(prop.name);
        }
      }
    }
    const missingProps = meta.props.filter(prop => prop.onUpdate && !existingProps.has(prop.name));
    if (missingProps.length === 0) {
      return node;
    }
    const newUpdates = [...node.updates];
    for (const prop of missingProps) {
      const val = prop.onUpdate(undefined, this.#em);
      newUpdates.push(ColumnUpdateNode.create(ColumnNode.create(prop.name), ValueNode.create(val)));
    }
    return {
      ...node,
      updates: Object.freeze(newUpdates),
    };
  }
  processInsertValues(node, meta) {
    if (!node.columns?.length || !node.values || !ValuesNode.is(node.values)) {
      return node;
    }
    const columnProps = this.mapColumnsToProperties(node.columns, meta);
    const shouldConvert = this.shouldConvertValues();
    let changed = false;
    const convertedRows = node.values.values.map(row => {
      if (ValueListNode.is(row)) {
        if (row.values.length !== columnProps.length) {
          return row;
        }
        const values = row.values.map((valueNode, idx) => {
          if (!ValueNode.is(valueNode)) {
            return valueNode;
          }
          const converted = this.prepareInputValue(columnProps[idx], valueNode.value, shouldConvert);
          if (converted === valueNode.value) {
            return valueNode;
          }
          changed = true;
          return valueNode.immediate ? ValueNode.createImmediate(converted) : ValueNode.create(converted);
        });
        return ValueListNode.create(values);
      }
      if (PrimitiveValueListNode.is(row)) {
        if (row.values.length !== columnProps.length) {
          return row;
        }
        const values = row.values.map((value, idx) => {
          const converted = this.prepareInputValue(columnProps[idx], value, shouldConvert);
          if (converted !== value) {
            changed = true;
          }
          return converted;
        });
        return PrimitiveValueListNode.create(values);
      }
      return row;
    });
    if (!changed) {
      return node;
    }
    return {
      ...node,
      values: ValuesNode.create(convertedRows),
    };
  }
  processUpdateValues(node, meta) {
    if (!node.updates?.length) {
      return node;
    }
    const shouldConvert = this.shouldConvertValues();
    let changed = false;
    const updates = node.updates.map(updateNode => {
      if (!ValueNode.is(updateNode.value)) {
        return updateNode;
      }
      const columnName = ColumnNode.is(updateNode.column)
        ? this.normalizeColumnName(updateNode.column.column)
        : undefined;
      const property = this.findProperty(meta, columnName);
      const converted = this.prepareInputValue(property, updateNode.value.value, shouldConvert);
      if (converted === updateNode.value.value) {
        return updateNode;
      }
      changed = true;
      const newValueNode = updateNode.value.immediate
        ? ValueNode.createImmediate(converted)
        : ValueNode.create(converted);
      return {
        ...updateNode,
        value: newValueNode,
      };
    });
    if (!changed) {
      return node;
    }
    return {
      ...node,
      updates,
    };
  }
  mapColumnsToProperties(columns, meta) {
    return columns.map(column => {
      const columnName = this.normalizeColumnName(column.column);
      return this.findProperty(meta, columnName);
    });
  }
  normalizeColumnName(identifier) {
    const name = identifier.name;
    if (!name.includes('.')) {
      return name;
    }
    const parts = name.split('.');
    return parts[parts.length - 1] ?? name;
  }
  findProperty(meta, columnName) {
    if (!meta || !columnName) {
      return undefined;
    }
    if (meta.properties[columnName]) {
      return meta.properties[columnName];
    }
    return meta.props.find(prop => prop.fieldNames?.includes(columnName));
  }
  shouldConvertValues() {
    return !!this.#options.convertValues;
  }
  prepareInputValue(prop, value, enabled) {
    if (!enabled || !prop || value == null) {
      return value;
    }
    if (typeof value === 'object' && value !== null) {
      if (isRaw(value)) {
        return value;
      }
      if ('kind' in value) {
        return value;
      }
    }
    if (prop.customType && !isRaw(value)) {
      return prop.customType.convertToDatabaseValue(value, this.#platform, {
        fromQuery: true,
        key: prop.name,
        mode: 'query-data',
      });
    }
    if (value instanceof Date) {
      return this.#platform.processDateProperty(value);
    }
    return value;
  }
  /**
   * Look up a table name/alias in the context stack.
   * Searches from current scope (top of stack) to parent scopes (bottom).
   * This supports correlated subqueries and references to outer query tables.
   */
  lookupInContextStack(tableNameOrAlias) {
    // Search from top of stack (current scope) to bottom (parent scopes)
    for (let i = this.#contextStack.length - 1; i >= 0; i--) {
      const context = this.#contextStack[i];
      if (context.has(tableNameOrAlias)) {
        return context.get(tableNameOrAlias);
      }
    }
    return undefined;
  }
  /**
   * Process WITH node (CTE definitions)
   */
  processWithNode(withNode, context) {
    for (const cte of withNode.expressions) {
      const cteName = this.getCTEName(cte.name);
      if (cteName) {
        // CTEs are not entities, so map to undefined
        // They will be transformed recursively by transformSelectQuery
        context.set(cteName, undefined);
        // Also try to extract the source table from the CTE's expression
        // This helps resolve columns in subsequent queries that use the CTE
        if (cte.expression?.kind === 'SelectQueryNode') {
          const sourceMeta = this.extractSourceTableFromSelectQuery(cte.expression);
          if (sourceMeta) {
            this.#subqueryAliasMap.set(cteName, sourceMeta);
            // Add CTE to entityMap so it can be used for result transformation if needed
            // (though CTEs usually don't appear in result rows directly, but their columns might)
            this.#entityMap.set(cteName, sourceMeta);
          }
        }
      }
    }
  }
  /**
   * Extract CTE name from CommonTableExpressionNameNode
   */
  getCTEName(nameNode) {
    if (TableNode.is(nameNode.table)) {
      return this.getTableName(nameNode.table);
    }
    return undefined;
  }
  /**
   * Process a FROM item (can be TableNode or AliasNode)
   */
  processFromItem(
    from, // OperationNode type - can be TableNode, AliasNode, or SelectQueryNode
    context,
  ) {
    if (AliasNode.is(from)) {
      if (TableNode.is(from.node)) {
        // Regular table with alias
        const tableName = this.getTableName(from.node);
        if (tableName && from.alias) {
          const meta = this.findEntityMetadata(tableName);
          const aliasName = this.extractAliasName(from.alias);
          if (aliasName) {
            context.set(aliasName, meta);
            if (meta) {
              this.#entityMap.set(aliasName, meta);
            }
            // Also map the alias in subqueryAliasMap if the table name is a CTE
            if (this.#subqueryAliasMap.has(tableName)) {
              this.#subqueryAliasMap.set(aliasName, this.#subqueryAliasMap.get(tableName));
            }
          }
        }
      } else if (from.node?.kind === 'SelectQueryNode') {
        // Subquery with alias
        const aliasName = this.extractAliasName(from.alias);
        if (aliasName) {
          context.set(aliasName, undefined);
          // Try to extract the source table from the subquery
          const sourceMeta = this.extractSourceTableFromSelectQuery(from.node);
          if (sourceMeta) {
            this.#subqueryAliasMap.set(aliasName, sourceMeta);
          }
        }
      } else {
        // Other types with alias
        const aliasName = this.extractAliasName(from.alias);
        if (aliasName) {
          context.set(aliasName, undefined);
        }
      }
    } else if (TableNode.is(from)) {
      // Table without alias
      const tableName = this.getTableName(from);
      if (tableName) {
        const meta = this.findEntityMetadata(tableName);
        context.set(tableName, meta);
        if (meta) {
          this.#entityMap.set(tableName, meta);
        }
      }
    }
  }
  /**
   * Process a JOIN node
   */
  processJoinNode(join, context) {
    const joinTable = join.table;
    if (AliasNode.is(joinTable)) {
      if (TableNode.is(joinTable.node)) {
        // Regular table with alias in JOIN
        const tableName = this.getTableName(joinTable.node);
        if (tableName && joinTable.alias) {
          const meta = this.findEntityMetadata(tableName);
          const aliasName = this.extractAliasName(joinTable.alias);
          if (aliasName) {
            context.set(aliasName, meta);
            if (meta) {
              this.#entityMap.set(aliasName, meta);
            }
            // Also map the alias in subqueryAliasMap if the table name is a CTE
            if (this.#subqueryAliasMap.has(tableName)) {
              this.#subqueryAliasMap.set(aliasName, this.#subqueryAliasMap.get(tableName));
            }
          }
        }
      } else if (joinTable.node?.kind === 'SelectQueryNode') {
        // Subquery with alias in JOIN
        const aliasName = this.extractAliasName(joinTable.alias);
        if (aliasName) {
          context.set(aliasName, undefined);
          // Try to extract the source table from the subquery
          const sourceMeta = this.extractSourceTableFromSelectQuery(joinTable.node);
          if (sourceMeta) {
            this.#subqueryAliasMap.set(aliasName, sourceMeta);
          }
        }
      } else {
        // Other types with alias
        const aliasName = this.extractAliasName(joinTable.alias);
        if (aliasName) {
          context.set(aliasName, undefined);
        }
      }
    } else if (TableNode.is(joinTable)) {
      // Table without alias in JOIN
      const tableName = this.getTableName(joinTable);
      if (tableName) {
        const meta = this.findEntityMetadata(tableName);
        // Use table name (meta.tableName) as key to match transformUpdateQuery behavior
        if (meta) {
          context.set(meta.tableName, meta);
          this.#entityMap.set(meta.tableName, meta);
          // Also set with entity name for backward compatibility
          context.set(tableName, meta);
        } else {
          context.set(tableName, undefined);
        }
      }
    }
  }
  /**
   * Extract the primary source table from a SELECT query
   * This helps resolve columns from subqueries to their original entity tables
   */
  extractSourceTableFromSelectQuery(selectQuery) {
    if (!selectQuery.from?.froms || selectQuery.from.froms.length === 0) {
      return undefined;
    }
    // Get the first FROM table
    const firstFrom = selectQuery.from.froms[0];
    let sourceTable;
    if (AliasNode.is(firstFrom) && TableNode.is(firstFrom.node)) {
      sourceTable = firstFrom.node;
    } else if (TableNode.is(firstFrom)) {
      sourceTable = firstFrom;
    }
    if (sourceTable) {
      const tableName = this.getTableName(sourceTable);
      if (tableName) {
        return this.findEntityMetadata(tableName);
      }
    }
    return undefined;
  }
  /**
   * Extract alias name from an alias node
   */
  extractAliasName(alias) {
    if (typeof alias === 'object' && 'name' in alias) {
      return alias.name;
    }
    return undefined;
  }
  /**
   * Extract table name from a TableNode
   */
  getTableName(node) {
    if (!node) {
      return undefined;
    }
    if (TableNode.is(node) && SchemableIdentifierNode.is(node.table)) {
      const identifier = node.table.identifier;
      if (typeof identifier === 'object' && 'name' in identifier) {
        return identifier.name;
      }
    }
    return undefined;
  }
  /**
   * Find entity metadata by table name or entity name
   */
  findEntityMetadata(name) {
    const byEntity = this.#metadata.getByClassName(name, false);
    if (byEntity) {
      return byEntity;
    }
    const allMetadata = Array.from(this.#metadata);
    const byTable = allMetadata.find(m => m.tableName === name);
    if (byTable) {
      return byTable;
    }
    return undefined;
  }
  /**
   * Transform result rows by mapping database column names to property names
   * This is called for SELECT queries when columnNamingStrategy is 'property'
   */
  transformResult(rows, entityMap) {
    // Only transform if columnNamingStrategy is 'property' or convertValues is true, and we have data
    if (
      (this.#options.columnNamingStrategy !== 'property' && !this.#options.convertValues) ||
      !rows ||
      rows.length === 0
    ) {
      return rows;
    }
    // If no entities found (e.g. raw query without known tables), return rows as is
    if (entityMap.size === 0) {
      return rows;
    }
    // Build a global mapping from database field names to property objects
    const fieldToPropertyMap = this.buildGlobalFieldMap(entityMap);
    const relationFieldMap = this.buildGlobalRelationFieldMap(entityMap);
    // Transform each row
    return rows.map(row => this.transformRow(row, fieldToPropertyMap, relationFieldMap));
  }
  buildGlobalFieldMap(entityMap) {
    const map = {};
    for (const [alias, meta] of entityMap.entries()) {
      Object.assign(map, this.buildFieldToPropertyMap(meta, alias));
    }
    return map;
  }
  buildGlobalRelationFieldMap(entityMap) {
    const map = {};
    for (const [alias, meta] of entityMap.entries()) {
      Object.assign(map, this.buildRelationFieldMap(meta, alias));
    }
    return map;
  }
  /**
   * Build a mapping from database field names to property objects
   * Format: { 'field_name': EntityProperty }
   */
  buildFieldToPropertyMap(meta, alias) {
    const map = {};
    for (const prop of meta.props) {
      if (prop.fieldNames && prop.fieldNames.length > 0) {
        for (const fieldName of prop.fieldNames) {
          if (!(fieldName in map)) {
            map[fieldName] = prop;
          }
          if (alias) {
            const dotted = `${alias}.${fieldName}`;
            if (!(dotted in map)) {
              map[dotted] = prop;
            }
            const underscored = `${alias}_${fieldName}`;
            if (!(underscored in map)) {
              map[underscored] = prop;
            }
            const doubleUnderscored = `${alias}__${fieldName}`;
            if (!(doubleUnderscored in map)) {
              map[doubleUnderscored] = prop;
            }
          }
        }
      }
      if (!(prop.name in map)) {
        map[prop.name] = prop;
      }
    }
    return map;
  }
  /**
   * Build a mapping for relation fields
   * For ManyToOne relations, we need to map from the foreign key field to the relation property
   * Format: { 'foreign_key_field': 'relationPropertyName' }
   */
  buildRelationFieldMap(meta, alias) {
    const map = {};
    for (const prop of meta.props) {
      // For ManyToOne/OneToOne relations, find the foreign key field
      if (prop.kind === ReferenceKind.MANY_TO_ONE || prop.kind === ReferenceKind.ONE_TO_ONE) {
        if (prop.fieldNames && prop.fieldNames.length > 0) {
          const fieldName = prop.fieldNames[0];
          map[fieldName] = prop.name;
          if (alias) {
            map[`${alias}.${fieldName}`] = prop.name;
            map[`${alias}_${fieldName}`] = prop.name;
            map[`${alias}__${fieldName}`] = prop.name;
          }
        }
      }
    }
    return map;
  }
  /**
   * Transform a single row by mapping column names to property names
   */
  transformRow(row, fieldToPropertyMap, relationFieldMap) {
    const transformed = { ...row };
    // First pass: map regular fields from fieldName to propertyName and convert values
    for (const [fieldName, prop] of Object.entries(fieldToPropertyMap)) {
      if (!(fieldName in transformed)) {
        continue;
      }
      const converted = this.prepareOutputValue(prop, transformed[fieldName]);
      if (this.#options.columnNamingStrategy === 'property' && prop.name !== fieldName) {
        if (!(prop.name in transformed)) {
          transformed[prop.name] = converted;
        } else {
          transformed[prop.name] = converted;
        }
        delete transformed[fieldName];
        continue;
      }
      if (this.#options.convertValues) {
        transformed[fieldName] = converted;
      }
    }
    // Second pass: handle relation fields
    // Only run if columnNamingStrategy is 'property', as we don't want to rename FKs otherwise
    if (this.#options.columnNamingStrategy === 'property') {
      for (const [fieldName, relationPropertyName] of Object.entries(relationFieldMap)) {
        if (fieldName in transformed && !(relationPropertyName in transformed)) {
          // Move the foreign key value to the relation property name
          transformed[relationPropertyName] = transformed[fieldName];
          delete transformed[fieldName];
        }
      }
    }
    return transformed;
  }
  prepareOutputValue(prop, value) {
    if (!this.#options.convertValues || !prop || value == null) {
      return value;
    }
    if (prop.customType) {
      return prop.customType.convertToJSValue(value, this.#platform);
    }
    // Aligned with EntityComparator.getResultMapper logic
    if (prop.runtimeType === 'boolean') {
      // Use !! conversion like EntityComparator: value == null ? value : !!value
      return value == null ? value : !!value;
    }
    if (prop.runtimeType === 'Date' && !this.#platform.isNumericProperty(prop)) {
      // Aligned with EntityComparator: exclude numeric timestamp properties
      // If already Date instance or null, return as is
      if (value == null || value instanceof Date) {
        return value;
      }
      // Handle timezone like EntityComparator.parseDate
      const tz = this.#platform.getTimezone();
      if (!tz || tz === 'local') {
        return this.#platform.parseDate(value);
      }
      // For non-local timezone, check if value already has timezone info
      // Number (timestamp) doesn't need timezone handling, string needs check
      if (
        typeof value === 'number' ||
        (typeof value === 'string' && (value.includes('+') || value.lastIndexOf('-') > 10 || value.endsWith('Z')))
      ) {
        return this.#platform.parseDate(value);
      }
      // Append timezone if not present (only for string values)
      return this.#platform.parseDate(value + tz);
    }
    // For all other runtimeTypes (number, string, bigint, Buffer, object, any, etc.)
    // EntityComparator just assigns directly without conversion
    return value;
  }
}
