"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Parser = void 0;
const fast_xml_parser_1 = require("fast-xml-parser");
const zod_1 = require("zod");
const actionSchema = zod_1.z
    .string()
    .transform((s) => s.toUpperCase())
    .pipe(zod_1.z.literal('INSERT_OR_UPDATE').or(zod_1.z.literal('UPDATE')).or(zod_1.z.literal('DELETE')));
const textElementSchema = zod_1.z.union([
    zod_1.z.record(zod_1.z.literal('#text').or(zod_1.z.string().startsWith('@_')), zod_1.z.string()),
    zod_1.z.record(zod_1.z.string().startsWith('@_display_value').or(zod_1.z.string().startsWith('@_')), zod_1.z.literal('')),
    zod_1.z.object({ '#text': zod_1.z.string() }),
]);
const updateNameSchema = zod_1.z
    .object({
    sys_update_name: zod_1.z.object({ '#text': zod_1.z.string() }),
})
    .catchall(zod_1.z.array(textElementSchema).or(textElementSchema).or(zod_1.z.string()));
const recordSchema = zod_1.z
    .object({
    sys_id: textElementSchema,
    '@_action': actionSchema.optional(),
})
    .catchall(zod_1.z.array(textElementSchema).or(textElementSchema).or(zod_1.z.string()))
    .or(zod_1.z
    .object({
    '@_action': zod_1.z.literal('delete_multiple'),
})
    .catchall(textElementSchema.or(zod_1.z.string())));
const deleteMultipleSchema = zod_1.z
    .object({
    '@_query': zod_1.z.string(),
    '@_action': zod_1.z.literal('delete_multiple').or(zod_1.z.literal('DELETE')),
})
    .catchall(zod_1.z.array(textElementSchema).or(textElementSchema).or(zod_1.z.string()));
/**
 * Choice Schema in a nested payload (example: sys_hub_action_type_definition)
 * {
 *     'sys_choice': [
 *          {
 *              '@_action': 'delete_multiple'
 *          },
 *          {
 *              '@_field': 'field_name',
 *              '@_table': 'table_name',
 *               'sys_choice' : [
 *                      {... individual records}
 *                 ]
 *          }
 *     ]
 * }
 *
 */
const choiceSchema = zod_1.z
    .object({
    '@_field': zod_1.z.string(),
    '@_table': zod_1.z.string(),
})
    .catchall(zod_1.z.array(recordSchema).or(recordSchema));
/**
 * FLAT PAYLOAD EXAMPLE:
 *
 * {
 *     "sys_script": {
 *         "name": {
 *             "#text": "Test",
 *             "@_language": "en"
 *         },
 *         "sys_id": {
 *             "#text": "1f5344fbfc2f456da4d56ec183fc5821"
 *         },
 *         "@_action": "INSERT_OR_UPDATE"
 *     },
 *     "@_table": "sys_script",
 * }
 */
const flatPayloadSchema = zod_1.z.record(zod_1.z.string(), recordSchema.or(zod_1.z.array(recordSchema)).or(zod_1.z.string()));
/**
 * NESTED PAYLOAD EXAMPLE:
 *
 * {
 *     "sys_ui_list": {
 *         "sys_ui_list_element": [
 *             {
 *                 "list_id": {
 *                     "#text": "617b3e2fa3a77110a6251d6ee31e6171",
 *                     "@_view": "Default view"
 *                 },
 *                 "sys_id": {
 *                     "#text": "a97b76afa3a77110a6251d6ee31e6183"
 *                 },
 *                 "@_action": "INSERT_OR_UPDATE"
 *             },
 *             {
 *                 "list_id": {
 *                     "#text": "617b3e2fa3a77110a6251d6ee31e6171",
 *                     "@_view": "Default view"
 *                 },
 *                 "sys_id": {
 *                     "#text": "2d7b76afa3a77110a6251d6ee31e6183"
 *                 },
 *                 "@_action": "INSERT_OR_UPDATE"
 *             }
 *         ],
 *         "sys_ui_list": {
 *             "sys_id": {
 *                 "#text": "617b3e2fa3a77110a6251d6ee31e6171"
 *             },
 *             "view": {
 *                 "#text": "Default view",
 *                 "@_display_value": "Default view"
 *             },
 *             "@_action": "INSERT_OR_UPDATE"
 *         },
 *         "@_table": "task"
 *     },
 *     "sys_update_version": {
 *         "sys_id": {
 *             "#text": "bf163201a3ef7910a6251d6ee31e615a"
 *         },
 *         "@_action": "INSERT_OR_UPDATE"
 *     },
 *     "@_example": "something"
 * }
 */
const recordUpdateSchema = zod_1.z
    .object({
    sys_id: textElementSchema,
    '@_action': actionSchema,
    payload: textElementSchema,
})
    .catchall(zod_1.z.array(textElementSchema).or(textElementSchema).or(zod_1.z.string()));
const updateSetSchema = zod_1.z
    .object({
    sys_id: textElementSchema,
    '@_action': actionSchema,
    application: textElementSchema,
})
    .catchall(zod_1.z.array(textElementSchema).or(textElementSchema).or(zod_1.z.string()));
const nestedPayloadSchema = zod_1.z.record(zod_1.z.string(), flatPayloadSchema
    .or(recordSchema)
    .or(deleteMultipleSchema)
    .or(zod_1.z.array(recordSchema.or(choiceSchema).or(deleteMultipleSchema).or(recordUpdateSchema)))
    .or(zod_1.z.string())
    .or(actionSchema));
const IGNORED_ELEMENTS = [
    'sys_scope',
    'sys_package',
    'sys_mod_count',
    'sys_class_name',
    'sys_updated_on',
    'sys_updated_by',
    'sys_created_on',
    'sys_created_by',
    'sys_update_name',
];
const IGNORED_TABLES = [
    'sys_metadata_delete',
    'sys_metadata_link',
    'sys_update_version',
    'sys_store_app',
    'sys_index',
    'sys_translated_text',
    'sys_es_latest_script',
    'sys_update_set',
    'sys_remote_update_set',
    'sys_update_xml',
];
class Parser {
    project;
    ignoredElements;
    ignoredTables;
    constructor(project, ignoredElements = IGNORED_ELEMENTS, ignoredTables = IGNORED_TABLES) {
        this.project = project;
        this.ignoredElements = new Set(ignoredElements);
        this.ignoredTables = new Set([...ignoredTables]);
    }
    async parsePayload(file) {
        const result = new fast_xml_parser_1.XMLParser({
            parseTagValue: false,
            ignoreAttributes: false,
            alwaysCreateTextNode: true,
            ignoreDeclaration: true,
            htmlEntities: true,
        }).parse(file.content.trim(), true);
        const payload = result['record_update'] ?? result['unload'];
        if (!payload) {
            return [];
        }
        const parsed = nestedPayloadSchema.safeParse(payload);
        if (!parsed.success) {
            throw new Error(`Zod error while parsing payload from file: ${file.path}`);
        }
        return this.processPayload({ path: this.project.resolvePath(file.path), content: file.content }, parsed.data);
    }
    /**
     * Recursively processes a parsed XML payload to extract ServiceNow records.
     *
     * This method iterates through each property in the payload and attempts to parse it as either:
     * - A record (or array of records) which gets processed via {@link processRecord}
     * - A nested payload which gets recursively processed
     *
     * Properties starting with `@_` are treated as XML attributes and ignored.
     * Tables in the ignored tables list and `delete_multiple` actions are skipped.
     *
     * @param file - The source file containing the XML payload
     * @param payload - The parsed and validated nested payload structure
     * @param isRecordUpdate - Whether the root of this xml is an update set and parser is parsing sys_update_xml
     * @param overrides - Optional overrides to propogate from parent to child records
     * @returns A promise resolving to an array of processed Record objects
     * @throws Error if a payload property has an invalid structure that cannot be parsed as a record or nested payload
     */
    async processPayload(file, payload, isRecordUpdate = false, overrides) {
        const records = [];
        if (!overrides?.action && payload['@_action']) {
            const action = actionSchema.parse(payload['@_action']);
            overrides = { ...overrides, action };
        }
        for (const property in payload) {
            const values = [payload[property]].flat(); // Sometimes an array, sometimes not, so normalize it
            for (const value of values) {
                const recordParseResult = recordSchema.safeParse(value);
                if (recordParseResult.success) {
                    if (property === 'sys_update_xml') {
                        const parsedUpdatePayload = recordUpdateSchema.parse(recordParseResult.data);
                        const result = new fast_xml_parser_1.XMLParser({
                            parseTagValue: false,
                            ignoreAttributes: false,
                            alwaysCreateTextNode: true,
                            ignoreDeclaration: true,
                            htmlEntities: true,
                        }).parse(parsedUpdatePayload.payload['#text'], true);
                        const payload = result['record_update'] ?? result['unload'];
                        const payloadParseResult = flatPayloadSchema.safeParse(payload);
                        if (payloadParseResult.success) {
                            records.push(...(await this.processPayload(file, payloadParseResult.data, true)));
                            continue;
                        }
                    }
                    if (property === 'sys_remote_update_set' || property === 'sys_update_set') {
                        const parsedUpdatePayload = updateSetSchema.parse(recordParseResult.data);
                        if (parsedUpdatePayload.application['#text'] !== this.project.getConfig().scopeId) {
                            // Error is swallowed, so print it here.
                            this.project
                                .getLogger()
                                .error(`Skipping update set ${parsedUpdatePayload.sys_id['#text']} from incorrect scope`);
                            throw new Error(`Skipping update set ${parsedUpdatePayload.sys_id['#text']} from incorrect scope`);
                        }
                    }
                    if (!this.ignoredTables.has(property) && recordParseResult.data['@_action'] !== 'delete_multiple') {
                        records.push(await this.processRecord(file, property, recordParseResult.data, isRecordUpdate, overrides));
                    }
                    continue;
                }
                const payloadParseResult = flatPayloadSchema.safeParse(value);
                if (payloadParseResult.success) {
                    records.push(...(await this.processPayload(file, payloadParseResult.data, isRecordUpdate)));
                    continue;
                }
                if (property.startsWith('@_')) {
                    continue; // Ignore attributes
                }
                throw new Error(`Payload property "${property}" has invalid structure: ${JSON.stringify(value, null, 4)}`);
            }
        }
        return records;
    }
    async processRecord(file, table, record, isRecordUpdate = false, overrides) {
        const { '@_action': action, ...data } = record;
        const refColumns = this.project.getPlugins().getReferenceColumns(table);
        let source = file;
        if (isRecordUpdate) {
            const updateName = updateNameSchema.safeParse(data);
            source = {
                content: '',
                path: updateName.success ? this.project.resolvePath(updateName.data.sys_update_name['#text']) : '',
            };
        }
        return this.project.getFactory().createRecord({
            source,
            table,
            action: overrides?.action ?? action ?? 'INSERT_OR_UPDATE',
            properties: Object.fromEntries(await Promise.all(Object.entries(data)
                .map(([k, v]) => {
                // Some records create duplicate XML elements (e.g. sys_atf_step's <inputs/> element)
                // This is probably a platform issue, so this is a workaround to handle such cases.
                return [k, Array.isArray(v) ? v[0] : v];
            })
                .filter((entry) => {
                const [k, v] = entry;
                return (!!v &&
                    typeof v === 'object' &&
                    !Array.isArray(v) &&
                    !this.ignoredElements.has(k) &&
                    !k.startsWith('@_'));
            })
                .map(async ([k, v]) => [
                k,
                refColumns[k] && v['#text']
                    ? await this.processReference(file, refColumns[k], v)
                    : (v['#text'] ?? ''),
            ]))),
        });
    }
    async processReference(file, refTable, ref) {
        const value = ref['#text'];
        const attributes = Object.fromEntries(Object.entries(ref)
            .filter(([k]) => k.startsWith('@_'))
            .map(([k, v]) => [k.slice(2), v]) // Remove the '@_' prefix
        );
        const coalesceStrategy = this.project.getPlugins().getCoalesceStrategy(refTable);
        if (!coalesceStrategy || !Array.isArray(coalesceStrategy)) {
            // If there is no coalesce strategy, or if it's a dynamic coalesce strategy, then the sys ID should be correct already
            return this.project.getFactory().createReference({ source: file, table: refTable, guid: value });
        }
        const coalesceKeys = {};
        for (const k of coalesceStrategy) {
            if (!attributes[k]) {
                // Not safe to perform a lookup with incomplete information, so we have no choice but to just use the existing sys ID
                return this.project.getFactory().createReference({ source: file, table: refTable, guid: value });
            }
            coalesceKeys[k] = attributes[k];
        }
        // Use the coalesce keys which can be used to perform a lookup later, and provide the existing sys ID as a fallback
        return this.project
            .getFactory()
            .createReference({ source: file, table: refTable, guid: value, keys: coalesceKeys });
    }
}
exports.Parser = Parser;
//# sourceMappingURL=parser.js.map