"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TEMPLATE_MUTATION = exports.TEMPLATE_GET_HANDLERS = exports.register = void 0;
const admin_1 = require("../clients/admin");
const error_1 = require("../error");
const logger_1 = require("../logger");
const types_1 = require("../types");
const registry_1 = require("./registry");
const query_template_1 = require("./query-template");
const types_2 = require("./types");
function register(config, webhookRegistry) {
    return async function register({ session, }) {
        const log = (0, logger_1.logger)(config);
        log.info('Registering webhooks', { shop: session.shop });
        const registerReturn = Object.keys(webhookRegistry).reduce((acc, topic) => {
            acc[topic] = [];
            return acc;
        }, {});
        const existingHandlers = await getExistingHandlers(config, session);
        log.debug(`Existing topics: [${Object.keys(existingHandlers).join(', ')}]`, { shop: session.shop });
        for (const topic in webhookRegistry) {
            if (!Object.prototype.hasOwnProperty.call(webhookRegistry, topic)) {
                continue;
            }
            if (types_1.privacyTopics.includes(topic)) {
                continue;
            }
            registerReturn[topic] = await registerTopic({
                config,
                session,
                topic,
                existingHandlers: existingHandlers[topic] || [],
                handlers: (0, registry_1.getHandlers)(webhookRegistry)(topic),
            });
            // Remove this topic from the list of existing handlers so we have a list of leftovers
            delete existingHandlers[topic];
        }
        // Delete any leftover handlers
        for (const topic in existingHandlers) {
            if (!Object.prototype.hasOwnProperty.call(existingHandlers, topic)) {
                continue;
            }
            const GraphqlClient = (0, admin_1.graphqlClientClass)({ config });
            const client = new GraphqlClient({ session });
            registerReturn[topic] = await runMutations({
                config,
                client,
                topic,
                handlers: existingHandlers[topic],
                operation: types_2.WebhookOperation.Delete,
            });
        }
        return registerReturn;
    };
}
exports.register = register;
async function getExistingHandlers(config, session) {
    const GraphqlClient = (0, admin_1.graphqlClientClass)({ config });
    const client = new GraphqlClient({ session });
    const existingHandlers = {};
    let hasNextPage;
    let endCursor = null;
    do {
        const query = buildCheckQuery(endCursor);
        const response = await client.request(query);
        response.data?.webhookSubscriptions?.edges.forEach((edge) => {
            const handler = buildHandlerFromNode(edge);
            if (!existingHandlers[edge.node.topic]) {
                existingHandlers[edge.node.topic] = [];
            }
            existingHandlers[edge.node.topic].push(handler);
        });
        endCursor = response.data?.webhookSubscriptions?.pageInfo.endCursor;
        hasNextPage = response.data?.webhookSubscriptions?.pageInfo.hasNextPage;
    } while (hasNextPage);
    return existingHandlers;
}
function buildCheckQuery(endCursor) {
    return (0, query_template_1.queryTemplate)(exports.TEMPLATE_GET_HANDLERS, {
        END_CURSOR: JSON.stringify(endCursor),
    });
}
function buildHandlerFromNode(edge) {
    const endpoint = edge.node.endpoint;
    let handler;
    switch (endpoint.__typename) {
        case 'WebhookHttpEndpoint':
            handler = {
                deliveryMethod: types_2.DeliveryMethod.Http,
                callbackUrl: endpoint.callbackUrl,
                // This is a dummy for now because we don't really care about it
                callback: async () => { },
            };
            break;
        case 'WebhookEventBridgeEndpoint':
            handler = {
                deliveryMethod: types_2.DeliveryMethod.EventBridge,
                arn: endpoint.arn,
            };
            break;
        case 'WebhookPubSubEndpoint':
            handler = {
                deliveryMethod: types_2.DeliveryMethod.PubSub,
                pubSubProject: endpoint.pubSubProject,
                pubSubTopic: endpoint.pubSubTopic,
            };
            break;
    }
    // Set common fields
    handler.id = edge.node.id;
    handler.includeFields = edge.node.includeFields;
    handler.metafieldNamespaces = edge.node.metafieldNamespaces;
    // Sort the array fields to make them cheaper to compare later on
    handler.includeFields?.sort();
    handler.metafieldNamespaces?.sort();
    return handler;
}
async function registerTopic({ config, session, topic, existingHandlers, handlers, }) {
    let registerResults = [];
    const { toCreate, toUpdate, toDelete } = categorizeHandlers(config, existingHandlers, handlers);
    const GraphqlClient = (0, admin_1.graphqlClientClass)({ config });
    const client = new GraphqlClient({ session });
    let operation = types_2.WebhookOperation.Create;
    registerResults = registerResults.concat(await runMutations({ config, client, topic, operation, handlers: toCreate }));
    operation = types_2.WebhookOperation.Update;
    registerResults = registerResults.concat(await runMutations({ config, client, topic, operation, handlers: toUpdate }));
    operation = types_2.WebhookOperation.Delete;
    registerResults = registerResults.concat(await runMutations({ config, client, topic, operation, handlers: toDelete }));
    return registerResults;
}
function categorizeHandlers(config, existingHandlers, handlers) {
    const handlersByKey = handlers.reduce((acc, value) => {
        acc[(0, registry_1.handlerIdentifier)(config, value)] = value;
        return acc;
    }, {});
    const existingHandlersByKey = existingHandlers.reduce((acc, value) => {
        acc[(0, registry_1.handlerIdentifier)(config, value)] = value;
        return acc;
    }, {});
    const toCreate = { ...handlersByKey };
    const toUpdate = {};
    const toDelete = {};
    for (const existingKey in existingHandlersByKey) {
        if (!Object.prototype.hasOwnProperty.call(existingHandlersByKey, existingKey)) {
            continue;
        }
        const existingHandler = existingHandlersByKey[existingKey];
        const handler = handlersByKey[existingKey];
        if (existingKey in handlersByKey) {
            delete toCreate[existingKey];
            if (!areHandlerFieldsEqual(existingHandler, handler)) {
                toUpdate[existingKey] = handler;
                toUpdate[existingKey].id = existingHandler.id;
            }
        }
        else {
            toDelete[existingKey] = existingHandler;
        }
    }
    return {
        toCreate: Object.values(toCreate),
        toUpdate: Object.values(toUpdate),
        toDelete: Object.values(toDelete),
    };
}
function areHandlerFieldsEqual(arr1, arr2) {
    const includeFieldsEqual = arraysEqual(arr1.includeFields || [], arr2.includeFields || []);
    const metafieldNamespacesEqual = arraysEqual(arr1.metafieldNamespaces || [], arr2.metafieldNamespaces || []);
    return includeFieldsEqual && metafieldNamespacesEqual;
}
function arraysEqual(arr1, arr2) {
    if (arr1.length !== arr2.length) {
        return false;
    }
    for (let i = 0; i < arr1.length; i++) {
        if (arr1[i] !== arr2[i]) {
            return false;
        }
    }
    return true;
}
async function runMutations({ config, client, topic, handlers, operation, }) {
    const registerResults = [];
    for (const handler of handlers) {
        registerResults.push(await runMutation({ config, client, topic, handler, operation }));
    }
    return registerResults;
}
async function runMutation({ config, client, topic, handler, operation, }) {
    let registerResult;
    (0, logger_1.logger)(config).debug(`Running webhook mutation`, { topic, operation });
    try {
        const query = buildMutation(config, topic, handler, operation);
        const result = await client.request(query);
        registerResult = {
            deliveryMethod: handler.deliveryMethod,
            success: isSuccess(result, handler, operation),
            result,
            operation,
        };
    }
    catch (error) {
        if (error instanceof error_1.InvalidDeliveryMethodError) {
            registerResult = {
                deliveryMethod: handler.deliveryMethod,
                success: false,
                result: { message: error.message },
                operation,
            };
        }
        else {
            throw error;
        }
    }
    return registerResult;
}
function buildMutation(config, topic, handler, operation) {
    const params = {};
    let identifier;
    if (handler.id) {
        identifier = `id: "${handler.id}"`;
    }
    else {
        identifier = `topic: ${topic}`;
    }
    const mutationArguments = {
        MUTATION_NAME: getMutationName(handler, operation),
        IDENTIFIER: identifier,
        MUTATION_PARAMS: '',
    };
    if (operation !== types_2.WebhookOperation.Delete) {
        switch (handler.deliveryMethod) {
            case types_2.DeliveryMethod.Http:
                params.callbackUrl = `"${(0, registry_1.addHostToCallbackUrl)(config, handler.callbackUrl)}"`;
                break;
            case types_2.DeliveryMethod.EventBridge:
                params.arn = `"${handler.arn}"`;
                break;
            case types_2.DeliveryMethod.PubSub:
                params.pubSubProject = `"${handler.pubSubProject}"`;
                params.pubSubTopic = `"${handler.pubSubTopic}"`;
                break;
            default:
                throw new error_1.InvalidDeliveryMethodError(`Unrecognized delivery method '${handler.deliveryMethod}'`);
        }
        if (handler.includeFields) {
            params.includeFields = JSON.stringify(handler.includeFields);
        }
        if (handler.metafieldNamespaces) {
            params.metafieldNamespaces = JSON.stringify(handler.metafieldNamespaces);
        }
        if (handler.subTopic) {
            const subTopicString = `subTopic: "${handler.subTopic}",`;
            mutationArguments.MUTATION_PARAMS = subTopicString;
        }
        const paramsString = Object.entries(params)
            .map(([key, value]) => `${key}: ${value}`)
            .join(', ');
        mutationArguments.MUTATION_PARAMS += `webhookSubscription: {${paramsString}}`;
    }
    return (0, query_template_1.queryTemplate)(exports.TEMPLATE_MUTATION, mutationArguments);
}
function getMutationName(handler, operation) {
    switch (operation) {
        case types_2.WebhookOperation.Create:
            return `${getEndpoint(handler)}Create`;
        case types_2.WebhookOperation.Update:
            return `${getEndpoint(handler)}Update`;
        case types_2.WebhookOperation.Delete:
            return 'webhookSubscriptionDelete';
        default:
            throw new error_1.ShopifyError(`Unrecognized operation '${operation}'`);
    }
}
function getEndpoint(handler) {
    switch (handler.deliveryMethod) {
        case types_2.DeliveryMethod.Http:
            return 'webhookSubscription';
        case types_2.DeliveryMethod.EventBridge:
            return 'eventBridgeWebhookSubscription';
        case types_2.DeliveryMethod.PubSub:
            return 'pubSubWebhookSubscription';
        default:
            throw new error_1.ShopifyError(`Unrecognized delivery method '${handler.deliveryMethod}'`);
    }
}
function isSuccess(result, handler, operation) {
    const mutationName = getMutationName(handler, operation);
    return Boolean(result.data &&
        result.data[mutationName] &&
        result.data[mutationName].userErrors.length === 0);
}
exports.TEMPLATE_GET_HANDLERS = `query shopifyApiReadWebhookSubscriptions {
  webhookSubscriptions(
    first: 250,
    after: {{END_CURSOR}},
  ) {
    edges {
      node {
        id
        topic
        includeFields
        metafieldNamespaces
        endpoint {
          __typename
          ... on WebhookHttpEndpoint {
            callbackUrl
          }
          ... on WebhookEventBridgeEndpoint {
            arn
          }
          ... on WebhookPubSubEndpoint {
            pubSubProject
            pubSubTopic
          }
        }
      }
    }
    pageInfo {
      endCursor
      hasNextPage
    }
  }
}`;
exports.TEMPLATE_MUTATION = `
  mutation shopifyApiCreateWebhookSubscription {
    {{MUTATION_NAME}}(
      {{IDENTIFIER}},
      {{MUTATION_PARAMS}}
    ) {
      userErrors {
        field
        message
      }
    }
  }
`;
//# sourceMappingURL=register.js.map