import { Condition } from "./Condition";
import { Expression } from "./Expression";
import { ISQLFlavor } from "./Flavor";
import { Q, SelectQuery, Table } from "./Query";
import { compressString } from "./compression";
import { MySQLFlavor } from "./flavors/mysql";
import {
  ICompilable,
  IMetadata,
  IQueryTarget,
  ISequelizable,
  ISequelizableOptions,
  ISerializable,
  ISerializableOptions,
  MetadataOperationType,
  OperationType,
} from "./interfaces";

type RowRecord = Record<string, Expression>;
type RowRecordInput = Record<string, Expression | any>;

export class MutationBase {
  protected _table: Table;

  constructor(table: string, alias?: string) {
    this._table = new Table(table, alias);
  }

  // AST getter for targets
  public getTable(): Table {
    return this._table;
  }

  public getTableNames(): string[] {
    return [this._table.getTableName()];
  }

  public clone(): this {
    const clone = new (this.constructor as any)();
    clone._table = this._table.clone();
    return clone;
  }

  static deserialize(json: string) {
    const parsed = JSON.parse(json);
    switch (parsed.type) {
      case OperationType.DELETE:
        return DeleteMutation.fromJSON(parsed);
      case OperationType.INSERT:
        return InsertMutation.fromJSON(parsed);
      case OperationType.UPDATE:
        return UpdateMutation.fromJSON(parsed);
      default:
        throw new Error("Unknown mutation type");
    }
  }
}

export class DeleteMutation
  extends MutationBase
  implements ISerializable, ISequelizable, IMetadata, ICompilable
{
  protected _where: Condition[] = [];

  public getOperationType(): MetadataOperationType {
    return MetadataOperationType.DELETE;
  }

  // AST getter for targets
  public getWhere(): Condition[] {
    return this._where;
  }

  public clone(): this {
    const clone = super.clone();
    clone._where = [...this._where];
    return clone;
  }

  where(condition: Condition): this {
    const clone = this.clone();
    clone._where.push(condition);
    return clone;
  }

  toSQL(
    flavor: ISQLFlavor = new MySQLFlavor(),
    options?: ISequelizableOptions,
    transformed = false
  ): string {
    if (options?.transformDeleteMutation && !transformed) {
      return options.transformDeleteMutation(this).toSQL(flavor, options, true);
    }
    let sql = `DELETE FROM ${this._table.toSQL(flavor)}`;
    if (this._where.length) {
      sql += ` WHERE ${this._where
        .map((condition) => condition.toSQL(flavor))
        .join(" AND ")}`;
    }
    return sql;
  }

  /**
   * Compile this mutation using the provided target.
   */
  compile<T>(target: IQueryTarget<T>): T {
    return target.compileDelete(this);
  }

  serialize(opts: ISerializableOptions = { compress: false }): string {
    const json = JSON.stringify(this.toJSON());
    return opts.compress ? compressString(json) : json;
  }

  toJSON() {
    return {
      type: OperationType.DELETE,
      table: this._table.toJSON(),
      where: this._where.map((condition) => condition.toJSON()),
    };
  }

  static fromJSON({ table, where }: any): DeleteMutation {
    const deleteMutation = new DeleteMutation(table.source, table.alias);
    deleteMutation._where = where.map((condition: any) =>
      Condition.fromJSON(condition)
    );
    return deleteMutation;
  }
}

export class InsertMutation
  extends MutationBase
  implements ISerializable, ISequelizable, IMetadata, ICompilable
{
  protected _values?: RowRecord[];
  protected _selectWithColumns: [SelectQuery, string[] | undefined];

  public getOperationType(): MetadataOperationType {
    return MetadataOperationType.INSERT;
  }

  // AST getters for targets
  public getValues(): RowRecord[] | undefined {
    return this._values;
  }
  public getSelectWithColumns(): [SelectQuery, string[] | undefined] | undefined {
    return this._selectWithColumns;
  }

  public clone(): this {
    const clone = super.clone();
    clone._values = this._values && [...this._values];
    return clone;
  }

  removeAllValues(): this {
    const clone = this.clone();
    clone._values = undefined;
    return clone;
  }

  allValues(): RowRecord[] {
    return this._values;
  }

  values(values: RowRecordInput[]): this {
    const clone = this.clone();
    if (clone._selectWithColumns) throw new Error("select already set");
    clone._values = [...(clone._values ?? []), ...values];
    return clone;
  }

  select(query: SelectQuery, columns?: string[]): this {
    const clone = this.clone();
    if (clone._values) throw new Error("values already set");
    clone._selectWithColumns = [query, columns];
    return clone;
  }

  toSQL(
    flavor: ISQLFlavor = new MySQLFlavor(),
    options?: ISequelizableOptions,
    transformed = false
  ): string {
    if (options?.transformInsertMutation && !transformed) {
      return options.transformInsertMutation(this).toSQL(flavor, options, true);
    }
    if (this._values) {
      return `INSERT INTO ${this._table.toSQL(flavor)} (${Object.keys(
        this._values[0]
      )
        .map((k) => flavor.escapeColumn(k))
        .join(", ")}) VALUES ${this._values
        .map(
          (value) =>
            `(${Object.values(value)
              .map((v) => flavor.escapeValue(v))
              .join(", ")})`
        )
        .join(", ")}`;
    }
    if (this._selectWithColumns) {
      const [query, columns] = this._selectWithColumns;
      return `INSERT INTO ${this._table.toSQL(flavor)}${
        columns
          ? ` (${columns.map((k) => flavor.escapeColumn(k)).join(", ")})`
          : ""
      } ${query.toSQL(flavor)}`;
    }
    throw new Error("values or select must be set for insert query");
  }

  /**
   * Compile this mutation using the provided target.
   */
  compile<T>(target: IQueryTarget<T>): T {
    return target.compileInsert(this);
  }

  serialize(opts: ISerializableOptions = { compress: false }): string {
    const json = JSON.stringify(this.toJSON());
    return opts.compress ? compressString(json) : json;
  }

  toJSON() {
    return {
      type: OperationType.INSERT,
      table: this._table.toJSON(),
      values: this._values,
      select: this._selectWithColumns && [
        this._selectWithColumns[0].toJSON(),
        this._selectWithColumns[1],
      ],
    };
  }

  static fromJSON({ table, values, select }: any): InsertMutation {
    const insertMutation = new InsertMutation(table.source, table.alias);
    insertMutation._values = values;
    if (select) {
      insertMutation._selectWithColumns = [
        SelectQuery.fromJSON(select[0]),
        select[1],
      ];
    }
    return insertMutation;
  }
}

export class UpdateMutation
  extends MutationBase
  implements ISerializable, ISequelizable, IMetadata, ICompilable
{
  protected _values: RowRecord = {};
  protected _where: Condition[] = [];

  public getOperationType(): MetadataOperationType {
    return MetadataOperationType.UPDATE;
  }

  // AST getters for targets
  public getSetValues(): RowRecord {
    return this._values;
  }
  public getWhere(): Condition[] {
    return this._where;
  }

  public clone(): this {
    const clone = super.clone();
    clone._values = { ...this._values };
    clone._where = [...this._where];
    return clone;
  }

  set(values: RowRecordInput): this {
    const clone = this.clone();
    const _values = {};
    for (const [key, value] of Object.entries(values)) {
      if (value === null) {
        _values[key] = Q.null();
        continue;
      }
      if (value instanceof SelectQuery) {
        _values[key] = value;
        continue;
      }
      _values[key] = value instanceof Expression ? value : Q.exprValue(value);
    }
    clone._values = { ...clone._values, ..._values };
    return clone;
  }

  where(condition: Condition): this {
    const clone = this.clone();
    clone._where.push(condition);
    return clone;
  }

  toSQL(
    flavor: ISQLFlavor = new MySQLFlavor(),
    options?: ISequelizableOptions,
    transformed = false
  ): string {
    if (options?.transformUpdateMutation && !transformed) {
      return options.transformUpdateMutation(this).toSQL(flavor, options, true);
    }
    if (!this._values) throw new Error("No values to update");

    let sql = `UPDATE ${this._table.toSQL(flavor)} SET ${Object.entries(
      this._values
    )
      .map(([key, value]) => {
        return `${flavor.escapeColumn(key)} = ${
          value instanceof SelectQuery
            ? `(${value.toSQL(flavor)})`
            : value.toSQL(flavor)
        }`;
      })
      .join(", ")}`;
    if (this._where.length) {
      sql += ` WHERE ${this._where
        .map((condition) => condition.toSQL(flavor))
        .join(" AND ")}`;
    }
    return sql;
  }

  /**
   * Compile this mutation using the provided target.
   */
  compile<T>(target: IQueryTarget<T>): T {
    return target.compileUpdate(this);
  }

  serialize(opts: ISerializableOptions = { compress: false }): string {
    const json = JSON.stringify(this.toJSON());
    return opts.compress ? compressString(json) : json;
  }

  toJSON() {
    const _values = {};
    for (const [key, value] of Object.entries(this._values)) {
      _values[key] = value.serialize();
    }
    return {
      type: OperationType.UPDATE,
      table: this._table.toJSON(),
      values: _values,
      where: this._where.map((condition) => condition.toJSON()),
    };
  }

  static fromJSON({ table, values, where }: any): UpdateMutation {
    const updateMutation = new UpdateMutation(table.source, table.alias);
    const _values = {};
    for (const [key, value] of Object.entries(values)) {
      _values[key] = Q.deserializeRaw(value as string);
    }
    updateMutation._values = _values;
    updateMutation._where = where.map((condition: any) =>
      Condition.fromJSON(condition)
    );
    return updateMutation;
  }
}
