import { GraphQLNonNull, Kind, } from 'graphql';
import { defaultMergedResolver, } from '@graphql-tools/delegate';
import { appendObjectFields, modifyObjectFields, relocatedError, selectObjectFields, } from '@graphql-tools/utils';
import { defaultCreateProxyingResolver } from '../generateProxyingResolvers.js';
import MapFields from './MapFields.js';
export default class WrapFields {
    constructor(outerTypeName, wrappingFieldNames, wrappingTypeNames, fieldNames, prefix = 'gqtld') {
        this.outerTypeName = outerTypeName;
        this.wrappingFieldNames = wrappingFieldNames;
        this.wrappingTypeNames = wrappingTypeNames;
        this.numWraps = wrappingFieldNames.length;
        this.fieldNames = fieldNames;
        const remainingWrappingFieldNames = this.wrappingFieldNames.slice();
        const outerMostWrappingFieldName = remainingWrappingFieldNames.shift();
        if (outerMostWrappingFieldName == null) {
            throw new Error(`Cannot wrap fields, no wrapping field name provided.`);
        }
        this.transformer = new MapFields({
            [outerTypeName]: {
                [outerMostWrappingFieldName]: (fieldNode, fragments, transformationContext) => hoistFieldNodes({
                    fieldNode,
                    path: remainingWrappingFieldNames,
                    fieldNames,
                    fragments,
                    transformationContext: transformationContext,
                    prefix,
                }),
            },
        }, {
            [outerTypeName]: (value, context) => dehoistValue(value, context),
        }, (errors, context) => dehoistErrors(errors, context));
    }
    transformSchema(originalWrappingSchema, subschemaConfig) {
        const fieldNames = this.fieldNames;
        const targetFieldConfigMap = selectObjectFields(originalWrappingSchema, this.outerTypeName, !fieldNames ? () => true : fieldName => fieldNames.includes(fieldName));
        const newTargetFieldConfigMap = Object.create(null);
        for (const fieldName in targetFieldConfigMap) {
            const field = targetFieldConfigMap[fieldName];
            const newField = {
                ...field,
                resolve: defaultMergedResolver,
            };
            newTargetFieldConfigMap[fieldName] = newField;
        }
        let wrapIndex = this.numWraps - 1;
        let wrappingTypeName = this.wrappingTypeNames[wrapIndex];
        let wrappingFieldName = this.wrappingFieldNames[wrapIndex];
        let newSchema = appendObjectFields(originalWrappingSchema, wrappingTypeName, newTargetFieldConfigMap);
        for (wrapIndex--; wrapIndex > -1; wrapIndex--) {
            const nextWrappingTypeName = this.wrappingTypeNames[wrapIndex];
            newSchema = appendObjectFields(newSchema, nextWrappingTypeName, {
                [wrappingFieldName]: {
                    type: new GraphQLNonNull(newSchema.getType(wrappingTypeName)),
                    resolve: defaultMergedResolver,
                },
            });
            wrappingTypeName = nextWrappingTypeName;
            wrappingFieldName = this.wrappingFieldNames[wrapIndex];
        }
        const targetSchema = subschemaConfig.schema;
        let wrappingOperation;
        switch (this.outerTypeName) {
            case targetSchema.getQueryType()?.name:
                wrappingOperation = 'query';
                break;
            case targetSchema.getMutationType()?.name:
                wrappingOperation = 'mutation';
                break;
            case targetSchema.getSubscriptionType()?.name:
                wrappingOperation = 'subscription';
                break;
        }
        let resolve;
        if (wrappingOperation) {
            const createProxyingResolver = subschemaConfig.createProxyingResolver ?? defaultCreateProxyingResolver;
            resolve = createProxyingResolver({
                subschemaConfig,
                operation: wrappingOperation,
                fieldName: wrappingFieldName,
            });
        }
        else {
            resolve = defaultMergedResolver;
        }
        const wrappingType = new GraphQLNonNull(newSchema.getType(wrappingTypeName));
        const newFieldConfig = wrappingOperation === 'subscription'
            ? {
                type: wrappingType,
                subscribe: resolve,
                resolve: (payload) => payload,
            }
            : {
                type: wrappingType,
                resolve,
            };
        [newSchema] = modifyObjectFields(newSchema, this.outerTypeName, fieldName => !!newTargetFieldConfigMap[fieldName], {
            [wrappingFieldName]: newFieldConfig,
        });
        return this.transformer.transformSchema(newSchema, subschemaConfig);
    }
    transformRequest(originalRequest, delegationContext, transformationContext) {
        transformationContext.nextIndex = 0;
        transformationContext.paths = Object.create(null);
        return this.transformer.transformRequest(originalRequest, delegationContext, transformationContext);
    }
    transformResult(originalResult, delegationContext, transformationContext) {
        return this.transformer.transformResult(originalResult, delegationContext, transformationContext);
    }
}
function collectFields(selectionSet, fragments, fields = [], visitedFragmentNames = {}) {
    if (selectionSet != null) {
        for (const selection of selectionSet.selections) {
            switch (selection.kind) {
                case Kind.FIELD:
                    fields.push(selection);
                    break;
                case Kind.INLINE_FRAGMENT:
                    collectFields(selection.selectionSet, fragments, fields, visitedFragmentNames);
                    break;
                case Kind.FRAGMENT_SPREAD: {
                    const fragmentName = selection.name.value;
                    if (!visitedFragmentNames[fragmentName]) {
                        visitedFragmentNames[fragmentName] = true;
                        collectFields(fragments[fragmentName].selectionSet, fragments, fields, visitedFragmentNames);
                    }
                    break;
                }
                default:
                    // unreachable
                    break;
            }
        }
    }
    return fields;
}
function aliasFieldNode(fieldNode, str) {
    return {
        ...fieldNode,
        alias: {
            kind: Kind.NAME,
            value: str,
        },
    };
}
function hoistFieldNodes({ fieldNode, fieldNames, path, fragments, transformationContext, prefix, index = 0, wrappingPath = [], }) {
    const alias = fieldNode.alias != null ? fieldNode.alias.value : fieldNode.name.value;
    let newFieldNodes = [];
    if (index < path.length) {
        const pathSegment = path[index];
        for (const possibleFieldNode of collectFields(fieldNode.selectionSet, fragments)) {
            if (possibleFieldNode.name.value === pathSegment) {
                const newWrappingPath = wrappingPath.concat([alias]);
                newFieldNodes = newFieldNodes.concat(hoistFieldNodes({
                    fieldNode: possibleFieldNode,
                    fieldNames,
                    path,
                    fragments,
                    transformationContext,
                    prefix,
                    index: index + 1,
                    wrappingPath: newWrappingPath,
                }));
            }
        }
    }
    else {
        for (const possibleFieldNode of collectFields(fieldNode.selectionSet, fragments)) {
            if (!fieldNames || fieldNames.includes(possibleFieldNode.name.value)) {
                const nextIndex = transformationContext.nextIndex;
                transformationContext.nextIndex++;
                const indexingAlias = `__${prefix}${nextIndex}__`;
                transformationContext.paths[indexingAlias] = {
                    pathToField: wrappingPath.concat([alias]),
                    alias: possibleFieldNode.alias != null
                        ? possibleFieldNode.alias.value
                        : possibleFieldNode.name.value,
                };
                newFieldNodes.push(aliasFieldNode(possibleFieldNode, indexingAlias));
            }
        }
    }
    return newFieldNodes;
}
export function dehoistValue(originalValue, context) {
    if (originalValue == null) {
        return originalValue;
    }
    const newValue = Object.create(null);
    for (const alias in originalValue) {
        let obj = newValue;
        const path = context.paths[alias];
        if (path == null) {
            newValue[alias] = originalValue[alias];
            continue;
        }
        const pathToField = path.pathToField;
        const fieldAlias = path.alias;
        for (const key of pathToField) {
            obj = obj[key] = obj[key] || Object.create(null);
        }
        obj[fieldAlias] = originalValue[alias];
    }
    return newValue;
}
function dehoistErrors(errors, context) {
    if (errors === undefined) {
        return undefined;
    }
    return errors.map(error => {
        const originalPath = error.path;
        if (originalPath == null) {
            return error;
        }
        let newPath = [];
        for (const pathSegment of originalPath) {
            if (typeof pathSegment !== 'string') {
                newPath.push(pathSegment);
                continue;
            }
            const path = context.paths[pathSegment];
            if (path == null) {
                newPath.push(pathSegment);
                continue;
            }
            newPath = newPath.concat(path.pathToField, [path.alias]);
        }
        return relocatedError(error, newPath);
    });
}
