import { assert } from "../../../shared/src/asserts.js";
import { SUBQ_PREFIX } from "../../../zero-protocol/src/ast.js";
import { hashOfAST } from "../../../zero-protocol/src/query-hash.js";
import { NotImplementedError } from "../error.js";
import { defaultFormat } from "../../../zero-types/src/format.js";
import { cmp, and, simplifyCondition, ExpressionBuilder } from "./expression.js";
import { queryInternalsTag } from "./query-internals.js";
function newQuery(schema, table) {
  return newQueryImpl(schema, table, { table }, defaultFormat, "client");
}
function newQueryImpl(schema, tableName, ast, format, system) {
  const inner = (tableName2, ast2, format2, customQueryID, currentJunction) => new QueryImpl(
    schema,
    tableName2,
    ast2,
    format2,
    system,
    customQueryID,
    currentJunction,
    inner
  );
  return inner(tableName, ast, format, void 0, void 0);
}
class QueryImpl {
  [queryInternalsTag] = true;
  #schema;
  #tableName;
  #ast;
  format;
  #hash = "";
  #system;
  #currentJunction;
  customQueryID;
  #newQuery;
  constructor(schema, tableName, ast, format, system, customQueryID, currentJunction, newQuery2) {
    this.#schema = schema;
    this.#tableName = tableName;
    this.#ast = ast;
    this.format = format;
    this.#system = system;
    this.#currentJunction = currentJunction;
    this.customQueryID = customQueryID;
    this.#newQuery = newQuery2;
  }
  run(_options) {
    throwQueryNotRunnable();
  }
  preload(_options) {
    throwQueryNotRunnable();
  }
  materialize(_factoryOrTTL, _ttl) {
    throwQueryNotRunnable();
  }
  nameAndArgs(name, args) {
    return this.#newQuery(
      this.#tableName,
      this.#ast,
      this.format,
      {
        name,
        args
      },
      this.#currentJunction
    );
  }
  hash() {
    if (!this.#hash) {
      this.#hash = hashOfAST(this.#ast);
    }
    return this.#hash;
  }
  one = () => this.#newQuery(
    this.#tableName,
    {
      ...this.#ast,
      limit: 1
    },
    {
      ...this.format,
      singular: true
    },
    this.customQueryID,
    this.#currentJunction
  );
  whereExists = (relationship, cbOrOptions, options) => {
    const cb = typeof cbOrOptions === "function" ? cbOrOptions : void 0;
    const opts = typeof cbOrOptions === "function" ? options : cbOrOptions;
    return this.where(({ exists }) => exists(relationship, cb, opts));
  };
  related = (relationship, cb) => {
    if (relationship.startsWith(SUBQ_PREFIX)) {
      throw new Error(
        `Relationship names may not start with "${SUBQ_PREFIX}". That is a reserved prefix.`
      );
    }
    cb = cb ?? ((q) => q);
    const related = this.#schema.relationships[this.#tableName][relationship];
    assert(related, "Invalid relationship");
    if (isOneHop(related)) {
      const { destSchema, destField, sourceField, cardinality } = related[0];
      const q = this.#newQuery(
        destSchema,
        {
          table: destSchema,
          alias: relationship
        },
        {
          relationships: {},
          singular: cardinality === "one"
        },
        this.customQueryID,
        void 0
      );
      const subQuery = asQueryImpl(cb(q));
      assert(
        isCompoundKey(sourceField),
        "The source of a relationship must specify at last 1 field"
      );
      assert(
        isCompoundKey(destField),
        "The destination of a relationship must specify at last 1 field"
      );
      assert(
        sourceField.length === destField.length,
        "The source and destination of a relationship must have the same number of fields"
      );
      return this.#newQuery(
        this.#tableName,
        {
          ...this.#ast,
          related: [
            ...this.#ast.related ?? [],
            {
              system: this.#system,
              correlation: {
                parentField: sourceField,
                childField: destField
              },
              subquery: subQuery.#ast
            }
          ]
        },
        {
          ...this.format,
          relationships: {
            ...this.format.relationships,
            [relationship]: subQuery.format
          }
        },
        this.customQueryID,
        this.#currentJunction
      );
    }
    if (isTwoHop(related)) {
      const [firstRelation, secondRelation] = related;
      const { destSchema } = secondRelation;
      const junctionSchema = firstRelation.destSchema;
      const sq = asQueryImpl(
        cb(
          this.#newQuery(
            destSchema,
            {
              table: destSchema,
              alias: relationship
            },
            {
              relationships: {},
              singular: secondRelation.cardinality === "one"
            },
            this.customQueryID,
            relationship
          )
        )
      );
      assert(isCompoundKey(firstRelation.sourceField), "Invalid relationship");
      assert(isCompoundKey(firstRelation.destField), "Invalid relationship");
      assert(isCompoundKey(secondRelation.sourceField), "Invalid relationship");
      assert(isCompoundKey(secondRelation.destField), "Invalid relationship");
      return this.#newQuery(
        this.#tableName,
        {
          ...this.#ast,
          related: [
            ...this.#ast.related ?? [],
            {
              system: this.#system,
              correlation: {
                parentField: firstRelation.sourceField,
                childField: firstRelation.destField
              },
              hidden: true,
              subquery: {
                table: junctionSchema,
                alias: relationship,
                related: [
                  {
                    system: this.#system,
                    correlation: {
                      parentField: secondRelation.sourceField,
                      childField: secondRelation.destField
                    },
                    subquery: sq.#ast
                  }
                ]
              }
            }
          ]
        },
        {
          ...this.format,
          relationships: {
            ...this.format.relationships,
            [relationship]: sq.format
          }
        },
        this.customQueryID,
        this.#currentJunction
      );
    }
    throw new Error(`Invalid relationship ${relationship}`);
  };
  where = function(fieldOrExpressionFactory, opOrValue, value) {
    let cond;
    if (typeof fieldOrExpressionFactory === "function") {
      cond = fieldOrExpressionFactory(this.expressionBuilder());
    } else {
      assert(arguments.length >= 2, "Invalid condition. Too few arguments.");
      if (arguments.length === 2) {
        cond = cmp(fieldOrExpressionFactory, opOrValue);
      } else {
        cond = cmp(fieldOrExpressionFactory, opOrValue, value);
      }
    }
    const existingWhere = this.#ast.where;
    if (existingWhere) {
      cond = and(existingWhere, cond);
    }
    const where = simplifyCondition(cond);
    return this.#newQuery(
      this.#tableName,
      {
        ...this.#ast,
        where
      },
      this.format,
      this.customQueryID,
      this.#currentJunction
    );
  }.bind(this);
  start = (row, opts) => this.#newQuery(
    this.#tableName,
    {
      ...this.#ast,
      start: {
        row,
        exclusive: !opts?.inclusive
      }
    },
    this.format,
    this.customQueryID,
    this.#currentJunction
  );
  limit = (limit) => {
    if (limit < 0) {
      throw new Error("Limit must be non-negative");
    }
    if ((limit | 0) !== limit) {
      throw new Error("Limit must be an integer");
    }
    if (this.#currentJunction) {
      throw new NotImplementedError(
        "Limit is not supported in junction relationships yet. Junction relationship being limited: " + this.#currentJunction
      );
    }
    return this.#newQuery(
      this.#tableName,
      {
        ...this.#ast,
        limit
      },
      this.format,
      this.customQueryID,
      this.#currentJunction
    );
  };
  orderBy = (field, direction) => {
    if (this.#currentJunction) {
      throw new NotImplementedError(
        "Order by is not supported in junction relationships yet. Junction relationship being ordered: " + this.#currentJunction
      );
    }
    return this.#newQuery(
      this.#tableName,
      {
        ...this.#ast,
        orderBy: [...this.#ast.orderBy ?? [], [field, direction]]
      },
      this.format,
      this.customQueryID,
      this.#currentJunction
    );
  };
  #exists = (relationship, cb, options) => {
    cb = cb ?? ((q) => q);
    const flip = options?.flip;
    const scalar = options?.scalar;
    const related = this.#schema.relationships[this.#tableName][relationship];
    assert(related, "Invalid relationship");
    if (isOneHop(related)) {
      const { destSchema: destTableName, sourceField, destField } = related[0];
      assert(isCompoundKey(sourceField), "Invalid relationship");
      assert(isCompoundKey(destField), "Invalid relationship");
      const subQuery = asQueryImpl(
        cb(
          this.#newQuery(
            destTableName,
            {
              table: destTableName,
              alias: `${SUBQ_PREFIX}${relationship}`
            },
            defaultFormat,
            this.customQueryID,
            void 0
          )
        )
      );
      return {
        type: "correlatedSubquery",
        related: {
          system: this.#system,
          correlation: {
            parentField: sourceField,
            childField: destField
          },
          subquery: subQuery.#ast
        },
        op: "EXISTS",
        ...flip !== void 0 ? { flip } : {},
        ...scalar !== void 0 ? { scalar } : {}
      };
    }
    assert(!scalar, "scalar option only supports one-hop relationships");
    if (isTwoHop(related)) {
      const [firstRelation, secondRelation] = related;
      assert(isCompoundKey(firstRelation.sourceField), "Invalid relationship");
      assert(isCompoundKey(firstRelation.destField), "Invalid relationship");
      assert(isCompoundKey(secondRelation.sourceField), "Invalid relationship");
      assert(isCompoundKey(secondRelation.destField), "Invalid relationship");
      const { destSchema } = secondRelation;
      const junctionSchema = firstRelation.destSchema;
      const queryToDest = cb(
        this.#newQuery(
          destSchema,
          {
            table: destSchema,
            alias: `${SUBQ_PREFIX}zhidden_${relationship}`
          },
          defaultFormat,
          this.customQueryID,
          relationship
        )
      );
      return {
        type: "correlatedSubquery",
        related: {
          system: this.#system,
          correlation: {
            parentField: firstRelation.sourceField,
            childField: firstRelation.destField
          },
          subquery: {
            table: junctionSchema,
            alias: `${SUBQ_PREFIX}${relationship}`,
            where: {
              type: "correlatedSubquery",
              related: {
                system: this.#system,
                correlation: {
                  parentField: secondRelation.sourceField,
                  childField: secondRelation.destField
                },
                subquery: asQueryImpl(queryToDest).#ast
              },
              op: "EXISTS",
              ...flip !== void 0 ? { flip } : {}
            }
          }
        },
        op: "EXISTS",
        ...flip !== void 0 ? { flip } : {}
      };
    }
    throw new Error(`Invalid relationship ${relationship}`);
  };
  get ast() {
    return this.#ast;
  }
  expressionBuilder() {
    return new ExpressionBuilder(this.#exists);
  }
}
function asQueryImpl(q) {
  assert(q instanceof QueryImpl, "Expected QueryImpl instance");
  return q;
}
function throwQueryNotRunnable() {
  throw new Error("Query is not runnable");
}
function isCompoundKey(field) {
  return Array.isArray(field) && field.length >= 1;
}
function isOneHop(r) {
  return r.length === 1;
}
function isTwoHop(r) {
  return r.length === 2;
}
export {
  QueryImpl,
  asQueryImpl,
  newQuery,
  newQueryImpl
};
//# sourceMappingURL=query-impl.js.map
