/**
 * QueryBuilder — fluent query accumulator for a query read model.
 *
 * Pure accumulator: builder methods accumulate query state into a
 * QueryPlan, then run() delegates execution to QueryRunner.
 */
import QueryError from "../../errors/QueryError.js";
import ImmutableBytes from "../snapshot/ImmutableBytes.js";
import BoundedSupportRule from "./BoundedSupportRule.js";
import SupportFragmentPlan from "./SupportFragmentPlan.js";
import QueryPlan from "./QueryPlan.js";
import QueryRunner from "./QueryRunner.js";
const DEFAULT_PATTERN = '*';
const NUMERIC_AGGREGATE_KEYS = ['sum', 'avg', 'min', 'max'];
function assertMatchPattern(pattern) {
    const isString = typeof pattern === 'string';
    const isStringArray = Array.isArray(pattern) && pattern.every((p) => typeof p === 'string');
    if (!isString && !isStringArray) {
        throw new QueryError('match() expects a string pattern or array of string patterns', {
            code: 'E_QUERY_MATCH_TYPE',
            context: { receivedType: typeof pattern },
        });
    }
}
function createPredicate(fn) {
    if (isQueryWhereObject(fn)) {
        return objectToPredicate(fn);
    }
    if (typeof fn === 'function') {
        return fn;
    }
    throw new QueryError('where() expects a predicate function or object', {
        code: 'E_QUERY_WHERE_TYPE',
        context: { receivedType: typeof fn },
    });
}
function isQueryWhereObject(value) {
    return value !== null &&
        typeof value === 'object' &&
        !Array.isArray(value) &&
        !(value instanceof ImmutableBytes);
}
function assertAggregateSpec(spec) {
    if (spec === null ||
        typeof spec !== 'object' ||
        Array.isArray(spec) ||
        spec instanceof ImmutableBytes) {
        throw new QueryError('aggregate() expects an object', {
            code: 'E_QUERY_AGGREGATE_TYPE',
            context: { receivedType: typeof spec },
        });
    }
}
function isPrimitive(value) {
    return value === null ||
        typeof value === 'string' ||
        typeof value === 'number' ||
        typeof value === 'boolean';
}
function objectToPredicate(obj) {
    const entries = Object.entries(obj);
    for (const [key, value] of entries) {
        if (!isPrimitive(value)) {
            throw new QueryError('where() object shorthand only accepts primitive property values', {
                code: 'E_QUERY_WHERE_VALUE_TYPE',
                context: { key, receivedType: typeof value },
            });
        }
    }
    return ({ props }) => {
        for (const [key, value] of entries) {
            if (!(key in props) || props[key] !== value) {
                return false;
            }
        }
        return true;
    };
}
function assertLabel(label) {
    if (label === undefined) {
        return;
    }
    if (typeof label !== 'string') {
        throw new QueryError('label must be a string', {
            code: 'E_QUERY_LABEL_TYPE',
            context: { receivedType: typeof label },
        });
    }
}
function normalizeDepth(depth) {
    if (depth === undefined) {
        return [1, 1];
    }
    if (typeof depth === 'number') {
        if (!Number.isInteger(depth) || depth < 0) {
            throw new QueryError('depth must be a non-negative integer', {
                code: 'E_QUERY_DEPTH_TYPE',
                context: { value: depth },
            });
        }
        return [depth, depth];
    }
    if (Array.isArray(depth) && depth.length === 2) {
        const [min, max] = depth;
        if (!Number.isInteger(min) || !Number.isInteger(max) || min < 0 || max < 0) {
            throw new QueryError('depth values must be non-negative integers', {
                code: 'E_QUERY_DEPTH_TYPE',
                context: { value: depth },
            });
        }
        if (min > max) {
            throw new QueryError('depth min must be <= max', {
                code: 'E_QUERY_DEPTH_RANGE',
                context: { min, max },
            });
        }
        return [min, max];
    }
    throw new QueryError('depth must be a number or [min, max] array', {
        code: 'E_QUERY_DEPTH_TYPE',
        context: { receivedType: typeof depth, value: depth },
    });
}
/**
 * Fluent query builder for materialized WARP state.
 *
 * Accumulates query state via chaining methods, then delegates
 * execution to QueryRunner via run().
 */
export default class QueryBuilder {
    _provider;
    _pattern;
    _operations;
    _select;
    _aggregate;
    constructor(provider) {
        this._provider = provider;
        this._pattern = null;
        this._operations = [];
        this._select = null;
        this._aggregate = null;
    }
    toPlan() {
        return new QueryPlan({
            pattern: this._pattern ?? DEFAULT_PATTERN,
            operations: this._operations,
            select: this._select,
            aggregate: this._aggregate,
        });
    }
    supportRule() {
        return BoundedSupportRule.fromQueryPlan(this.toPlan());
    }
    supportFragmentPlan() {
        return SupportFragmentPlan.fromSupportRule(this.supportRule());
    }
    match(pattern) {
        assertMatchPattern(pattern);
        this._pattern = pattern;
        return this;
    }
    where(fn) {
        const predicate = createPredicate(fn);
        this._operations.push({ type: 'where', fn: predicate });
        return this;
    }
    outgoing(label, options) {
        if (this._aggregate) {
            throw new QueryError('outgoing() cannot be called after aggregate()', {
                code: 'E_QUERY_AGGREGATE_TERMINAL',
            });
        }
        assertLabel(label);
        const depth = normalizeDepth(options?.depth);
        this._operations.push({ type: 'outgoing', ...(label !== undefined ? { label } : {}), depth });
        return this;
    }
    incoming(label, options) {
        if (this._aggregate) {
            throw new QueryError('incoming() cannot be called after aggregate()', {
                code: 'E_QUERY_AGGREGATE_TERMINAL',
            });
        }
        assertLabel(label);
        const depth = normalizeDepth(options?.depth);
        this._operations.push({ type: 'incoming', ...(label !== undefined ? { label } : {}), depth });
        return this;
    }
    select(fields) {
        if (this._aggregate) {
            throw new QueryError('select() cannot be called after aggregate()', {
                code: 'E_QUERY_AGGREGATE_TERMINAL',
            });
        }
        if (fields === undefined) {
            this._select = null;
            return this;
        }
        if (!Array.isArray(fields)) {
            throw new QueryError('select() expects an array of fields', {
                code: 'E_QUERY_SELECT_TYPE',
                context: { receivedType: typeof fields },
            });
        }
        this._select = fields;
        return this;
    }
    aggregate(spec) {
        assertAggregateSpec(spec);
        for (const key of NUMERIC_AGGREGATE_KEYS) {
            const value = spec[key];
            if (value !== undefined && typeof value !== 'string') {
                throw new QueryError(`aggregate() expects ${key} to be a string path`, {
                    code: 'E_QUERY_AGGREGATE_TYPE',
                    context: { key, receivedType: typeof value },
                });
            }
        }
        if (spec.count !== undefined && typeof spec.count !== 'boolean') {
            throw new QueryError('aggregate() expects count to be boolean', {
                code: 'E_QUERY_AGGREGATE_TYPE',
                context: { key: 'count', receivedType: typeof spec.count },
            });
        }
        this._aggregate = spec;
        return this;
    }
    async run() {
        const runner = new QueryRunner(this._provider);
        return await runner.run(this.toPlan());
    }
}
