import {
  BuildQueryOptions,
  DeleteQueryBuilder,
  FieldAlias,
  FromQueryBuilder,
  GroupByField,
  GroupByQueryBuilder,
  InsertOptions,
  InsertQueryBuilder,
  InsertValue,
  JoinQueryBuilder,
  JoinType,
  Limit,
  LimitQueryBuilder,
  Offset,
  OffsetQueryBuilder,
  OrderByField,
  OrderByQueryBuilder,
  QueryFunction,
  RawField,
  SelectFields,
  SelectQueryBuilder,
  SetQueryBuilder,
  SQL_CONSTRUCTORS,
  UpdateOptions,
  UpdateQueryBuilder,
  UpdateQueryBuilderWithoutSet,
  UpdateValue,
  WhereCondition,
  WhereQueryBuilder
} from '@dto/types';
import { format as _format } from 'mysql2';

// Type Guards
function isRawField<T extends string>(field: any): field is RawField {
  return field && typeof field === 'object' && 'raw' in field && typeof field.raw === 'string';
}

function isFieldAlias<T extends string>(field: any): field is FieldAlias<T> {
  return field && typeof field === 'object' && !('raw' in field);
}

export class SQLBuilder<ColumnKeys extends string, QueryReturnType = any> {
  #queryParts: SQL_CONSTRUCTORS;
  queryFn?: QueryFunction;
  // message: string = 'Call .buildQuery() or .executeQuery() to get the result';
  constructor(queryFn?: QueryFunction) {
    this.#queryParts = {
      count: { sql: "", params: [] },
      select: { sql: "", params: [] },
      from: { sql: "", params: [] },
      join: { sql: "", params: [] },
      where: { sql: "", params: [] },
      groupBy: { sql: "", params: [] },
      orderBy: { sql: "", params: [] },
      limit: { sql: "", params: [] },
      offset: { sql: "", params: [] },
      insert: { sql: "", params: [] },
      update: { sql: "", params: [] },
      set: { sql: "", params: [] },
      delete: { sql: "", params: [] },
    };
    this.queryFn = queryFn;
  }

  private extractTableAndAlias(table: string, alias?: string): [string, string | undefined] {
    let tableName: string;
    let extractedAlias: string | undefined;

    if (alias) {
      tableName = table;
    } else {
      [tableName, extractedAlias] = table.split(' ');
    }

    return [tableName, alias || extractedAlias];
  }

  private getCurrentUnixTimestamp(): number {
    return Math.floor(Date.now() / 1000); // Unix timestamp
  }

  private processFields<T extends string>(
    fields: SelectFields<T>
  ): { sql: string; params: any[] } {
    const wildcardPattern = /^[a-zA-Z_][a-zA-Z0-9_]*\.\*$/; // Pattern to match table.* (e.g., c.*)

    if (typeof fields === 'string') {
      if (fields === '*') {
        // Case 1: Select all fields
        return { sql: 'SELECT *', params: [] };
      } else if (wildcardPattern.test(fields)) {
        // Case 2: Select all fields from a specific table (e.g., c.*)
        return { sql: `SELECT ${fields}`, params: [] };
      } else {
        // Case 3: Select a single specific field with placeholder
        return { sql: 'SELECT ??', params: [fields] };
      }
    } else if (Array.isArray(fields) && fields.length > 0) {
      // Handling an array of fields
      const fieldClauses: string[] = []; // Array to accumulate SQL fragments for fields
      const fieldParams: any[] = []; // Array to accumulate parameters corresponding to placeholders

      fields.forEach((field) => {
        if (typeof field === 'string') {
          if (wildcardPattern.test(field)) {
            // Field is in the format table.*
            fieldClauses.push(field);
          } else {
            // Regular field with placeholder
            fieldClauses.push('??');
            fieldParams.push(field);
          }
        } else if (isFieldAlias(field)) {
          // Field with an alias (e.g., { email: 'email_address' })
          Object.entries(field).forEach(([col, alias]) => {
            fieldClauses.push('?? AS ??'); // SQL fragment with two placeholders
            fieldParams.push(col, alias); // Corresponding parameters
          });
        } else if (isRawField(field)) {
          // Raw SQL field (e.g., { raw: 'COUNT(*)', alias: 'total' })
          let rawField = field.raw;
          if (field.alias) {
            // Append alias if provided
            rawField += ` AS \`${field.alias}\``;
          }
          fieldClauses.push(rawField); // Insert raw SQL directly without placeholders

          if (field.params) {
            // Append raw field parameters to the main params array
            fieldParams.push(...field.params);
          }
        }
      });

      // Combine all field clauses into a single SELECT statement
      const sql = `SELECT ${fieldClauses.join(', ')}`;
      return { sql, params: fieldParams };
    } else if (typeof fields === 'object' && fields !== null) {
      // Handling a single field object
      if (isRawField(fields)) {
        // Single RawField object
        let rawField = fields.raw;
        if (fields.alias) {
          // Append alias if provided
          rawField += ` AS \`${fields.alias}\``;
        }
        const params = fields.params ? [...fields.params] : []; // Clone params if they exist
        return { sql: `SELECT ${rawField}`, params };
      }

      // Single FieldAlias object (e.g., { email: 'email_address' })
      const aliases = Object.entries(fields)
        .map(([col, alias]) => `?? AS ??`)
        .join(', ');
      const params = Object.entries(fields).flatMap(([col, alias]) => [col, alias]);
      return { sql: `SELECT ${aliases}`, params };
    } else {
      // Default case: Select all fields
      return { sql: 'SELECT *', params: [] };
    }
  }

  private checkEmptyObject(obj: Object) {
    return Object.keys(obj || {}).length === 0;
  }

  private checkTableName(tableName: string, caller?: string) {
    if (!tableName) {
      throw new Error(this.printPrefixMessage('Table name is required'));
    }
  }

  private throwEmptyObjectError(obj: Object, message?: string) {
    if (this.checkEmptyObject(obj)) {
      throw new Error(message || 'Object cannot be empty');
    }
  }

  private printPrefixMessage(message: string) {
    return `[SQLBuilder] :: ${message}`;
  }

  /**
   * Validates that increment and decrement are not both provided for the same field
   */
  private validateIncrementDecrement(values: UpdateValue<ColumnKeys>, context: string = 'Values'): void {
    Object.entries(values).forEach(([key, value]) => {
      if (typeof value === 'object' && value !== null) {
        if ('increment' in value && 'decrement' in value) {
          throw new Error(`${context} :: Both increment and decrement provided for field ${key}`);
        }
      }
    });
  }

  /**
   * Builds SQL clauses and parameters for update operations, handling increment/decrement values
   * @returns Object with clauses array and params array
   */
  private buildUpdateClausesAndParams(
    values: UpdateValue<ColumnKeys>
  ): { clauses: string[]; params: any[] } {
    const clauses: string[] = [];
    const params: any[] = [];

    Object.entries(values).forEach(([key, value]) => {
      if (typeof value === 'object' && value !== null) {
        if ('increment' in value) {
          clauses.push(`?? = ?? + ?`);
          params.push(key, key, value.increment);
        } else if ('decrement' in value) {
          clauses.push(`?? = ?? - ?`);
          params.push(key, key, value.decrement);
        } else {
          clauses.push(`?? = ?`);
          params.push(key, value);
        }
      } else {
        clauses.push(`?? = ?`);
        params.push(key, value);
      }
    });

    return { clauses, params };
  }

  // private uniqueFields<T extends string>(fields: (T | FieldAlias<T>)[]) {
  //   const seen = new Map<string, T | FieldAlias<T>>();

  //   fields.forEach(field => {
  //     if (typeof field === 'string') {
  //       seen.set(field, field);
  //     } else if (typeof field === 'object') {
  //       const key = Object.keys(field)[0];
  //       const value = field[key as T];
  //       const fieldString = `${key}:${value}`;
  //       if (!seen.has(fieldString)) {
  //         seen.set(fieldString, field);
  //       }
  //     }
  //   });
  //   return Array.from(seen.values());
  // }
  private uniqueFields<T extends string>(
    fields: Array<T | FieldAlias<T> | RawField>
  ): Array<T | FieldAlias<T> | RawField> {
    const seen = new Map<string, T | FieldAlias<T> | RawField>();

    fields.forEach((field) => {
      if (typeof field === 'string') {
        // Handle string fields
        if (!seen.has(field)) {
          seen.set(field, field);
        }
      } else if (isRawField<T>(field)) {
        // Handle RawField
        const key = field.alias ? `${field.raw} AS ${field.alias}` : field.raw;
        if (!seen.has(key)) {
          seen.set(key, field);
        }
      } else if (isFieldAlias<T>(field)) {
        // Handle FieldAlias
        const key = Object.keys(field)[0];
        const alias = field[key as T];
        const fieldString = alias ? `${key}:${alias}` : key;
        if (!seen.has(fieldString)) {
          seen.set(fieldString, field);
        }
      }
    });

    return Array.from(seen.values());
  }

  private buildWhereClause(conditions: WhereCondition<ColumnKeys>, parentOperator: string = 'AND'): { clause: string, params: any[] } {
    const processConditions = (conditions: WhereCondition<ColumnKeys>, parentOperator: string = 'AND'): { clause: string, params: any[] } => {
      const clauses: string[] = [];
      const localParams: any[] = [];

      for (const key in conditions) {
        const value = (conditions as any)[key];
        switch (key) {
          case 'AND':
          case 'OR':
            if (!Array.isArray(value) || value.length === 0) {
              throw new Error(this.printPrefixMessage(`processConditions :: ${key} :: condition must be a non-empty array`));
            }
            const nestedConditions = value as WhereCondition<ColumnKeys>[];
            const nestedClauses = nestedConditions.map(nestedCondition => processConditions(nestedCondition, key));
            const nestedClauseStrings = nestedClauses.map(nestedResult => `(${nestedResult.clause})`);
            clauses.push(nestedClauseStrings.join(` ${key} `));
            nestedClauses.forEach(nestedResult => localParams.push(...nestedResult.params));
            break;
          case 'RAW':
            // Handle RAW SQL condition
            const isValidRaw = typeof value === 'object' && value !== null && 'sql' in value && typeof value.sql === 'string' && value.sql.length > 0;
            if (!isValidRaw) {
              throw new Error(this.printPrefixMessage('processConditions :: RAW :: raw (sql) must be a string'));
            }
            clauses.push(value.sql);
            if (value.params && Array.isArray(value.params)) {
              localParams.push(...value.params);
            }
            break;
          default:
          const sanitizedKey = key.replace(/[^a-zA-Z0-9_.]/g, '');

          if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
            for (const operator in value) {
              // Handle IN
              if (operator === 'IN' && Array.isArray(value[operator])) {
                if (value[operator].length === 0) {
                  throw new Error(this.printPrefixMessage(`processConditions :: IN :: condition must be a non-empty array`));
                }
                clauses.push(`${sanitizedKey} IN (${value[operator].map(() => '?').join(', ')})`);
                localParams.push(...value[operator]);
                // Handle NOT_IN
              } else if (operator === 'NOT_IN' && Array.isArray(value[operator])) {
                if (value[operator].length === 0) {
                  throw new Error(this.printPrefixMessage(`processConditions :: NOT_IN :: condition must be a non-empty array`));
                }
                clauses.push(`${sanitizedKey} NOT IN (${value[operator].map(() => '?').join(', ')})`);
                localParams.push(...value[operator]);
                // Handle BETWEEN
              } else if (operator === 'BETWEEN' && Array.isArray(value[operator])) {
                if (value[operator].length !== 2) {
                  throw new Error(this.printPrefixMessage(`processConditions :: BETWEEN :: condition must be an array with exactly 2 elements`));
                }
                clauses.push(`${sanitizedKey} BETWEEN ? AND ?`);
                localParams.push(value[operator][0], value[operator][1]);
                // Handle NOT_BETWEEN
              } else if (operator === 'NOT_BETWEEN' && Array.isArray(value[operator])) {
                if (value[operator].length !== 2) {
                  throw new Error(this.printPrefixMessage(`processConditions :: NOT_BETWEEN :: condition must be an array with exactly 2 elements`));
                }
                clauses.push(`${sanitizedKey} NOT BETWEEN ? AND ?`);
                localParams.push(value[operator][0], value[operator][1]);
                // Handle =, !=, <, <=, >, >=
              } else if (['=', '!=', '<', '<=', '>', '>='].includes(operator)) {
                clauses.push(`${sanitizedKey} ${operator} ?`);
                localParams.push(value[operator]);
                // Handle LIKE, NOT LIKE
              } else if (operator === 'LIKE' || operator === 'NOT_LIKE') {
                const patternType = value[operator];
                const clauseString = `${sanitizedKey} ${operator === 'NOT_LIKE' ? 'NOT LIKE' : 'LIKE'} ?`;
                if (typeof patternType === 'string') {
                  // Direct string pattern
                  clauses.push(clauseString);
                  localParams.push(patternType);
                } else if (typeof patternType === 'object' && patternType !== null) {
                  // Validation step to ensure only one of contains, startsWith, or endsWith is present
                  const patternKeys = ['contains', 'startsWith', 'endsWith'];
                  const presentKeys = patternKeys.filter(key => key in patternType);
                  if (presentKeys.length > 1) {
                    throw new Error(this.printPrefixMessage(`processConditions :: ${operator} :: Only one of 'contains', 'startsWith', or 'endsWith' can be provided`));
                  }
                  switch (true) {
                    case !!patternType.contains:
                      clauses.push(clauseString);
                      localParams.push(`%${patternType.contains}%`);
                      break;
                    case !!patternType.startsWith:
                      clauses.push(clauseString);
                      localParams.push(`${patternType.startsWith}%`);
                      break;
                    case !!patternType.endsWith:
                      clauses.push(clauseString);
                      localParams.push(`%${patternType.endsWith}`);
                      break;
                    default:
                      throw new Error(this.printPrefixMessage(`processConditions :: ${operator} :: Invalid pattern type`));
                  }
                }
                // Handle REGEXP
              } else if (operator === "REGEXP") {
                if (typeof value[operator] !== 'string' || value[operator].length === 0) {
                  throw new Error(this.printPrefixMessage(`processConditions :: ${operator} :: condition must be a non empty string`));
                }
                clauses.push(`${sanitizedKey} REGEXP ?`);
                localParams.push(value[operator]);
                // Handle IS_NULL, IS_NOT_NULL
              } else if (['IS_NULL', 'IS_NOT_NULL'].includes(operator)) {
                if (value[operator] !== true) {
                  throw new Error(this.printPrefixMessage(`processConditions :: ${operator} :: condition must be true`));
                }
                clauses.push(`${sanitizedKey} ${operator.replace(/_/g, ' ')}`);
              } else {
                throw new Error(this.printPrefixMessage(`processConditions :: Unsupported operator: ${operator}`));
              }
            }
          } else {
            // Determine if the value is an array for IN condition
            // e.g. { user_id: [1, 2, 3] } => user_id IN (1, 2, 3)
            if (Array.isArray(value)) {
              if (value.length === 0) {
                throw new Error(this.printPrefixMessage(`processConditions :: ${key} :: condition must be a non-empty array`));
              }
              clauses.push(`${sanitizedKey} IN (${value.map(() => '?').join(', ')})`);
              localParams.push(...value);
            } else {
              // Normal equal condition
              // e.g. { user_id: 1 } => user_id = 1
              clauses.push(`${sanitizedKey} = ?`);
              localParams.push(value);
            }
          }
        }
      }

      return { clause: clauses.join(` ${parentOperator} `), params: localParams };
    };

    return processConditions(conditions, parentOperator);
  }

  // For simple count query e.g. SELECT COUNT(*) FROM table
  count(field: ColumnKeys | (string & {}) = '*', alias?: string): SelectQueryBuilder<ColumnKeys, QueryReturnType> {
    const countClause = `COUNT(${field === '*' ? '*' : '??'})`;
    this.#queryParts.select.sql = `SELECT ${alias ? `${countClause} AS ??` : countClause}`;
    this.#queryParts.select.params = field === '*' ? [] : [field];
    if (alias) {
      this.#queryParts.select.params.push(alias);
    }
    return this;
  }

  max(field: ColumnKeys, alias?: string): SelectQueryBuilder<ColumnKeys, QueryReturnType> {
    this.#queryParts.select.sql = `SELECT MAX(??)${alias ? ` AS ??` : ''}`;
    this.#queryParts.select.params = alias ? [field, alias] : [field];
    return this;
  }

  min(field: ColumnKeys, alias?: string): SelectQueryBuilder<ColumnKeys, QueryReturnType> {
    this.#queryParts.select.sql = `SELECT MIN(??)${alias ? ` AS ??` : ''}`;
    this.#queryParts.select.params = alias ? [field, alias] : [field];
    return this;
  }

  avg(field: ColumnKeys, alias?: string): SelectQueryBuilder<ColumnKeys, QueryReturnType> {
    this.#queryParts.select.sql = `SELECT AVG(??)${alias ? ` AS ??` : ''}`;
    this.#queryParts.select.params = alias ? [field, alias] : [field];
    return this;
  }

  sum(field: ColumnKeys, alias?: string): SelectQueryBuilder<ColumnKeys, QueryReturnType> {
    this.#queryParts.select.sql = `SELECT SUM(??)${alias ? ` AS ??` : ''}`;
    this.#queryParts.select.params = alias ? [field, alias] : [field];
    return this;
  }

  select(fields: SelectFields<ColumnKeys> = "*"): SelectQueryBuilder<ColumnKeys, QueryReturnType> {
    const _fields = Array.isArray(fields) ? this.uniqueFields<ColumnKeys>(fields) : fields;
    const { sql, params } = this.processFields<ColumnKeys>(_fields as SelectFields<ColumnKeys>);
    this.#queryParts.select.sql = sql;
    this.#queryParts.select.params = params;
    return this
  }

  from(table: string, alias?: string): FromQueryBuilder<ColumnKeys, QueryReturnType> {
    this.checkTableName(table, 'from');
    const [tableName, extractedAlias] = this.extractTableAndAlias(table, alias);

    this.#queryParts.from.sql = `FROM ??`;
    this.#queryParts.from.params = [tableName];
    if (extractedAlias) {
      this.#queryParts.from.sql += ` AS ??`;
      this.#queryParts.from.params.push(alias || extractedAlias);
    }
    return this;
  }

  // Overload signatures for the join method
  join(joinType: JoinType, table: string, onCondition: string): JoinQueryBuilder<ColumnKeys, QueryReturnType>;
  join(joinType: JoinType, table: string, alias: string, onCondition: string): JoinQueryBuilder<ColumnKeys, QueryReturnType>;
  // Implementation of the join method
  join(joinType: JoinType, table: string, aliasOrOnCondition: string, onCondition?: string): JoinQueryBuilder<ColumnKeys, QueryReturnType> {
    let alias: string | undefined;
    let actualOnCondition: string;

    if (onCondition) {
      alias = aliasOrOnCondition;
      actualOnCondition = onCondition;
    } else {
      actualOnCondition = aliasOrOnCondition;
    }

    const [tableName, extractedAlias] = this.extractTableAndAlias(table, alias);

    const joinClause = `${joinType.toUpperCase()} JOIN ??${extractedAlias ? ' AS ??' : ''} ON ${actualOnCondition}`;
    const joinParams = extractedAlias ? [tableName, extractedAlias] : [tableName];

    // Append the new join clause and parameters
    this.#queryParts.join.sql += (this.#queryParts.join.sql ? ' ' : '') + joinClause;
    this.#queryParts.join.params.push(...joinParams);
    return this;
  }

  where(conditions: WhereCondition<ColumnKeys>): WhereQueryBuilder<ColumnKeys, QueryReturnType> {
    const { clause, params } = this.buildWhereClause(conditions);
    this.#queryParts.where.sql = clause ? `WHERE ${clause}` : '';
    this.#queryParts.where.params = params;
    return this;
  }

  groupBy(fields: GroupByField<ColumnKeys>): GroupByQueryBuilder<QueryReturnType> {
    if (typeof fields === 'string') {
      this.#queryParts.groupBy.sql = 'GROUP BY ??';
      this.#queryParts.groupBy.params = [fields];
    } else if (Array.isArray(fields) && fields.length > 0) {
      this.#queryParts.groupBy.sql = 'GROUP BY ' + fields.map(() => '??').join(', ');
      this.#queryParts.groupBy.params = fields;
    }
    return this;
  }

  orderBy(fields: OrderByField<ColumnKeys>[]): OrderByQueryBuilder<QueryReturnType> {
    if (!Array.isArray(fields) || fields.length === 0) return this;
    // this.#queryParts.orderBy.sql = `ORDER BY ${fields.map(({ field, direction }) => `?? ${direction || 'ASC'}`).join(', ')}`;
    // this.#queryParts.orderBy.params = fields.map(({ field }) => field);
    const orderItems: string[] = [];
    const params: any[] = [];

    fields.forEach((field) => {
      if (field.raw) {
        // Raw SQL expression - use raw value
        orderItems.push(`${field.raw} ${field.direction || 'ASC'}`);
      } else {
        // Regular column name - use placeholder
        orderItems.push(`?? ${field.direction || 'ASC'}`);
        params.push(field.field);
      }
    });

    this.#queryParts.orderBy.sql = `ORDER BY ${orderItems.join(', ')}`;
    this.#queryParts.orderBy.params = params;

    return this;
  }

  limit(limit?: Limit): LimitQueryBuilder<QueryReturnType> {
    if (limit === undefined || limit === null || Number.isNaN(limit) || limit < 0) return this;
    this.#queryParts.limit.sql = `LIMIT ?`;
    this.#queryParts.limit.params = [limit];
    return this;
  }

  offset(offset?: Offset): OffsetQueryBuilder<QueryReturnType> {
    if (offset === undefined
      || offset === null
      || Number.isNaN(offset)
      || offset < 0
      || this.#queryParts.limit.params.length === 0
    ) return this;
    this.#queryParts.offset.sql = `OFFSET ?`;
    this.#queryParts.offset.params = [offset];
    return this;
  }
  update(table: string): UpdateQueryBuilder<ColumnKeys, QueryReturnType>;
  update(table: string, values: UpdateValue<ColumnKeys>): UpdateQueryBuilderWithoutSet<ColumnKeys, QueryReturnType>;
  update(table: string, values: UpdateValue<ColumnKeys>, options?: UpdateOptions): UpdateQueryBuilderWithoutSet<ColumnKeys, QueryReturnType>;
  update(table: string, values?: UpdateValue<ColumnKeys>, options?: UpdateOptions): UpdateQueryBuilder<ColumnKeys, QueryReturnType> | UpdateQueryBuilderWithoutSet<ColumnKeys, QueryReturnType> {
    this.checkTableName(table, 'update');
    const {
      enableTimestamps = false,
      utimeField = 'utime',
      utimeValue = this.getCurrentUnixTimestamp,
      primaryKey
    } = options || {};

    if (enableTimestamps) {
      const currentTime = typeof utimeValue === 'function' ? utimeValue() : utimeValue;
      if (values) {
        (values as Record<string, any>)[utimeField] = currentTime;
      }
    }

    // Avoid updating the primary key
    if ((primaryKey && values) && (values as Record<string, any>)?.[primaryKey]) {
      delete (values as Record<string, any>)[primaryKey];
    }

    this.#queryParts.update.sql = `UPDATE ??`;
    this.#queryParts.update.params = [table];

    if (values) {
      this.throwEmptyObjectError(values as Object, this.printPrefixMessage('Update :: Data cannot be empty'));
      this.set(values);
      return this as UpdateQueryBuilderWithoutSet<ColumnKeys, QueryReturnType>;
    }
    return this;
  }

  set(values: UpdateValue<ColumnKeys>): SetQueryBuilder<ColumnKeys, QueryReturnType> {
    this.throwEmptyObjectError(values, this.printPrefixMessage('Set :: Values cannot be empty'));
    this.validateIncrementDecrement(values, 'Set');
    const { clauses, params } = this.buildUpdateClausesAndParams(values);
    this.#queryParts.set.sql = `SET ${clauses.join(', ')}`;
    this.#queryParts.set.params = params;
    return this;
  }

  insert(table: string, values: InsertValue<ColumnKeys>, options?: InsertOptions<ColumnKeys>): InsertQueryBuilder<QueryReturnType> {
    this.checkTableName(table, 'insert');
    const {
      enableTimestamps = false,
      ctimeField = 'ctime',
      utimeField = 'utime',
      ctimeValue = this.getCurrentUnixTimestamp,
      utimeValue = this.getCurrentUnixTimestamp,
    } = options || {};

    const isMultipleInsert = Array.isArray(values);
    const rows = isMultipleInsert ? values : [values];

    if (isMultipleInsert && rows.length === 0) {
      throw new Error(this.printPrefixMessage('Insert :: Values cannot be empty'));
    }

    if (enableTimestamps) {
      if (typeof ctimeValue !== 'function') {
        throw new Error(this.printPrefixMessage('ctimeValue must be a function'));
      }
      if (typeof utimeValue !== 'function') {
        throw new Error(this.printPrefixMessage('utimeValue must be a function'));
      }
      rows.forEach((row) => {
        // if not provided, set the current time
        if (!row.hasOwnProperty(ctimeField)) {
          (row as Record<string, any>)[ctimeField] = ctimeValue();
        }
        // if not provided, set the current time
        if (!row.hasOwnProperty(utimeField)) {
          (row as Record<string, any>)[utimeField] = utimeValue();
        }
      });
    }

    const columns = Object.keys(rows[0]); // e.g. ['name', 'email', 'password']
    const placeholders = columns.map(() => '?').join(', '); // e.g. '?, ?, ?'
    const columnPlaceholders = columns.map(() => '??').join(', '); // e.g. 'name, email, password'
    const valueParams = rows.flatMap(row => Object.values(row)); // e.g. ['doe', 'doe@gmail.com', 'password']
    const columnParams = columns;

    const valuesPlaceholders = rows.map(() => `(${placeholders})`).join(', '); // e.g. '(?, ?, ?), (?, ?, ?)'

    let insertClause = `INSERT ${options?.insertIgnore ? 'IGNORE ' : ''}INTO ?? (${columnPlaceholders}) VALUES ${valuesPlaceholders}`;

    if (options?.onDuplicateKeyUpdate) {
      this.validateIncrementDecrement(options.onDuplicateKeyUpdate, 'onDuplicateKeyUpdate');
      const { clauses, params } = this.buildUpdateClausesAndParams(options.onDuplicateKeyUpdate);
      insertClause += ` ON DUPLICATE KEY UPDATE ${clauses.join(', ')}`;
      valueParams.push(...params);
    }

    this.#queryParts.insert.sql = insertClause;
    this.#queryParts.insert.params = [table, ...columnParams, ...valueParams];
    return this;
  }

  deleteFrom(table: string): DeleteQueryBuilder<ColumnKeys, QueryReturnType> {
    this.checkTableName(table, 'delete');
    this.#queryParts.delete.sql = `DELETE FROM ??`;
    this.#queryParts.delete.params = [table];
    return this;
  }

  buildQuery(options?: BuildQueryOptions): {
    sql: string;
    params: any[];
    [Symbol.iterator](): Iterator<any>
  } {
    let sql = '';
    let params: any[] = [];

    // Handle COUNT clause
    if (this.#queryParts.count.sql) {
      sql += this.#queryParts.count.sql;
      params.push(...this.#queryParts.count.params);
    }

    // Handle SELECT clause
    if (this.#queryParts.select.sql) {
      sql += this.#queryParts.select.sql;
      params.push(...this.#queryParts.select.params);
    }

    // Handle UPDATE clause
    if (this.#queryParts.update.sql) {
      sql += this.#queryParts.update.sql;
      params.push(...this.#queryParts.update.params);
    }

    // Handle INSERT clause
    if (this.#queryParts.insert.sql) {
      sql += this.#queryParts.insert.sql;
      params.push(...this.#queryParts.insert.params);
    }

    // Handle DELETE clause
    if (this.#queryParts.delete.sql) {
      sql += this.#queryParts.delete.sql;
      params.push(...this.#queryParts.delete.params);
    }

    // Handle SET clause
    if (this.#queryParts.set.sql) {
      sql += ` ${this.#queryParts.set.sql}`;
      params.push(...this.#queryParts.set.params);
    }

    // Handle FROM clause
    if (this.#queryParts.from.sql) {
      sql += ` ${this.#queryParts.from.sql}`;
      params.push(...this.#queryParts.from.params);
    }

    // Handle JOIN clauses
    if (this.#queryParts.join.sql) {
      sql += ` ${this.#queryParts.join.sql}`;
      params.push(...this.#queryParts.join.params);
    }

    // Handle WHERE clause
    if (this.#queryParts.where.sql) {
      sql += ` ${this.#queryParts.where.sql}`;
      params.push(...this.#queryParts.where.params);
    }

    // Handle GROUP BY clause
    if (this.#queryParts.groupBy.sql) {
      sql += ` ${this.#queryParts.groupBy.sql}`;
      params.push(...this.#queryParts.groupBy.params);
    }

    // Handle ORDER BY clause
    if (this.#queryParts.orderBy.sql) {
      sql += ` ${this.#queryParts.orderBy.sql}`;
      params.push(...this.#queryParts.orderBy.params);
    }

    // Handle LIMIT clause
    if (this.#queryParts.limit.sql) {
      sql += ` ${this.#queryParts.limit.sql}`;
      params.push(...this.#queryParts.limit.params);
    }

    // Handle OFFSET clause
    if (this.#queryParts.offset.sql) {
      sql += ` ${this.#queryParts.offset.sql}`;
      params.push(...this.#queryParts.offset.params);
    }

    // Format SQL if required
    if (options?.format) {
      sql = _format(sql, params);
      params = [];
    }
    return {
      sql,
      params: params,
      // For allowing array destructuring
      [Symbol.iterator](): Iterator<any> {
        let index = 0;
        const values = [sql, params];
        return {
          next(): IteratorResult<any> {
            if (index < values.length) {
              return { value: values[index++], done: false };
            } else {
              return { value: undefined, done: true };
            }
          }
        };
      }
    };
  }

  executeQuery<T = QueryReturnType>(): Promise<T> {
    if (!this.queryFn) throw new Error(this.printPrefixMessage('executeQuery :: Query function is not defined / provided in the constructor'));
    const [sql, params] = this.buildQuery();
    return this.queryFn<T>(sql, params);
  }
}
