import { mergeWithArrayConcat } from '@takeshape/util';
import assign from 'lodash/fp/assign.js';
import omit from 'lodash/fp/omit.js';
import isEmpty from 'lodash/isEmpty.js';
import { parseTemplateShape } from "../refs.js";
import { isAllOfSchema, isOneOfSchema, isRefSchema } from "./type-utils.js";
/**
 * Safely turn a "template" shape reference (`@args`, `args`, `shape`) into a `RefItem`
 *
 * @param context A partial project schema
 * @param refExpression
 * A ref expression might include "template" name and refer to a local or remote
 * shape, `Foo` or `CreateArgs<Foo>` or `CreateArgs<remote:Foo>`.*
 */
export function refExpressionToRefItem(refExpression) {
    const { shapeName, template } = parseTemplateShape(refExpression);
    return atRefToRefItem(shapeName, template);
}
/**
 * Sugar for converting a `refExpression` directly into a shape, without the
 * intermediate `refItem`.
 *
 * @param context A partial project schema
 * @param refExpression
 * A ref expression might include "template" name and refer to a local or remote
 * shape, `Foo` or `CreateArgs<Foo>` or `CreateArgs<remote:Foo>`.
 */
export function refExpressionToShape(shapes, refExpression) {
    return refItemToShape(shapes, refExpressionToRefItem(refExpression));
}
/**
 * Provided a service name and a referenced type. Supports 2 or 3 part
 * references, e.g., `shopify:my-store:ProductInput` or `my-store:ProductInput`.
 *
 * @returns
 * A 2 member array, where 0 is the service name, 1 is the referenced type.
 */
export function splitAtRef(atRef) {
    const refParts = atRef.split(':');
    if (refParts.length === 1) {
        return ['local', atRef];
    }
    if (refParts.length === 2) {
        return [refParts[0], refParts[1]];
    }
    throw new Error(`Invalid ref string ${atRef}`);
}
/**
 * Parses and returns a typeName, serviceKey, and potentially a namespace for
 * the type.
 */
export function atRefToRefItem(atRef, template) {
    const [serviceKey, typeName] = splitAtRef(atRef);
    return {
        typeName,
        layerId: serviceKey,
        template
    };
}
/**
 * Given a content schema and project schema, finds a `@ref` or `$ref` and
 * returns a `RefItem` suitable for loading a referenced schema...
 */
export function getRef(refSchema) {
    if (refSchema['@ref']) {
        // @ref: my-service:ShapeName
        return atRefToRefItem(refSchema['@ref']);
    }
}
export function getRefWithPath(refSchema, schemaPath = []) {
    const refItem = getRef(refSchema);
    if (refItem) {
        return {
            ...refItem,
            path: schemaPath.concat(['@ref'])
        };
    }
}
/**
 * Tests for a `@ref` or `$ref` property directly on the passed schema.
 *
 * @param schema Any schema that might could have a ref property.
 */
export function hasRefProperty(schema) {
    return Boolean(schema['@ref'] ?? schema.$ref);
}
/**
 * From a schema with a ref, will return the top-level ref, and potentially
 * a ref item from a nested `items` property.
 */
export function getRefOrItemsRef(refSchema, schemaPath = []) {
    const refItem = getRefWithPath(refSchema, schemaPath);
    if (refItem) {
        return refItem;
    }
    if (refSchema.items) {
        return getRefOrItemsRef(refSchema.items, schemaPath.concat(['items']));
    }
}
/**
 * Turns a `RefItem` into an `@ref`.
 */
export function refItemToString(refItem) {
    return `${refItem.layerId}:${refItem.typeName}`;
}
/**
 * Get a Shape referenced by a `RefItem`.
 */
export function refItemToShape(shapes, refItem) {
    return shapes[refItemToString(refItem)];
}
/**
 * Helper fn to omit `ref` props from the target schema, and then extend it with the source schema.
 */
export const omitRefAndExtend = (targetSchema, sourceSchema) => assign(omit(['@ref', '$ref'], targetSchema), sourceSchema);
/**
 * If there is a $ref, this will insert a content schema from the top level
 * in place of the reference.
 */
export function followRef(shapes, contentSchema) {
    const refItem = getRef(contentSchema);
    if (refItem) {
        const referencedSchema = refItemToShape(shapes, refItem)?.schema;
        if (referencedSchema) {
            return omitRefAndExtend(contentSchema, referencedSchema);
        }
    }
    return contentSchema;
}
/**
 * Returns whether a schema has refs.
 */
export function hasRef(contentSchema) {
    if (isRefSchema(contentSchema)) {
        return true;
    }
    if (isAllOfSchema(contentSchema)) {
        return Boolean(contentSchema.allOf.find(hasRef));
    }
    if (isOneOfSchema(contentSchema)) {
        return Boolean(contentSchema.oneOf.find(hasRef));
    }
    return false;
}
/**
 * Returns whether a schema has refs which can be followed.
 */
export function hasResolvableRef(shapes, contentSchema) {
    if (isRefSchema(contentSchema)) {
        return Boolean(refItemToShape(shapes, getRef(contentSchema)));
    }
    if (isAllOfSchema(contentSchema)) {
        return Boolean(contentSchema.allOf.find((s) => hasResolvableRef(shapes, s)));
    }
    if (isOneOfSchema(contentSchema)) {
        return Boolean(contentSchema.oneOf.find((s) => hasResolvableRef(shapes, s)));
    }
    return false;
}
export function getArgsReferenceWithPath(argsSchema, schemaPath = []) {
    const isQuery = 'args' in argsSchema;
    const args = isQuery ? argsSchema.args : argsSchema['@args'];
    const refItem = typeof args === 'string' ? refExpressionToRefItem(args) : undefined;
    if (refItem) {
        return {
            ...refItem,
            path: schemaPath.concat([isQuery ? 'args' : '@args'])
        };
    }
}
/*
 * Helper fn to omit `allOf` props from the target schema, and then extend it with the source schema.
 */
const omitAllOfAndExtend = (targetSchema, sourceSchema) => assign(omit(['allOf'], targetSchema), sourceSchema);
export function dereferenceSchema(shapes, shapeOrFieldSchema, schemaPath = []) {
    const resolveAll = (resolveSchema, resolvePath) => {
        try {
            if (isAllOfSchema(resolveSchema)) {
                const allOfSchema = resolveSchema.allOf
                    .map((stub, index) => resolveAll(stub, [...resolvePath, 'allOf', index]))
                    .reduce((prev, curr) => mergeWithArrayConcat(prev, curr), {});
                return isEmpty(allOfSchema) ? undefined : omitAllOfAndExtend(resolveSchema, allOfSchema);
            }
            if (isRefSchema(resolveSchema)) {
                const refItem = getRefWithPath(resolveSchema);
                if (!refItem) {
                    throw new Error('ref could not be parsed');
                }
                const refSchema = refItemToShape(shapes, refItem);
                if (refSchema) {
                    return resolveAll(omitRefAndExtend(resolveSchema, refSchema), refItem.path);
                }
                return;
            }
            return resolveSchema;
        }
        catch (err) {
            throw new Error(`error at '${resolvePath.join('.')}', ${err.message}`);
        }
    };
    return resolveAll(shapeOrFieldSchema, schemaPath) ?? shapeOrFieldSchema;
}
export function parseReturnShape(shape) {
    if (typeof shape === 'object') {
        if (isRefSchema(shape.items)) {
            const ref = getRef(shape.items);
            if (!ref) {
                throw new Error(`Could not parse ${JSON.stringify(shape.items)}: invalid ref`);
            }
            return { isArray: true, ref, shapeName: refItemToString(ref) };
        }
        if (typeof shape.items.type !== 'string') {
            throw new Error(`Could not parse ${JSON.stringify(shape.items)}: invalid type`);
        }
        return { isArray: true, shapeName: shape.items.type };
    }
    const ref = refExpressionToRefItem(shape);
    const shapeName = refItemToString(ref);
    return { isArray: false, shapeName, ref, template: ref.template };
}
