import { LockMode, QueryFlag, RawQueryFragment, Utils } from '@mikro-orm/core';
import { NativeQueryBuilder } from '../../query/NativeQueryBuilder.js';
import { QueryType } from '../../query/enums.js';
/** @internal */
export class MsSqlNativeQueryBuilder extends NativeQueryBuilder {
  compile() {
    if (!this.type) {
      throw new Error('No query type provided');
    }
    this.parts.length = 0;
    this.params.length = 0;
    if (this.options.flags?.has(QueryFlag.IDENTITY_INSERT)) {
      this.parts.push(`set identity_insert ${this.getTableName()} on;`);
    }
    const { prefix, suffix } = this.appendOutputTable();
    if (prefix) {
      this.parts.push(prefix);
    }
    if (this.options.comment) {
      this.parts.push(...this.options.comment.map(comment => `/* ${comment} */`));
    }
    this.compileCtes();
    if (this.options.onConflict && !Utils.isEmpty(Utils.asArray(this.options.data)[0])) {
      this.compileUpsert();
    } else {
      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;
      }
      if (suffix) {
        this.parts[this.parts.length - 1] += ';';
        this.parts.push(suffix);
      } else if ([QueryType.INSERT, QueryType.UPDATE, QueryType.DELETE].includes(this.type)) {
        this.parts[this.parts.length - 1] += '; select @@rowcount;';
      }
    }
    if (this.options.flags?.has(QueryFlag.IDENTITY_INSERT)) {
      this.parts.push(`set identity_insert ${this.getTableName()} off;`);
    }
    return this.combineParts();
  }
  compileInsert() {
    if (!this.options.data) {
      throw new Error('No data provided');
    }
    this.parts.push('insert');
    this.addHintComment();
    this.parts.push(`into ${this.getTableName()}`);
    if (Object.keys(this.options.data).length === 0) {
      this.addOutputClause('inserted');
      this.parts.push('default values');
      return;
    }
    const parts = this.processInsertData();
    if (this.options.flags?.has(QueryFlag.OUTPUT_TABLE)) {
      this.parts[this.parts.length - 2] += ' into #out ';
    }
    this.parts.push(parts.join(', '));
  }
  appendOutputTable() {
    if (!this.options.flags?.has(QueryFlag.OUTPUT_TABLE)) {
      return { prefix: '', suffix: '' };
    }
    const returningFields = this.options.returning;
    const selections = returningFields.map(field => `[t].${this.platform.quoteIdentifier(field)}`).join(',');
    return {
      prefix: `select top(0) ${selections} into #out from ${this.getTableName()} as t left join ${this.getTableName()} on 0 = 1;`,
      suffix: `select ${selections} from #out as t; drop table #out`,
    };
  }
  compileUpsert() {
    const clause = this.options.onConflict;
    const dataAsArray = Utils.asArray(this.options.data);
    const keys = Object.keys(dataAsArray[0]);
    const values = keys.map(() => '?');
    const parts = [];
    for (const data of dataAsArray) {
      for (const key of keys) {
        this.params.push(data[key]);
      }
      parts.push(`(${values.join(', ')})`);
    }
    this.parts.push(`merge into ${this.getTableName()}`);
    this.parts.push(`using (values ${parts.join(', ')}) as tsource(${keys.map(key => this.quote(key)).join(', ')})`);
    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) {
      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');
      if (!clause.merge || Array.isArray(clause.merge)) {
        const parts = (clause.merge || keys)
          .filter(field => !Array.isArray(clause.fields) || !clause.fields.includes(field))
          .map(column => `${this.quote(column)} = tsource.${this.quote(column)}`);
        this.parts.push(parts.join(', '));
      } else if (typeof clause.merge === 'object') {
        const parts = Object.entries(clause.merge).map(([key, value]) => {
          this.params.push(value);
          return `${this.getTableName()}.${this.quote(key)} = ?`;
        });
        this.parts.push(parts.join(', '));
      }
    }
    this.addOutputClause('inserted');
    this.parts[this.parts.length - 1] += ';';
  }
  compileSelect() {
    this.parts.push('select');
    if (this.options.limit != null && this.options.offset == null) {
      this.parts.push(`top (?)`);
      this.params.push(this.options.limit);
    }
    this.addHintComment();
    this.parts.push(`${this.getFields()} from ${this.getTableName()}`);
    this.addLockClause();
    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) {
      /* v8 ignore next */
      if (!this.options.orderBy) {
        throw new Error('Order by clause is required for pagination');
      }
      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);
      }
    }
  }
  addLockClause() {
    if (
      !this.options.lockMode ||
      ![LockMode.PESSIMISTIC_READ, LockMode.PESSIMISTIC_WRITE].includes(this.options.lockMode)
    ) {
      return;
    }
    const map = {
      [LockMode.PESSIMISTIC_READ]: 'with (holdlock)',
      [LockMode.PESSIMISTIC_WRITE]: 'with (updlock)',
    };
    if (this.options.lockMode !== LockMode.OPTIMISTIC) {
      this.parts.push(map[this.options.lockMode]);
    }
  }
  compileTruncate() {
    const tableName = this.getTableName();
    const sql = `delete from ${tableName}; declare @count int = case @@rowcount when 0 then 1 else 0 end; dbcc checkident ('${tableName.replace(/[[\]]/g, '')}', reseed, @count)`;
    this.parts.push(sql);
  }
  /** MSSQL has no RECURSIVE keyword — CTEs are implicitly recursive. */
  getCteKeyword(_hasRecursive) {
    return 'with';
  }
}
