import { sql } from "kysely";
const VALID_OPS = ["eq", "ne", "gt", "gte", "lt", "lte", "in", "nin", "like", "ilike", "exists"];
const normalizeField = (key) => key.startsWith("cf_") ? `cf:${key.slice(3)}` : key;
const MAX_DNF_DISJUNCTS = 1e3;
class AdvancedFilterComplexityError extends Error {
  constructor(disjunctCount) {
    super(
      `Advanced filter expanded to ${disjunctCount} disjuncts, exceeding the limit of ${MAX_DNF_DISJUNCTS}. Reduce the number of OR branches or simplify nested groups.`
    );
    this.code = "ADVANCED_FILTER_TOO_COMPLEX";
    this.name = "AdvancedFilterComplexityError";
    this.disjunctCount = disjunctCount;
  }
}
function compileToDnf(filter) {
  if (filter === null || filter === void 0) return [[]];
  if (Array.isArray(filter) || typeof filter !== "object") return [[]];
  const obj = filter;
  const partialDnfs = [];
  for (const [rawKey, rawVal] of Object.entries(obj)) {
    if (rawKey === "$and" && Array.isArray(rawVal)) {
      partialDnfs.push(cartesianAnd(rawVal.map(compileToDnf)));
      continue;
    }
    if (rawKey === "$or" && Array.isArray(rawVal)) {
      const expanded = rawVal.flatMap(compileToDnf);
      if (expanded.length > MAX_DNF_DISJUNCTS) {
        throw new AdvancedFilterComplexityError(expanded.length);
      }
      partialDnfs.push(expanded);
      continue;
    }
    const field = normalizeField(rawKey);
    if (rawVal !== null && typeof rawVal === "object" && !Array.isArray(rawVal)) {
      const clauses = [];
      for (const [opKey, opVal] of Object.entries(rawVal)) {
        const op = opKey.startsWith("$") ? opKey.slice(1) : opKey;
        if (VALID_OPS.includes(op)) {
          clauses.push({ field, op, value: opVal });
        }
      }
      if (clauses.length > 0) partialDnfs.push([clauses]);
    } else {
      partialDnfs.push([[{ field, op: "eq", value: rawVal }]]);
    }
  }
  return cartesianAnd(partialDnfs);
}
function cartesianAnd(dnfs) {
  if (dnfs.length === 0) return [[]];
  let result = [[]];
  for (const dnf of dnfs) {
    if (dnf.length === 0) {
      return [];
    }
    const projected = result.length * dnf.length;
    if (projected > MAX_DNF_DISJUNCTS) {
      throw new AdvancedFilterComplexityError(projected);
    }
    const next = [];
    for (const left of result) {
      for (const right of dnf) {
        next.push([...left, ...right]);
      }
    }
    result = next;
  }
  return result;
}
function clauseKey(c) {
  return JSON.stringify([c.field, c.op, c.value]);
}
function liftCommonClauses(dnf) {
  if (dnf.length <= 1) return { common: [], remaining: dnf };
  const counts = /* @__PURE__ */ new Map();
  for (const disjunct of dnf) {
    const seen = /* @__PURE__ */ new Set();
    for (const c of disjunct) {
      const key = clauseKey(c);
      if (seen.has(key)) continue;
      seen.add(key);
      const entry = counts.get(key);
      if (entry) entry.count += 1;
      else counts.set(key, { clause: c, count: 1 });
    }
  }
  const total = dnf.length;
  const commonKeys = /* @__PURE__ */ new Set();
  const common = [];
  for (const [key, { clause, count }] of counts) {
    if (count === total) {
      commonKeys.add(key);
      common.push(clause);
    }
  }
  if (common.length === 0) return { common: [], remaining: dnf };
  const remaining = dnf.map((d) => d.filter((c) => !commonKeys.has(clauseKey(c))));
  return { common, remaining };
}
function normalizeFilters(filters) {
  if (!filters) return [];
  if (Array.isArray(filters)) {
    return filters.map((f) => ({
      ...f,
      field: normalizeField(String(f.field))
    }));
  }
  const dnf = compileToDnf(filters);
  if (dnf.length === 0) return [];
  const nonEmpty = dnf.filter((disjunct) => disjunct.length > 0);
  if (nonEmpty.length === 0) return [];
  if (nonEmpty.length === 1) {
    return nonEmpty[0].map((c) => ({ field: c.field, op: c.op, value: c.value }));
  }
  const { common, remaining } = liftCommonClauses(nonEmpty);
  const out = [];
  for (const c of common) out.push({ field: c.field, op: c.op, value: c.value });
  const remainingNonEmpty = remaining.filter((d) => d.length > 0);
  if (remainingNonEmpty.length === 0) return out;
  if (remainingNonEmpty.length === 1) {
    for (const c of remainingNonEmpty[0]) out.push({ field: c.field, op: c.op, value: c.value });
    return out;
  }
  for (let i = 0; i < remainingNonEmpty.length; i++) {
    for (const c of remainingNonEmpty[i]) {
      out.push({ field: c.field, op: c.op, value: c.value, orGroup: `or_${i}` });
    }
  }
  return out;
}
function resolveJoins(baseTable, joins, resolveTable) {
  if (!joins || joins.length === 0) return [];
  const resolved = [];
  const seen = /* @__PURE__ */ new Set();
  for (const entry of joins) {
    if (!entry || typeof entry !== "object") continue;
    const alias = typeof entry.alias === "string" ? entry.alias.trim() : "";
    if (!alias) continue;
    if (seen.has(alias)) continue;
    const table = entry.table ?? (entry.entityId ? resolveTable(String(entry.entityId)) : null);
    if (!table) continue;
    const fromField = entry.from?.field?.trim();
    const toField = entry.to?.field?.trim();
    if (!fromField || !toField) continue;
    const fromAliasRaw = entry.from?.alias?.trim();
    const fromAlias = fromAliasRaw && fromAliasRaw.length > 0 ? fromAliasRaw : "base";
    const type = entry.type === "inner" ? "inner" : "left";
    resolved.push({
      alias,
      table,
      entityId: entry.entityId ? String(entry.entityId) : null,
      fromAlias,
      fromField,
      toField,
      type
    });
    seen.add(alias);
  }
  return resolved;
}
function buildJoinChain(alias, joinMap, baseTable, visited = /* @__PURE__ */ new Set()) {
  if (visited.has(alias)) {
    throw new Error(`QueryEngine: circular join reference detected for alias ${alias}`);
  }
  const cfg = joinMap.get(alias);
  if (!cfg) return [];
  visited.add(alias);
  if (!cfg.fromAlias || cfg.fromAlias === "base" || cfg.fromAlias === baseTable) {
    return [cfg];
  }
  const parentChain = buildJoinChain(cfg.fromAlias, joinMap, baseTable, visited);
  if (parentChain.length === 0) return [];
  return [...parentChain, cfg];
}
function partitionFilters(baseTable, filters, joinMap) {
  const baseFilters = [];
  const joinFilters = /* @__PURE__ */ new Map();
  for (const filter of filters) {
    const field = String(filter.field);
    if (field.startsWith("cf:")) continue;
    const parts = field.split(".");
    if (parts.length === 2) {
      const [aliasNameRaw, column] = parts;
      const aliasName = aliasNameRaw || "";
      if (joinMap.has(aliasName)) {
        const list = joinFilters.get(aliasName) ?? [];
        list.push({ alias: aliasName, column, op: filter.op, value: filter.value });
        joinFilters.set(aliasName, list);
        continue;
      }
      if (aliasName === baseTable || aliasName === "base") {
        baseFilters.push({
          field: column,
          op: filter.op,
          value: filter.value,
          qualified: `${baseTable}.${column}`
        });
        continue;
      }
    }
    baseFilters.push({ ...filter });
  }
  return { baseFilters, joinFilters };
}
async function applyJoinFilters({
  db,
  baseTable,
  builder,
  joinMap,
  joinFilters,
  aliasTables,
  qualifyBase,
  applyAliasScope,
  applyFilterOp,
  applyJoinFilterOp,
  columnExists
}) {
  const resolveAliasName = (aliasName) => {
    if (!aliasName || aliasName === "base") return baseTable;
    return aliasName;
  };
  let nextBuilder = builder;
  for (const [alias, filtersForAlias] of joinFilters.entries()) {
    const chain = buildJoinChain(alias, joinMap, baseTable);
    if (!chain.length) continue;
    const first = chain[0];
    let sub = db.selectFrom(`${first.table} as ${first.alias}`).select(sql`1`.as("one"));
    sub = await applyAliasScope(sub, first.alias, first.table);
    const parentAlias = resolveAliasName(first.fromAlias);
    const parentRef = parentAlias === baseTable ? qualifyBase(first.fromField) : `${parentAlias}.${first.fromField}`;
    sub = sub.whereRef(`${first.alias}.${first.toField}`, "=", parentRef);
    for (const cfg of chain.slice(1)) {
      const parent = resolveAliasName(cfg.fromAlias);
      const rightRef = parent === baseTable ? qualifyBase(cfg.fromField) : `${parent}.${cfg.fromField}`;
      const joinFn = cfg.type === "inner" ? "innerJoin" : "leftJoin";
      sub = sub[joinFn](`${cfg.table} as ${cfg.alias}`, (jb) => jb.onRef(`${cfg.alias}.${cfg.toField}`, "=", rightRef));
      sub = await applyAliasScope(sub, cfg.alias, cfg.table);
    }
    let existsDirective = null;
    for (const filter of filtersForAlias) {
      if (filter.op === "exists") {
        if (filter.value === false) existsDirective = false;
        else if (existsDirective === null) existsDirective = true;
        continue;
      }
      const join = joinMap.get(filter.alias);
      if (!join) continue;
      const targetTable = aliasTables.get(filter.alias);
      if (!targetTable) continue;
      if (columnExists) {
        const exists = await columnExists(targetTable, filter.column);
        if (!exists) continue;
      }
      const qualified = `${filter.alias}.${filter.column}`;
      if (applyJoinFilterOp) {
        const result = await applyJoinFilterOp(sub, filter, qualified, join, targetTable);
        if (result && result.applied) {
          sub = result.builder;
          continue;
        }
        if (result && result.builder) {
          sub = result.builder;
        }
      }
      sub = applyFilterOp(sub, qualified, filter.op, filter.value);
    }
    if (existsDirective === false) {
      const capturedSub = sub;
      nextBuilder = nextBuilder.where((eb) => eb.not(eb.exists(capturedSub)));
    } else {
      const capturedSub = sub;
      nextBuilder = nextBuilder.where((eb) => eb.exists(capturedSub));
    }
  }
  return nextBuilder;
}
export {
  AdvancedFilterComplexityError,
  MAX_DNF_DISJUNCTS,
  applyJoinFilters,
  buildJoinChain,
  normalizeFilters,
  partitionFilters,
  resolveJoins
};
//# sourceMappingURL=join-utils.js.map
