import { deepClone, isDefined, isRecord } from '@takeshape/util';
import set from 'lodash/set.js';
import uniq from 'lodash/uniq.js';
import minimatch from 'minimatch';
import { parseTemplateShape } from "../refs.js";
import { dereferenceSchema, getArgsReferenceWithPath, getRefOrItemsRef, parseReturnShape, refItemToString, splitAtRef } from "./refs.js";
import { isAllOfSchema, isObjectSchema, isOneOfSchema, isRefSchema } from "./type-utils.js";
import { visitSchemaProperties } from "./visitor.js";
const layerDefaults = {
    namespace: '',
    visibility: []
};
function filterMap(prefix, fields, filters) {
    const result = {};
    for (const [name, value] of Object.entries(fields)) {
        for (const filter of filters) {
            if (minimatch(`${prefix}.${name}`, filter)) {
                result[name] = value;
            }
        }
    }
    return result;
}
function filterLayer(layerSchema, filters) {
    return {
        ...layerSchema,
        queries: filterMap('queries', layerSchema.queries ?? {}, filters),
        mutations: filterMap('mutations', layerSchema.mutations ?? {}, filters),
        shapes: filterMap('shapes', layerSchema.shapes ?? {}, filters),
        workflows: filterMap('workflows', layerSchema.workflows ?? {}, filters)
    };
}
// TODO use non-versioned util eventually
export function isObjectSchemaV4_0_0(maybeObjectSchema) {
    return maybeObjectSchema.type === 'object' && isRecord(maybeObjectSchema.properties);
}
function mergeShape(a, b) {
    if (b.override === 'extend') {
        if (isObjectSchemaV4_0_0(a.schema) && isObjectSchemaV4_0_0(b.schema)) {
            const combinedRequired = uniq((a.schema.required ?? []).concat(b.schema.required ?? []));
            const combinedSchema = {
                type: 'object',
                properties: {
                    ...a.schema.properties,
                    ...b.schema.properties
                }
            };
            if (combinedRequired.length) {
                combinedSchema.required = combinedRequired;
            }
            return { ...a, ...b, schema: combinedSchema };
        }
    }
    return b;
}
function assignShapes(flattenedShapes, layerShapes) {
    for (const [shapeName, shape] of Object.entries(layerShapes)) {
        flattenedShapes[shapeName] = flattenedShapes[shapeName] ? mergeShape(flattenedShapes[shapeName], shape) : shape;
    }
}
function addRef(refs, ref, predicate) {
    if (ref && (!predicate || predicate(ref))) {
        refs.push(ref);
    }
}
/**
 * Walk through a `ProjectSchema` and collect all the `ref`s.
 */
export function getAllRefsInShapes(shapes, predicate) {
    const items = [];
    const shapeVisitor = (schema, schemaPath) => {
        if (schema['@args'] && typeof schema['@args'] === 'object') {
            visitSchemaProperties(schema['@args'], schemaPath.concat(['@args']), (argSchema, argPath) => {
                addRef(items, getRefOrItemsRef(argSchema, argPath), predicate);
            });
        }
        addRef(items, getArgsReferenceWithPath(schema, schemaPath), predicate);
        const newPath = schema.items ? schemaPath.concat('items') : schemaPath;
        const newSchema = schema.items ?? schema;
        if (isRefSchema(newSchema)) {
            addRef(items, getRefOrItemsRef(newSchema, newPath), predicate);
        }
        else if (isObjectSchema(newSchema) || isAllOfSchema(newSchema) || isOneOfSchema(newSchema)) {
            visitSchemaProperties(newSchema, newPath, shapeVisitor);
        }
    };
    for (const [shapeName, shape] of Object.entries(shapes)) {
        visitSchemaProperties(shape.schema, ['shapes', shapeName, 'schema'], shapeVisitor);
    }
    return items;
}
/**
 * Get all refs
 */
export function getAllRefsInQuery(shapes, queryPath, query, predicate) {
    const refItems = [];
    const propertyVisitor = (propSchema, propSchemaPath) => {
        addRef(refItems, getRefOrItemsRef(propSchema, propSchemaPath), predicate);
    };
    if (query.args && typeof query.args === 'string') {
        addRef(refItems, getArgsReferenceWithPath(query, queryPath), predicate);
    }
    if (query.args && typeof query.args === 'object') {
        visitSchemaProperties(dereferenceSchema(shapes, query.args), queryPath, propertyVisitor);
    }
    if (query.shape) {
        const refItem = parseReturnShape(query.shape).ref;
        if (refItem) {
            addRef(refItems, {
                ...refItem,
                path: queryPath.concat(['shape'])
            }, predicate);
        }
    }
    return refItems;
}
/**
 * Collect refs from `queries` or `mutations`.
 */
export function getAllRefsInQueries(context, rootPropertyName, predicate) {
    const refItems = [];
    const rootPath = [rootPropertyName];
    for (const [queryName, query] of Object.entries(context[rootPropertyName])) {
        const queryPath = rootPath.concat([queryName]);
        const queryRefs = getAllRefsInQuery(context.shapes, queryPath, query, predicate);
        if (queryRefs.length) {
            refItems.push(...queryRefs);
        }
    }
    return refItems;
}
/**
 * Get all refs in a ProjectSchema
 * @param predicate return true to include the ref in
 */
export function getAllRefs(projectSchema, predicate) {
    return [
        ...getAllRefsInShapes(projectSchema.shapes, predicate),
        ...getAllRefsInQueries(projectSchema, 'queries', predicate),
        ...getAllRefsInQueries(projectSchema, 'mutations', predicate)
    ];
}
export function collectReferencedShapeNames(layerId, refSet) {
    const addLayerId = (ref) => ({ ...ref, layerId });
    const collect = (schema) => {
        const args = schema['@args'];
        if (args && typeof args === 'object') {
            Object.values(args.properties).forEach(collect);
        }
        const argsRefItem = getArgsReferenceWithPath(schema);
        if (argsRefItem) {
            refSet.add(refItemToString(addLayerId(argsRefItem)));
        }
        const refItem = getRefOrItemsRef(schema);
        if (refItem) {
            refSet.add(refItemToString(addLayerId(refItem)));
        }
    };
    return collect;
}
function collectServiceRefs(flattenedSchema, getLayer) {
    const refs = new Set(getAllRefs(flattenedSchema, (refItem) => refItem.layerId !== 'local').map(refItemToString));
    if (refs.size) {
        for (const ref of refs) {
            const [layerId, shapeName] = splitAtRef(ref);
            if (layerId !== 'local') {
                const layer = getLayer(layerId);
                const shape = layer?.shapes?.[shapeName];
                if (shape) {
                    visitSchemaProperties(shape.schema, ['schema'], collectReferencedShapeNames(layerId, refs));
                }
            }
        }
    }
    return refs;
}
function ensureServiceLayer(flattenedSchema, layerId) {
    flattenedSchema.serviceLayers ||= {};
    let serviceLayer = flattenedSchema.serviceLayers[layerId];
    if (!serviceLayer) {
        serviceLayer = {
            layerId,
            schemaVersion: '4.0.0',
            shapes: {},
            queries: {},
            mutations: {}
        };
        flattenedSchema.serviceLayers[layerId] = serviceLayer;
    }
    return serviceLayer;
}
function rewriteLayerId(localShapes, layerId, query) {
    const newQuery = deepClone(query);
    const propertyVisitor = (propSchema, propSchemaPath) => {
        const ref = (propSchema.items ?? propSchema)['@ref'];
        if (ref) {
            const [argsLayerId, shapeName] = splitAtRef(ref);
            if (argsLayerId === 'local' && !localShapes[shapeName]) {
                const path = propSchemaPath.concat(propSchema.items ? ['items', '@ref'] : ['@ref']);
                set(newQuery, path, `${layerId}:${shapeName}`);
            }
        }
    };
    if (query.args) {
        if (typeof query.args === 'string') {
            const [argsLayerId, shapeName] = splitAtRef(parseTemplateShape(query.args).shapeName);
            if (argsLayerId === 'local' && !localShapes[shapeName]) {
                newQuery.args = `${layerId}:${shapeName}`;
            }
        }
        else {
            visitSchemaProperties(query.args, ['args'], propertyVisitor);
        }
    }
    if (typeof query.shape === 'string') {
        const [argsLayerId, shapeName] = splitAtRef(parseTemplateShape(query.shape).shapeName);
        if (argsLayerId === 'local' && !localShapes[shapeName]) {
            newQuery.shape = `${layerId}:${shapeName}`;
        }
    }
    else {
        const ref = query.shape.items['@ref'];
        if (ref) {
            const [argsLayerId, shapeName] = splitAtRef(ref);
            if (argsLayerId === 'local' && !localShapes[shapeName]) {
                set(newQuery, ['shape', 'items', '@ref'], `${layerId}:${shapeName}`);
            }
        }
    }
    return newQuery;
}
function assignQueries(localShapes, queries, layerId, layerQueries) {
    for (const [queryName, query] of Object.entries(layerQueries)) {
        queries[queryName] = rewriteLayerId(localShapes, layerId, query);
    }
}
export function flattenSchema(projectSchema, getLayer) {
    const { layers, ...rest } = projectSchema;
    const flattenedSchema = {
        ...rest,
        shapes: {},
        queries: {},
        mutations: {},
        workflows: {},
        forms: {}
    };
    const filteredLayers = layers
        .reverse()
        .map((layer) => {
        const config = typeof layer === 'string' ? { ...layerDefaults, id: layer } : layer;
        const layerSchema = getLayer(config.id);
        if (!layerSchema) {
            throw new Error(`Unable to flatten schema for project ${projectSchema.projectId}. Cannot get layer ${config.id}`);
        }
        return config.visibility?.length ? filterLayer(layerSchema, config.visibility) : undefined;
    })
        .filter(isDefined);
    for (const filteredLayer of filteredLayers) {
        const { layerId } = filteredLayer;
        // Preserve original queries
        if (projectSchema.services?.[layerId]) {
            const serviceLayer = ensureServiceLayer(flattenedSchema, layerId);
            if (serviceLayer.queries && filteredLayer.queries) {
                Object.assign(serviceLayer.queries, filteredLayer.queries);
            }
            if (serviceLayer.mutations && filteredLayer.mutations) {
                Object.assign(serviceLayer.mutations, filteredLayer.mutations);
            }
        }
        if (filteredLayer.workflows) {
            Object.assign(flattenedSchema.workflows, filteredLayer.workflows);
        }
        if (flattenedSchema.forms && filteredLayer.forms) {
            Object.assign(flattenedSchema.forms, filteredLayer.forms);
        }
        if (filteredLayer.shapes) {
            assignShapes(flattenedSchema.shapes, filteredLayer.shapes);
        }
    }
    for (const filteredLayer of filteredLayers) {
        if (filteredLayer.queries) {
            assignQueries(flattenedSchema.shapes, flattenedSchema.queries, filteredLayer.layerId, filteredLayer.queries);
        }
        if (filteredLayer.mutations) {
            assignQueries(flattenedSchema.shapes, flattenedSchema.mutations, filteredLayer.layerId, filteredLayer.mutations);
        }
    }
    // Preserve original service schema shapes
    for (const refStr of collectServiceRefs(flattenedSchema, getLayer)) {
        const [layerId, shapeName] = splitAtRef(refStr);
        if (layerId !== 'local') {
            const shape = getLayer(layerId)?.shapes?.[shapeName];
            if (shape) {
                const serviceLayer = ensureServiceLayer(flattenedSchema, layerId);
                serviceLayer.shapes ||= {};
                serviceLayer.shapes[shapeName] = shape;
            }
        }
    }
    return flattenedSchema;
}
