import { raw, RawQueryFragment, Utils } from '@mikro-orm/core';
import { QueryType } from '../../query/enums.js';
import { NativeQueryBuilder } from '../../query/NativeQueryBuilder.js';
/** @internal */
export function markOutBindings(obj) {
  Object.defineProperty(obj, '__outBindings', {
    value: true,
    writable: true,
    configurable: true,
    enumerable: false,
  });
}
/** @internal */
export class OracleNativeQueryBuilder extends NativeQueryBuilder {
  as(alias) {
    this.wrap('(', `) ${this.platform.quoteIdentifier(alias)}`);
    return this;
  }
  compile() {
    if (!this.type) {
      throw new Error('No query type provided');
    }
    this.parts.length = 0;
    this.params.length = 0;
    /* v8 ignore next 3: query comment branch */
    if (this.options.comment) {
      this.parts.push(...this.options.comment.map(comment => `/* ${comment} */`));
    }
    let copy;
    if (this.options.onConflict && !Utils.isEmpty(Utils.asArray(this.options.data)[0])) {
      this.compileUpsert();
    } else {
      if (this.options.returning && Array.isArray(this.options.data) && this.options.data.length > 1) {
        copy = [...this.options.data];
        this.options.data.length = 1;
      }
      switch (this.type) {
        case QueryType.SELECT:
        case QueryType.COUNT:
          this.compileSelect();
          break;
        case QueryType.INSERT:
          this.compileInsert();
          break;
        case QueryType.UPDATE:
          this.compileUpdate();
          break;
        case QueryType.DELETE:
          this.compileDelete();
          break;
        case QueryType.TRUNCATE:
          this.compileTruncate();
          break;
      }
      this.addOnConflictClause();
    }
    if (this.options.returning) {
      const isUpsert = this.options.onConflict && !Utils.isEmpty(Utils.asArray(this.options.data)[0]);
      const prefix = isUpsert ? `${this.getTableName()}.` : '';
      const fields = this.options.returning.map(field => prefix + this.quote(Array.isArray(field) ? field[0] : field));
      const into = this.options.returning.map(field => ':out_' + (Array.isArray(field) ? field[0] : field));
      const outBindings = this.options.returning.map(field => {
        const name = 'out_' + (Array.isArray(field) ? field[0] : field);
        const type = Array.isArray(field) ? field[1] : 'string';
        return [name, type];
      });
      markOutBindings(outBindings);
      this.parts.push(`returning ${fields.join(', ')}`);
      this.parts.push(`into ${into.join(', ')}`);
      this.params.push(outBindings);
    }
    this.addLockClause();
    if (!copy) {
      return this.combineParts();
    }
    // multi insert with returning
    const sql = this.parts.join(' ');
    const blockLines = [];
    const block2Lines = [];
    const keys = Object.keys(copy[0]);
    const last = this.params[this.params.length - 1];
    /* v8 ignore next 3: defensive check — output bindings are always set by compile() */
    if (!Array.isArray(last) || !('__outBindings' in last) || !last.__outBindings) {
      throw new Error('Output bindings are required for multi insert with returning');
    }
    const outBindings = {};
    markOutBindings(outBindings);
    for (let i = 0; i < copy.length; i++) {
      const params = [];
      for (const key of keys) {
        /* v8 ignore next 3: undefined value branch in multi-insert */
        if (typeof copy[i][key] === 'undefined') {
          params.push(this.platform.usesDefaultKeyword() ? raw('default') : null);
        } else {
          params.push(copy[i][key]);
        }
      }
      // we need to interpolate to allow proper escaping
      const formatted = this.platform.formatQuery(sql, params).replaceAll(`'`, `''`);
      /* v8 ignore next 3: returning field type branches */
      const using = this.options.returning.map(field => {
        const name = Array.isArray(field) ? field[0] : field;
        const type = Array.isArray(field) ? field[1] : 'string';
        outBindings[`out_${name}__${i}`] = {
          dir: this.platform.mapToBindType('out'),
          type: this.platform.mapToBindType(type),
        };
        return `out :out_${name}__${i}`;
      });
      blockLines.push(` execute immediate '${formatted}' using ${using.join(', ')};`);
      block2Lines.push(` execute immediate '${sql}' using ${using.join(', ')};`);
    }
    const block = `begin\n${blockLines.join('\n')}\n end;`;
    const block2 = `begin\n${block2Lines.join('\n')}\n end;`;
    // save raw query without interpolation for logging,
    Object.defineProperty(outBindings, '__rawQuery', {
      value: block2,
      writable: true,
      configurable: true,
      enumerable: false,
    });
    this.options.data = copy;
    return { sql: block, params: [outBindings] };
  }
  compileTruncate() {
    super.compileTruncate();
    this.parts.push('drop all storage cascade');
  }
  combineParts() {
    let sql = this.parts.join(' ');
    const last = this.params[this.params.length - 1];
    if (this.options.wrap) {
      const [a, b] = this.options.wrap;
      sql = `${a}${sql}${b}`;
    }
    if (!(Array.isArray(last) && '__outBindings' in last && last.__outBindings)) {
      return { sql, params: this.params };
    }
    const out = this.params.pop();
    const outBindings = {};
    markOutBindings(outBindings);
    this.params.push(outBindings);
    for (const item of out) {
      outBindings[item[0]] = {
        dir: this.platform.mapToBindType('out'),
        type: this.platform.mapToBindType(item[1]),
      };
    }
    return { sql, params: this.params };
  }
  compileUpsert() {
    const clause = this.options.onConflict;
    const dataAsArray = Utils.asArray(this.options.data);
    const keys = Object.keys(dataAsArray[0]);
    const parts = [];
    for (const data of dataAsArray) {
      for (const key of keys) {
        this.params.push(data[key]);
      }
      parts.push(`select ${keys.map(k => `? as ${this.quote(k)}`).join(', ')} from dual`);
    }
    this.parts.push(`merge into ${this.getTableName()}`);
    this.parts.push(`using (${parts.join(' union all ')}) tsource`);
    /* v8 ignore next 4: RawQueryFragment conflict fields branch */
    if (clause.fields instanceof RawQueryFragment) {
      this.parts.push(clause.fields.sql);
      this.params.push(...clause.fields.params);
    } else if (clause.fields.length > 0) {
      const fields = clause.fields.map(field => {
        const col = this.quote(field);
        return `${this.getTableName()}.${col} = tsource.${col}`;
      });
      this.parts.push(`on (${fields.join(' and ')})`);
    }
    const sourceColumns = keys.map(field => `tsource.${this.quote(field)}`).join(', ');
    const destinationColumns = keys.map(field => this.quote(field)).join(', ');
    this.parts.push(`when not matched then insert (${destinationColumns}) values (${sourceColumns})`);
    if (!clause.ignore) {
      /* v8 ignore next: merge type branch */
      if (!clause.merge || Array.isArray(clause.merge)) {
        const mergeParts = (clause.merge || keys)
          .filter(field => !Array.isArray(clause.fields) || !clause.fields.includes(field))
          .filter(field => keys.includes(field)) // only reference columns present in the source data
          .map(column => `${this.quote(column)} = tsource.${this.quote(column)}`);
        /* v8 ignore next 10: empty mergeParts branch */
        if (mergeParts.length > 0) {
          this.parts.push('when matched');
          if (clause.where) {
            this.parts.push(`and ${clause.where.sql}`);
            this.params.push(...clause.where.params);
          }
          this.parts.push('then update set');
          this.parts.push(mergeParts.join(', '));
        }
      } /* v8 ignore start: object-form merge branch */ else if (typeof clause.merge === 'object') {
        this.parts.push('when matched');
        if (clause.where) {
          this.parts.push(`and ${clause.where.sql}`);
          this.params.push(...clause.where.params);
        }
        this.parts.push('then update set');
        const parts = Object.entries(clause.merge).map(([key, value]) => {
          this.params.push(value);
          return `${this.getTableName()}.${this.quote(key)} = ?`;
        });
        this.parts.push(parts.join(', '));
      }
      /* v8 ignore stop */
    }
  }
  compileSelect() {
    this.parts.push('select');
    this.addHintComment();
    this.parts.push(`${this.getFields()} from ${this.getTableName()}`);
    if (this.options.joins) {
      for (const join of this.options.joins) {
        this.parts.push(join.sql);
        this.params.push(...join.params);
      }
    }
    if (this.options.where?.sql.trim()) {
      this.parts.push(`where ${this.options.where.sql}`);
      this.params.push(...this.options.where.params);
    }
    if (this.options.groupBy) {
      const fields = this.options.groupBy.map(field => this.quote(field));
      this.parts.push(`group by ${fields.join(', ')}`);
    }
    if (this.options.having) {
      this.parts.push(`having ${this.options.having.sql}`);
      this.params.push(...this.options.having.params);
    }
    if (this.options.orderBy) {
      this.parts.push(`order by ${this.options.orderBy}`);
    }
    if (this.options.offset != null) {
      this.parts.push(`offset ? rows`);
      this.params.push(this.options.offset);
    }
    if (this.options.limit != null) {
      this.parts.push(`fetch next ? rows only`);
      this.params.push(this.options.limit);
    }
  }
}
