import { unreachable, assert } from "../../../shared/src/asserts.js";
import { must } from "../../../shared/src/must.js";
import { Exists } from "../ivm/exists.js";
import { FanIn } from "../ivm/fan-in.js";
import { FanOut } from "../ivm/fan-out.js";
import { buildFilterPipeline } from "../ivm/filter-operators.js";
import { Filter } from "../ivm/filter.js";
import { FlippedJoin } from "../ivm/flipped-join.js";
import { Join } from "../ivm/join.js";
import { Skip } from "../ivm/skip.js";
import { Take } from "../ivm/take.js";
import { UnionFanIn } from "../ivm/union-fan-in.js";
import { UnionFanOut } from "../ivm/union-fan-out.js";
import { planQuery } from "../planner/planner-builder.js";
import { completeOrdering } from "../query/complete-ordering.js";
import { createPredicate } from "./filter.js";
function buildPipeline(ast, delegate, queryID, costModel, lc, planDebugger) {
  ast = delegate.mapAst ? delegate.mapAst(ast) : ast;
  ast = completeOrdering(
    ast,
    (tableName) => must(delegate.getSource(tableName)).tableSchema.primaryKey
  );
  if (costModel) {
    ast = planQuery(ast, costModel, planDebugger, lc);
  }
  return buildPipelineInternal(ast, delegate, queryID, "");
}
function bindStaticParameters(ast, staticQueryParameters) {
  const visit = (node) => ({
    ...node,
    where: node.where ? bindCondition(node.where) : void 0,
    related: node.related?.map((sq) => ({
      ...sq,
      subquery: visit(sq.subquery)
    }))
  });
  function bindCondition(condition) {
    if (condition.type === "simple") {
      return {
        ...condition,
        left: bindValue(condition.left),
        right: bindValue(condition.right)
      };
    }
    if (condition.type === "correlatedSubquery") {
      return {
        ...condition,
        related: {
          ...condition.related,
          subquery: visit(condition.related.subquery)
        }
      };
    }
    return {
      ...condition,
      conditions: condition.conditions.map(bindCondition)
    };
  }
  const bindValue = (value) => {
    if (isParameter(value)) {
      const anchor = must(
        staticQueryParameters,
        "Static query params do not exist"
      )[value.anchor];
      const resolvedValue = resolveField(anchor, value.field);
      return {
        type: "literal",
        value: resolvedValue
      };
    }
    return value;
  };
  return visit(ast);
}
function resolveField(anchor, field) {
  if (anchor === void 0) {
    return null;
  }
  if (Array.isArray(field)) {
    return field.reduce((acc, f) => acc?.[f], anchor) ?? null;
  }
  return anchor[field] ?? null;
}
function isParameter(value) {
  return value.type === "static";
}
const EXISTS_LIMIT = 3;
const PERMISSIONS_EXISTS_LIMIT = 1;
function assertNoNotExists(condition) {
  switch (condition.type) {
    case "simple":
      return;
    case "correlatedSubquery":
      if (condition.op === "NOT EXISTS") {
        throw new Error(
          "not(exists()) is not supported on the client - see https://bugs.rocicorp.dev/issue/3438"
        );
      }
      return;
    case "and":
    case "or":
      for (const c of condition.conditions) {
        assertNoNotExists(c);
      }
      return;
    default:
      unreachable();
  }
}
function buildPipelineInternal(ast, delegate, queryID, name, partitionKey) {
  const source = delegate.getSource(ast.table);
  if (!source) {
    throw new Error(`Source not found: ${ast.table}`);
  }
  ast = uniquifyCorrelatedSubqueryConditionAliases(ast);
  if (!delegate.enableNotExists && ast.where) {
    assertNoNotExists(ast.where);
  }
  const csqConditions = gatherCorrelatedSubqueryQueryConditions(ast.where);
  const splitEditKeys = partitionKey ? new Set(partitionKey) : /* @__PURE__ */ new Set();
  const aliases = /* @__PURE__ */ new Set();
  for (const csq of csqConditions) {
    aliases.add(csq.related.subquery.alias || "");
    for (const key of csq.related.correlation.parentField) {
      splitEditKeys.add(key);
    }
  }
  if (ast.related) {
    for (const csq of ast.related) {
      for (const key of csq.correlation.parentField) {
        splitEditKeys.add(key);
      }
    }
  }
  const conn = source.connect(
    must(ast.orderBy),
    ast.where,
    splitEditKeys,
    delegate.debug
  );
  let end = delegate.decorateSourceInput(conn, queryID);
  end = delegate.decorateInput(end, `${name}:source(${ast.table})`);
  const { fullyAppliedFilters } = conn;
  if (ast.start) {
    const skip = new Skip(end, ast.start);
    delegate.addEdge(end, skip);
    end = delegate.decorateInput(skip, `${name}:skip)`);
  }
  for (const csqCondition of csqConditions) {
    if (!csqCondition.flip) {
      end = applyCorrelatedSubQuery(
        {
          ...csqCondition.related,
          subquery: {
            ...csqCondition.related.subquery,
            limit: csqCondition.related.system === "permissions" ? PERMISSIONS_EXISTS_LIMIT : EXISTS_LIMIT
          }
        },
        delegate,
        queryID,
        end,
        name,
        true
      );
    }
  }
  if (ast.where && (!fullyAppliedFilters || delegate.applyFiltersAnyway)) {
    end = applyWhere(end, ast.where, delegate, name);
  }
  if (ast.limit !== void 0) {
    const takeName = `${name}:take`;
    const take = new Take(
      end,
      delegate.createStorage(takeName),
      ast.limit,
      partitionKey
    );
    delegate.addEdge(end, take);
    end = delegate.decorateInput(take, takeName);
  }
  if (ast.related) {
    for (const csq of ast.related) {
      end = applyCorrelatedSubQuery(csq, delegate, queryID, end, name, false);
    }
  }
  return end;
}
function applyWhere(input, condition, delegate, name) {
  if (!conditionIncludesFlippedSubqueryAtAnyLevel(condition)) {
    return buildFilterPipeline(
      input,
      delegate,
      (filterInput) => applyFilter(filterInput, condition, delegate, name)
    );
  }
  return applyFilterWithFlips(input, condition, delegate, name);
}
function applyFilterWithFlips(input, condition, delegate, name) {
  let end = input;
  assert(condition.type !== "simple", "Simple conditions cannot have flips");
  switch (condition.type) {
    case "and": {
      const [withFlipped, withoutFlipped] = partitionBranches(
        condition.conditions,
        conditionIncludesFlippedSubqueryAtAnyLevel
      );
      if (withoutFlipped.length > 0) {
        end = buildFilterPipeline(
          input,
          delegate,
          (filterInput) => applyAnd(
            filterInput,
            {
              conditions: withoutFlipped
            },
            delegate,
            name
          )
        );
      }
      assert(withFlipped.length > 0, "Impossible to have no flips here");
      for (const cond of withFlipped) {
        end = applyFilterWithFlips(end, cond, delegate, name);
      }
      break;
    }
    case "or": {
      const [withFlipped, withoutFlipped] = partitionBranches(
        condition.conditions,
        conditionIncludesFlippedSubqueryAtAnyLevel
      );
      assert(withFlipped.length > 0, "Impossible to have no flips here");
      const ufo = new UnionFanOut(end);
      delegate.addEdge(end, ufo);
      end = delegate.decorateInput(ufo, `${name}:ufo`);
      const branches = [];
      if (withoutFlipped.length > 0) {
        branches.push(
          buildFilterPipeline(
            end,
            delegate,
            (filterInput) => applyOr(
              filterInput,
              {
                conditions: withoutFlipped
              },
              delegate,
              name
            )
          )
        );
      }
      for (const cond of withFlipped) {
        branches.push(applyFilterWithFlips(end, cond, delegate, name));
      }
      const ufi = new UnionFanIn(ufo, branches);
      for (const branch of branches) {
        delegate.addEdge(branch, ufi);
      }
      end = delegate.decorateInput(ufi, `${name}:ufi`);
      break;
    }
    case "correlatedSubquery": {
      const sq = condition.related;
      const child = buildPipelineInternal(
        sq.subquery,
        delegate,
        "",
        `${name}.${sq.subquery.alias}`,
        sq.correlation.childField
      );
      const flippedJoin = new FlippedJoin({
        parent: end,
        child,
        parentKey: sq.correlation.parentField,
        childKey: sq.correlation.childField,
        relationshipName: must(
          sq.subquery.alias,
          "Subquery must have an alias"
        ),
        hidden: sq.hidden ?? false,
        system: sq.system ?? "client"
      });
      delegate.addEdge(end, flippedJoin);
      delegate.addEdge(child, flippedJoin);
      end = delegate.decorateInput(
        flippedJoin,
        `${name}:flipped-join(${sq.subquery.alias})`
      );
      break;
    }
  }
  return end;
}
function applyFilter(input, condition, delegate, name) {
  switch (condition.type) {
    case "and":
      return applyAnd(input, condition, delegate, name);
    case "or":
      return applyOr(input, condition, delegate, name);
    case "correlatedSubquery":
      return applyCorrelatedSubqueryCondition(input, condition, delegate, name);
    case "simple":
      return applySimpleCondition(input, delegate, condition);
  }
}
function applyAnd(input, condition, delegate, name) {
  for (const subCondition of condition.conditions) {
    input = applyFilter(input, subCondition, delegate, name);
  }
  return input;
}
function applyOr(input, condition, delegate, name) {
  const [subqueryConditions, otherConditions] = groupSubqueryConditions(condition);
  if (subqueryConditions.length === 0) {
    const filter = new Filter(
      input,
      createPredicate({
        type: "or",
        conditions: otherConditions
      })
    );
    delegate.addEdge(input, filter);
    return filter;
  }
  const fanOut = new FanOut(input);
  delegate.addEdge(input, fanOut);
  const branches = subqueryConditions.map(
    (subCondition) => applyFilter(fanOut, subCondition, delegate, name)
  );
  if (otherConditions.length > 0) {
    const filter = new Filter(
      fanOut,
      createPredicate({
        type: "or",
        conditions: otherConditions
      })
    );
    delegate.addEdge(fanOut, filter);
    branches.push(filter);
  }
  const ret = new FanIn(fanOut, branches);
  for (const branch of branches) {
    delegate.addEdge(branch, ret);
  }
  fanOut.setFanIn(ret);
  return ret;
}
function groupSubqueryConditions(condition) {
  const partitioned = [[], []];
  for (const subCondition of condition.conditions) {
    if (isNotAndDoesNotContainSubquery(subCondition)) {
      partitioned[1].push(subCondition);
    } else {
      partitioned[0].push(subCondition);
    }
  }
  return partitioned;
}
function isNotAndDoesNotContainSubquery(condition) {
  if (condition.type === "correlatedSubquery") {
    return false;
  }
  if (condition.type === "simple") {
    return true;
  }
  return condition.conditions.every(isNotAndDoesNotContainSubquery);
}
function applySimpleCondition(input, delegate, condition) {
  const filter = new Filter(input, createPredicate(condition));
  delegate.decorateFilterInput(
    filter,
    `${valuePosName(condition.left)}:${condition.op}:${valuePosName(condition.right)}`
  );
  delegate.addEdge(input, filter);
  return filter;
}
function valuePosName(left) {
  switch (left.type) {
    case "static":
      return left.field;
    case "literal":
      return left.value;
    case "column":
      return left.name;
  }
}
function applyCorrelatedSubQuery(sq, delegate, queryID, end, name, fromCondition) {
  if (sq.subquery.limit === 0 && fromCondition) {
    return end;
  }
  assert(sq.subquery.alias, "Subquery must have an alias");
  const child = buildPipelineInternal(
    sq.subquery,
    delegate,
    queryID,
    `${name}.${sq.subquery.alias}`,
    sq.correlation.childField
  );
  const joinName = `${name}:join(${sq.subquery.alias})`;
  const join = new Join({
    parent: end,
    child,
    parentKey: sq.correlation.parentField,
    childKey: sq.correlation.childField,
    relationshipName: sq.subquery.alias,
    hidden: sq.hidden ?? false,
    system: sq.system ?? "client"
  });
  delegate.addEdge(end, join);
  delegate.addEdge(child, join);
  return delegate.decorateInput(join, joinName);
}
function applyCorrelatedSubqueryCondition(input, condition, delegate, name) {
  assert(
    condition.op === "EXISTS" || condition.op === "NOT EXISTS",
    "Expected EXISTS or NOT EXISTS operator"
  );
  if (condition.related.subquery.limit === 0) {
    if (condition.op === "EXISTS") {
      const filter2 = new Filter(input, () => false);
      delegate.addEdge(input, filter2);
      return filter2;
    }
    const filter = new Filter(input, () => true);
    delegate.addEdge(input, filter);
    return filter;
  }
  const existsName = `${name}:exists(${condition.related.subquery.alias})`;
  const exists = new Exists(
    input,
    must(condition.related.subquery.alias),
    condition.related.correlation.parentField,
    condition.op
  );
  delegate.addEdge(input, exists);
  return delegate.decorateFilterInput(exists, existsName);
}
function gatherCorrelatedSubqueryQueryConditions(condition) {
  const csqs = [];
  const gather = (condition2) => {
    if (condition2.type === "correlatedSubquery") {
      csqs.push(condition2);
      return;
    }
    if (condition2.type === "and" || condition2.type === "or") {
      for (const c of condition2.conditions) {
        gather(c);
      }
      return;
    }
  };
  if (condition) {
    gather(condition);
  }
  return csqs;
}
function uniquifyCorrelatedSubqueryConditionAliases(ast) {
  if (!ast.where) {
    return ast;
  }
  const { where } = ast;
  if (where.type !== "and" && where.type !== "or") {
    return ast;
  }
  let count = 0;
  const uniquifyCorrelatedSubquery = (csqc) => ({
    ...csqc,
    related: {
      ...csqc.related,
      subquery: {
        ...csqc.related.subquery,
        alias: (csqc.related.subquery.alias ?? "") + "_" + count++
      }
    }
  });
  const uniquify = (cond) => {
    if (cond.type === "simple") {
      return cond;
    } else if (cond.type === "correlatedSubquery") {
      return uniquifyCorrelatedSubquery(cond);
    }
    const conditions = [];
    for (const c of cond.conditions) {
      conditions.push(uniquify(c));
    }
    return {
      type: cond.type,
      conditions
    };
  };
  const result = {
    ...ast,
    where: uniquify(where)
  };
  return result;
}
function conditionIncludesFlippedSubqueryAtAnyLevel(cond) {
  if (cond.type === "correlatedSubquery") {
    return !!cond.flip;
  }
  if (cond.type === "and" || cond.type === "or") {
    return cond.conditions.some(
      (c) => conditionIncludesFlippedSubqueryAtAnyLevel(c)
    );
  }
  return false;
}
function partitionBranches(conditions, predicate) {
  const matched = [];
  const notMatched = [];
  for (const c of conditions) {
    if (predicate(c)) {
      matched.push(c);
    } else {
      notMatched.push(c);
    }
  }
  return [matched, notMatched];
}
export {
  applyOr,
  assertNoNotExists,
  bindStaticParameters,
  buildPipeline,
  conditionIncludesFlippedSubqueryAtAnyLevel,
  groupSubqueryConditions,
  isNotAndDoesNotContainSubquery,
  partitionBranches
};
//# sourceMappingURL=builder.js.map
