import { compareUTF8 } from "compare-utf8";
import { defined } from "../../shared/src/arrays.js";
import { assert } from "../../shared/src/asserts.js";
import { must } from "../../shared/src/must.js";
import { readonly, literalUnion, readonlyArray, readonlyObject } from "../../shared/src/valita.js";
import { rowSchema } from "./data.js";
import { string, tuple, union, number, boolean, null as _null, literal, array, lazy, object } from "@badrap/valita";
const SUBQ_PREFIX = "zsubq_";
const selectorSchema = string();
const toStaticParam = Symbol();
const planIdSymbol = Symbol("planId");
const orderingElementSchema = readonly(
  tuple([selectorSchema, literalUnion("asc", "desc")])
);
const orderingSchema = readonlyArray(orderingElementSchema);
union(
  string(),
  number(),
  boolean(),
  _null()
);
const equalityOpsSchema = literalUnion("=", "!=", "IS", "IS NOT");
const orderOpsSchema = literalUnion("<", ">", "<=", ">=");
const likeOpsSchema = literalUnion(
  "LIKE",
  "NOT LIKE",
  "ILIKE",
  "NOT ILIKE"
);
const inOpsSchema = literalUnion("IN", "NOT IN");
const simpleOperatorSchema = union(
  equalityOpsSchema,
  orderOpsSchema,
  likeOpsSchema,
  inOpsSchema
);
const literalReferenceSchema = readonlyObject({
  type: literal("literal"),
  value: union(
    string(),
    number(),
    boolean(),
    _null(),
    readonlyArray(union(string(), number(), boolean()))
  )
});
const columnReferenceSchema = readonlyObject({
  type: literal("column"),
  name: string()
});
const parameterReferenceSchema = readonlyObject({
  type: literal("static"),
  // The "namespace" of the injected parameter.
  // Write authorization will send the value of a row
  // prior to the mutation being run (preMutationRow).
  // Read and write authorization will both send the
  // current authentication data (authData).
  anchor: literalUnion("authData", "preMutationRow"),
  field: union(string(), array(string()))
});
const conditionValueSchema = union(
  literalReferenceSchema,
  columnReferenceSchema,
  parameterReferenceSchema
);
const simpleConditionSchema = readonlyObject({
  type: literal("simple"),
  op: simpleOperatorSchema,
  left: conditionValueSchema,
  right: union(parameterReferenceSchema, literalReferenceSchema)
});
const correlatedSubqueryConditionOperatorSchema = literalUnion("EXISTS", "NOT EXISTS");
const correlatedSubqueryConditionSchema = readonlyObject({
  type: literal("correlatedSubquery"),
  related: lazy(() => correlatedSubquerySchema),
  op: correlatedSubqueryConditionOperatorSchema,
  flip: boolean().optional(),
  scalar: boolean().optional()
});
const conditionSchema = union(
  simpleConditionSchema,
  lazy(() => conjunctionSchema),
  lazy(() => disjunctionSchema),
  correlatedSubqueryConditionSchema
);
const conjunctionSchema = readonlyObject({
  type: literal("and"),
  conditions: readonlyArray(conditionSchema)
});
const disjunctionSchema = readonlyObject({
  type: literal("or"),
  conditions: readonlyArray(conditionSchema)
});
function mustCompoundKey(field) {
  assert(
    Array.isArray(field) && field.length >= 1,
    "Expected non-empty array for compound key"
  );
  return field;
}
const compoundKeySchema = readonly(
  tuple([string()]).concat(array(string()))
);
const correlationSchema = readonlyObject({
  parentField: compoundKeySchema,
  childField: compoundKeySchema
});
const correlatedSubquerySchemaOmitSubquery = readonlyObject({
  correlation: correlationSchema,
  hidden: boolean().optional(),
  system: literalUnion("permissions", "client", "test").optional()
});
const correlatedSubquerySchema = correlatedSubquerySchemaOmitSubquery.extend({
  subquery: lazy(() => astSchema)
});
const astSchema = readonlyObject({
  schema: string().optional(),
  table: string(),
  alias: string().optional(),
  where: conditionSchema.optional(),
  related: readonlyArray(correlatedSubquerySchema).optional(),
  limit: number().optional(),
  orderBy: orderingSchema.optional(),
  start: object({
    row: rowSchema,
    exclusive: boolean()
  }).optional()
});
function transformAST(ast, transform) {
  const { tableName, columnName } = transform;
  const colName = (c) => columnName(ast.table, c);
  const key = (table, k) => {
    const serverKey = k.map((col) => columnName(table, col));
    return mustCompoundKey(serverKey);
  };
  const where = ast.where ? transform.where(ast.where) : void 0;
  const transformed = {
    schema: ast.schema,
    table: tableName(ast.table),
    alias: ast.alias,
    where: where ? transformWhere(where, ast.table, transform) : void 0,
    related: ast.related ? transform.related(
      ast.related.map(
        (r) => ({
          correlation: {
            parentField: key(ast.table, r.correlation.parentField),
            childField: key(r.subquery.table, r.correlation.childField)
          },
          hidden: r.hidden,
          subquery: transformAST(r.subquery, transform),
          system: r.system
        })
      )
    ) : void 0,
    start: ast.start ? {
      ...ast.start,
      row: Object.fromEntries(
        Object.entries(ast.start.row).map(([col, val]) => [
          colName(col),
          val
        ])
      )
    } : void 0,
    limit: ast.limit,
    orderBy: ast.orderBy?.map(([col, dir]) => [colName(col), dir])
  };
  return transformed;
}
function transformWhere(where, table, transform) {
  const { columnName } = transform;
  const condValue = (c) => c.type !== "column" ? c : { ...c, name: columnName(table, c.name) };
  const key = (table2, k) => {
    const serverKey = k.map((col) => columnName(table2, col));
    return mustCompoundKey(serverKey);
  };
  if (where.type === "simple") {
    return { ...where, left: condValue(where.left) };
  } else if (where.type === "correlatedSubquery") {
    const { correlation, subquery } = where.related;
    return {
      ...where,
      related: {
        ...where.related,
        correlation: {
          parentField: key(table, correlation.parentField),
          childField: key(subquery.table, correlation.childField)
        },
        subquery: transformAST(subquery, transform)
      }
    };
  }
  return {
    type: where.type,
    conditions: transform.conditions(
      where.conditions.map((c) => transformWhere(c, table, transform))
    )
  };
}
const normalizeCache = /* @__PURE__ */ new WeakMap();
const NORMALIZE_TRANSFORM = {
  tableName: (t) => t,
  columnName: (_, c) => c,
  related: sortedRelated,
  where: flattened,
  conditions: (c) => c.sort(cmpCondition)
};
function normalizeAST(ast) {
  let normalized = normalizeCache.get(ast);
  if (!normalized) {
    normalized = transformAST(ast, NORMALIZE_TRANSFORM);
    normalizeCache.set(ast, normalized);
  }
  return normalized;
}
function mapAST(ast, mapper) {
  return transformAST(ast, {
    tableName: (table) => mapper.tableName(table),
    columnName: (table, col) => mapper.columnName(table, col),
    related: (r) => r,
    where: (w) => w,
    conditions: (c) => c
  });
}
function mapCondition(cond, table, mapper) {
  return transformWhere(cond, table, {
    tableName: (table2) => mapper.tableName(table2),
    columnName: (table2, col) => mapper.columnName(table2, col),
    related: (r) => r,
    where: (w) => w,
    conditions: (c) => c
  });
}
function sortedRelated(related) {
  return related.sort(cmpRelated);
}
function cmpCondition(a, b) {
  if (a.type === "simple") {
    if (b.type !== "simple") {
      return -1;
    }
    return compareValuePosition(a.left, b.left) || compareUTF8MaybeNull(a.op, b.op) || compareValuePosition(a.right, b.right);
  }
  if (b.type === "simple") {
    return 1;
  }
  if (a.type === "correlatedSubquery") {
    if (b.type !== "correlatedSubquery") {
      return -1;
    }
    return cmpRelated(a.related, b.related) || compareUTF8MaybeNull(a.op, b.op) || cmpOptionalBool(a.flip, b.flip) || cmpOptionalBool(a.scalar, b.scalar);
  }
  if (b.type === "correlatedSubquery") {
    return -1;
  }
  const val = compareUTF8MaybeNull(a.type, b.type);
  if (val !== 0) {
    return val;
  }
  for (let l = 0, r = 0; l < a.conditions.length && r < b.conditions.length; l++, r++) {
    const val2 = cmpCondition(a.conditions[l], b.conditions[r]);
    if (val2 !== 0) {
      return val2;
    }
  }
  return a.conditions.length - b.conditions.length;
}
function compareValuePosition(a, b) {
  if (a.type !== b.type) {
    return compareUTF8(a.type, b.type);
  }
  switch (a.type) {
    case "literal":
      assert(b.type === "literal", "Expected literal type for comparison");
      return compareUTF8(String(a.value), String(b.value));
    case "column":
      assert(b.type === "column", "Expected column type for comparison");
      return compareUTF8(a.name, b.name);
    case "static":
      throw new Error(
        "Static parameters should be resolved before normalization"
      );
  }
}
function cmpRelated(a, b) {
  return compareUTF8(must(a.subquery.alias), must(b.subquery.alias));
}
function flattened(cond) {
  if (cond.type === "simple" || cond.type === "correlatedSubquery") {
    return cond;
  }
  const conditions = defined(
    cond.conditions.flatMap(
      (c) => c.type === cond.type ? c.conditions.map((c2) => flattened(c2)) : flattened(c)
    )
  );
  switch (conditions.length) {
    case 0:
      return void 0;
    case 1:
      return conditions[0];
    default:
      return {
        type: cond.type,
        conditions
      };
  }
}
function compareUTF8MaybeNull(a, b) {
  if (a !== null && b !== null) {
    return compareUTF8(a, b);
  }
  if (b !== null) {
    return -1;
  }
  if (a !== null) {
    return 1;
  }
  return 0;
}
function cmpOptionalBool(a, b) {
  const toNum = (v2) => v2 === void 0 ? 0 : v2 ? 2 : 1;
  return toNum(a) - toNum(b);
}
export {
  SUBQ_PREFIX,
  astSchema,
  compoundKeySchema,
  conditionSchema,
  correlatedSubqueryConditionOperatorSchema,
  correlatedSubqueryConditionSchema,
  correlatedSubquerySchema,
  correlatedSubquerySchemaOmitSubquery,
  equalityOpsSchema,
  inOpsSchema,
  likeOpsSchema,
  mapAST,
  mapCondition,
  normalizeAST,
  orderOpsSchema,
  orderingSchema,
  planIdSymbol,
  selectorSchema,
  simpleConditionSchema,
  simpleOperatorSchema,
  toStaticParam
};
//# sourceMappingURL=ast.js.map
