import { Dayjs } from "dayjs";
import { ExpressionBase, ExpressionValue } from "./Expression";
import { ISQLFlavor } from "./Flavor";
import { Q, SelectQuery } from "./Query";
import { ISequelizable, ISerializable } from "./interfaces";

type ConditionValue = ExpressionValue | Dayjs;

// Values accepted by IN / NOT IN: a list of values, a subquery, or null.
type InValues = ConditionValue[] | SelectQuery | null;

// Normalizes the value(s) passed to an IN/NOT IN condition.
// - A SelectQuery (passed directly or as the only element of an array) becomes a subquery.
// - An empty/null list is rendered fail-closed as `(NULL)` so an empty IN never silently
//   drops the filter (which would expose all rows). The NULL marker survives serialization.
const normalizeInValues = (
  values: InValues
): { subquery?: SelectQuery; values: ExpressionBase[] } => {
  if (values instanceof SelectQuery) {
    return { subquery: values, values: [] };
  }
  const arr = values ?? [];
  const subqueryCount = arr.filter((v) => v instanceof SelectQuery).length;
  if (subqueryCount > 0) {
    // A subquery is only valid as the sole value: `IN (SELECT ...)`. Mixing a
    // subquery with scalar values (or multiple subqueries) is invalid SQL — fail
    // loud rather than emitting malformed SQL / throwing later on deserialize.
    if (subqueryCount > 1 || arr.length > 1) {
      throw new Error(
        "Cond.in/notIn: cannot mix a subquery with other values or use multiple subqueries; pass a single subquery or a list of scalar values"
      );
    }
    return { subquery: arr[0] as SelectQuery, values: [] };
  }
  if (arr.length === 0) {
    return { values: [Q.exprValue(Q.raw("NULL"))] };
  }
  return { values: arr.map((v) => Q.exprValue(v)) };
};

export class Condition implements ISequelizable, ISerializable {
  toSQL(flavor: ISQLFlavor): string {
    throw new Error("Method not implemented.");
  }
  toJSON(): any {
    throw new Error("Method not implemented.");
  }
  serialize(): string {
    return JSON.stringify(this.toJSON());
  }
  static fromJSON(json: any): Condition {
    switch (json.type) {
      case "BinaryCondition":
        return BinaryCondition.fromJSON(json);
      case "LogicalCondition":
        return LogicalCondition.fromJSON(json);
      case "BetweenCondition":
        return BetweenCondition.fromJSON(json);
      case "InCondition":
        return InCondition.fromJSON(json);
      case "NotInCondition":
        return NotInCondition.fromJSON(json);
      case "NullCondition":
        return NullCondition.fromJSON(json);
      case "LikeCondition":
        return LikeCondition.fromJSON(json);
      case "ColumnComparisonCondition":
        return ColumnComparisonCondition.fromJSON(json);
      case "NotCondition":
        return NotCondition.fromJSON(json);
      default:
        throw new Error(
          `Unknown condition type: ${json.type} (${JSON.stringify(json)})`
        );
    }
  }
  static deserialize(value: Condition | string): Condition | null {
    if (typeof value === "string") {
      try {
        return Condition.fromJSON(JSON.parse(value));
      } catch {
        return null;
      }
    }
    return value;
  }
}

type Operator = "=" | "!=" | ">" | "<" | ">=" | "<=";

class BinaryCondition extends Condition {
  key: ExpressionBase;
  value: ExpressionBase;
  operator: Operator;

  constructor(key: ConditionValue, value: ConditionValue, operator: Operator) {
    super();
    this.key = Q.expr(key);
    this.value = Q.value(value);
    this.operator = operator;
  }

  toSQL(flavor: ISQLFlavor): string {
    return `${this.key.toSQL(flavor)} ${this.operator} ${this.value.toSQL(
      flavor
    )}`;
  }

  // serialization
  toJSON(): any {
    return {
      type: "BinaryCondition",
      key: this.key.serialize(),
      value: this.value.serialize(),
      operator: this.operator,
    };
  }

  static fromJSON(json: any): BinaryCondition {
    return new BinaryCondition(
      ExpressionBase.deserialize(json.key),
      ExpressionBase.deserializeValue(json.value),
      json.operator
    );
  }
}

class LogicalCondition extends Condition {
  conditions: Condition[];
  operator: "AND" | "OR";

  constructor(conditions: Condition[], operator: "AND" | "OR") {
    super();
    this.conditions = conditions;
    this.operator = operator;
  }

  toSQL(flavor: ISQLFlavor): string {
    return `(${this.conditions
      .map((c) => c.toSQL(flavor))
      .join(` ${this.operator} `)})`;
  }

  // serialization
  toJSON(): any {
    return {
      type: "LogicalCondition",
      conditions: this.conditions.map((condition) => condition.toJSON()),
      operator: this.operator,
    };
  }

  static fromJSON(json: any): LogicalCondition {
    const conditions = json.conditions.map(Condition.fromJSON);
    return new LogicalCondition(conditions, json.operator);
  }
}

class BetweenCondition extends Condition {
  key: ExpressionBase;
  from: ExpressionBase;
  to: ExpressionBase;

  constructor(key: ConditionValue, from: ConditionValue, to: ConditionValue) {
    super();
    this.key = Q.expr(key);
    this.from = Q.expr(from);
    this.to = Q.expr(to);
  }

  toSQL(flavor: ISQLFlavor): string {
    return `${this.key.toSQL(flavor)} BETWEEN ${this.from.toSQL(
      flavor
    )} AND ${this.to.toSQL(flavor)}`;
  }

  // serialization
  toJSON(): any {
    return {
      type: "BetweenCondition",
      key: this.key.serialize(),
      from: this.from.serialize(),
      to: this.to.serialize(),
    };
  }

  static fromJSON(json: any): BetweenCondition {
    return new BetweenCondition(
      ExpressionBase.deserialize(json.key),
      ExpressionBase.deserializeValue(json.from),
      ExpressionBase.deserializeValue(json.to)
    );
  }
}

class InCondition extends Condition {
  key: ExpressionBase;
  values: ExpressionBase[];
  subquery?: SelectQuery;

  constructor(key: ConditionValue, values: InValues) {
    super();
    this.key = Q.expr(key);
    const normalized = normalizeInValues(values);
    this.values = normalized.values;
    this.subquery = normalized.subquery;
  }

  toSQL(flavor: ISQLFlavor): string {
    const right = this.subquery
      ? this.subquery.toSQL(flavor)
      : this.values.map((v) => v.toSQL(flavor)).join(", ");
    return `${this.key.toSQL(flavor)} IN (${right})`;
  }

  // serialization
  toJSON(): any {
    return {
      type: "InCondition",
      key: this.key.serialize(),
      ...(this.subquery
        ? { subquery: this.subquery.toJSON() }
        : { values: this.values.map((v) => v.serialize()) }),
    };
  }

  static fromJSON(json: any): InCondition {
    if (json.subquery) {
      return new InCondition(
        ExpressionBase.deserialize(json.key),
        SelectQuery.fromJSON(json.subquery)
      );
    }
    return new InCondition(
      ExpressionBase.deserialize(json.key),
      json.values.map(ExpressionBase.deserializeValue)
    );
  }
}

class NotInCondition extends Condition {
  key: ExpressionBase;
  values: ExpressionBase[];
  subquery?: SelectQuery;

  constructor(key: ConditionValue, values: InValues) {
    super();
    this.key = Q.expr(key);
    const normalized = normalizeInValues(values);
    this.values = normalized.values;
    this.subquery = normalized.subquery;
  }

  toSQL(flavor: ISQLFlavor): string {
    const right = this.subquery
      ? this.subquery.toSQL(flavor)
      : this.values.map((v) => v.toSQL(flavor)).join(", ");
    return `${this.key.toSQL(flavor)} NOT IN (${right})`;
  }

  // serialization
  toJSON(): any {
    return {
      type: "NotInCondition",
      key: this.key.serialize(),
      ...(this.subquery
        ? { subquery: this.subquery.toJSON() }
        : { values: this.values.map((v) => v.serialize()) }),
    };
  }

  static fromJSON(json: any): NotInCondition {
    if (json.subquery) {
      return new NotInCondition(
        ExpressionBase.deserialize(json.key),
        SelectQuery.fromJSON(json.subquery)
      );
    }
    return new NotInCondition(
      ExpressionBase.deserialize(json.key),
      json.values.map(ExpressionBase.deserializeValue)
    );
  }
}

class NullCondition extends Condition {
  key: ExpressionBase;
  isNull: boolean;

  constructor(key: ConditionValue, isNull: boolean) {
    super();
    this.key = Q.expr(key);
    this.isNull = isNull;
  }

  toSQL(flavor: ISQLFlavor): string {
    return `${this.key.toSQL(flavor)} IS ${this.isNull ? "" : "NOT "}NULL`;
  }

  // serialization
  toJSON(): any {
    return {
      type: "NullCondition",
      key: this.key.serialize(),
      isNull: this.isNull,
    };
  }

  static fromJSON(json: any): NullCondition {
    return new NullCondition(ExpressionBase.deserialize(json.key), json.isNull);
  }
}

class LikeCondition extends Condition {
  key: ExpressionBase;
  pattern: string;
  isLike: boolean;
  caseInsensitive: boolean;

  constructor(
    key: ConditionValue,
    pattern: string,
    isLike: boolean,
    caseInsensitive: boolean = false
  ) {
    super();
    this.key = Q.expr(key);
    this.pattern = pattern;
    this.isLike = isLike;
    this.caseInsensitive = caseInsensitive;
  }

  toSQL(flavor: ISQLFlavor): string {
    const escapedColumn = this.key.toSQL(flavor);
    const escapedPattern = flavor.escapeValue(this.pattern);
    return flavor.escapeLikeCondition(
      escapedColumn,
      escapedPattern,
      this.isLike,
      this.caseInsensitive
    );
  }

  // serialization
  toJSON(): any {
    return {
      type: "LikeCondition",
      key: this.key.serialize(),
      pattern: this.pattern,
      isLike: this.isLike,
      caseInsensitive: this.caseInsensitive,
    };
  }

  static fromJSON(json: any): LikeCondition {
    return new LikeCondition(
      ExpressionBase.deserialize(json.key),
      json.pattern,
      json.isLike,
      json.caseInsensitive ?? false
    );
  }
}

class ColumnComparisonCondition extends Condition {
  leftKey: ExpressionBase;
  rightKey: ExpressionBase;
  operator: Operator;

  constructor(
    leftKey: ConditionValue,
    rightKey: ConditionValue,
    operator: Operator
  ) {
    super();
    this.leftKey = Q.expr(leftKey);
    this.rightKey = Q.expr(rightKey);
    this.operator = operator;
  }

  toSQL(flavor: ISQLFlavor): string {
    return `${this.leftKey.toSQL(flavor)} ${
      this.operator
    } ${this.rightKey.toSQL(flavor)}`;
  }

  // serialization
  toJSON(): any {
    return {
      type: "ColumnComparisonCondition",
      leftKey: this.leftKey.serialize(),
      rightKey: this.rightKey.serialize(),
      operator: this.operator,
    };
  }

  static fromJSON(json: any): ColumnComparisonCondition {
    return new ColumnComparisonCondition(
      ExpressionBase.deserialize(json.leftKey),
      ExpressionBase.deserialize(json.rightKey),
      json.operator
    );
  }
}

class NotCondition extends Condition {
  condition: Condition;

  constructor(condition: Condition) {
    super();
    this.condition = condition;
  }

  toSQL(flavor: ISQLFlavor): string {
    return `NOT (${this.condition.toSQL(flavor)})`;
  }

  // serialization
  toJSON(): any {
    return {
      type: "NotCondition",
      condition: this.condition.toJSON(),
    };
  }

  static fromJSON(json: any): NotCondition {
    return new NotCondition(Condition.fromJSON(json.condition));
  }
}

export const Conditions = {
  fromString: (
    column: string,
    value: string | number,
    delimiters: string[] = [" "]
  ): Condition => {
    const str = `${value}`;
    if (str.indexOf(">=") === 0) {
      return Conditions.greaterThanOrEqual(column, str.substring(2));
    }
    if (str.indexOf("<=") === 0) {
      return Conditions.lessThanOrEqual(column, str.substring(2));
    }
    if (str.indexOf(">") === 0) {
      return Conditions.greaterThan(column, str.substring(1));
    }
    if (str.indexOf("<") === 0) {
      return Conditions.lessThan(column, str.substring(1));
    }
    if (str.indexOf("~") === 0) {
      const searchValue = str.substring(1);
      const conditions = [Conditions.like(column, `${searchValue}%`)];
      for (const delimiter of delimiters) {
        conditions.push(
          Conditions.like(column, `%${delimiter}${searchValue}%`)
        );
      }
      return Conditions.or(conditions);
    }
    if (str.indexOf("!~") === 0) {
      const searchValue = str.substring(2);
      const conditions = [Conditions.notLike(column, `${searchValue}%`)];
      for (const delimiter of delimiters) {
        conditions.push(
          Conditions.notLike(column, `%${delimiter}${searchValue}%`)
        );
      }
      return Conditions.and(conditions);
    }
    if (str.indexOf("!") === 0) {
      return Conditions.notEqual(column, `${str.substring(1)}%`);
    }
    return Conditions.equal(column, str);
  },
  equal: (key: ConditionValue, value: ConditionValue) =>
    new BinaryCondition(key, value, "="),
  notEqual: (key: ConditionValue, value: ConditionValue) =>
    new BinaryCondition(key, value, "!="),
  greaterThan: (key: ConditionValue, value: ConditionValue) =>
    new BinaryCondition(key, value, ">"),
  lessThan: (key: ConditionValue, value: ConditionValue) =>
    new BinaryCondition(key, value, "<"),
  greaterThanOrEqual: (key: ConditionValue, value: ConditionValue) =>
    new BinaryCondition(key, value, ">="),
  lessThanOrEqual: (key: ConditionValue, value: ConditionValue) =>
    new BinaryCondition(key, value, "<="),
  between: (key: ConditionValue, values: [ConditionValue, ConditionValue]) =>
    new BetweenCondition(key, Q.exprValue(values[0]), Q.exprValue(values[1])),
  // Fail-closed: an empty/null list renders `IN (NULL)` (never matches) instead of being
  // dropped, so an empty IN can never silently expand visibility. Accepts a subquery too.
  in: (key: ConditionValue, values: InValues) => new InCondition(key, values),
  notIn: (key: ConditionValue, values: InValues) =>
    new NotInCondition(key, values),
  and: (conditions: (Condition | null)[] | null) => {
    const _c = (conditions || []).filter((c) => c !== null);
    return _c.length > 0 ? new LogicalCondition(_c, "AND") : null;
  },
  or: (conditions: (Condition | null)[] | null) => {
    const _c = (conditions || []).filter((c) => c !== null);
    return _c.length > 0 ? new LogicalCondition(_c, "OR") : null;
  },
  isNull: (key: ConditionValue) => new NullCondition(key, true),
  isNotNull: (key: ConditionValue) => new NullCondition(key, false),
  // Deprecated: use isNull instead
  null: (key: ConditionValue) => new NullCondition(key, true),
  // Deprecated: use isNotNull instead
  notNull: (key: ConditionValue) => new NullCondition(key, false),
  like: (
    key: ConditionValue,
    pattern: string,
    opts?: { caseInsensitive?: boolean }
  ) => new LikeCondition(key, pattern, true, opts?.caseInsensitive ?? false),
  notLike: (
    key: ConditionValue,
    pattern: string,
    opts?: { caseInsensitive?: boolean }
  ) => new LikeCondition(key, pattern, false, opts?.caseInsensitive ?? false),
  ilike: (key: ConditionValue, pattern: string) =>
    new LikeCondition(key, pattern, true, true),
  notIlike: (key: ConditionValue, pattern: string) =>
    new LikeCondition(key, pattern, false, true),
  columnEqual: (leftKey: ConditionValue, rightKey: ConditionValue) =>
    new ColumnComparisonCondition(leftKey, rightKey, "="),
  columnNotEqual: (leftKey: ConditionValue, rightKey: ConditionValue) =>
    new ColumnComparisonCondition(leftKey, rightKey, "!="),
  columnGreaterThan: (leftKey: ConditionValue, rightKey: ConditionValue) =>
    new ColumnComparisonCondition(leftKey, rightKey, ">"),
  columnLessThan: (leftKey: ConditionValue, rightKey: ConditionValue) =>
    new ColumnComparisonCondition(leftKey, rightKey, "<"),
  columnGreaterThanOrEqual: (leftKey: ConditionValue, rightKey: ConditionValue) =>
    new ColumnComparisonCondition(leftKey, rightKey, ">="),
  columnLessThanOrEqual: (leftKey: ConditionValue, rightKey: ConditionValue) =>
    new ColumnComparisonCondition(leftKey, rightKey, "<="),
  not: (condition: Condition) => new NotCondition(condition),
};
export { Conditions as Cond };

// Export condition subclasses for use by query targets
export {
  BinaryCondition,
  LogicalCondition,
  BetweenCondition,
  InCondition,
  NotInCondition,
  NullCondition,
  LikeCondition,
  ColumnComparisonCondition,
  NotCondition,
};

// Export operator type for targets
export type { Operator };
