import { BaseEntity, BaseEntityEvent, CompositeKey, EntityFieldTSType, LogDebug, LogError, LogStatus, Metadata, } from '@memberjunction/core';
import { UserCache } from '@memberjunction/sqlserver-dataprovider';
import { GraphQLError } from 'graphql';
import { httpTransport, CloudEvent, emitterFor } from 'cloudevents';
import { MJEventType, MJGlobal, ENCRYPTED_SENTINEL, IsValueEncrypted, IsOnlyTimezoneShift } from '@memberjunction/global';
import { EncryptionEngine } from '@memberjunction/encryption';
import { PUSH_STATUS_UPDATES_TOPIC } from './PushStatusResolver.js';
import { FieldMapper } from '@memberjunction/graphql-dataprovider';
export class ResolverBase {
    static _emit = process.env.CLOUDEVENTS_HTTP_TRANSPORT ? emitterFor(httpTransport(process.env.CLOUDEVENTS_HTTP_TRANSPORT)) : null;
    static _cloudeventsHeaders = process.env.CLOUDEVENTS_HTTP_HEADERS ? JSON.parse(process.env.CLOUDEVENTS_HTTP_HEADERS) : {};
    static _eventSubscriptionKey = '___MJServer___ResolverBase___EventSubscriptions';
    get EventSubscriptions() {
        const g = MJGlobal.Instance.GetGlobalObjectStore();
        if (!g[ResolverBase._eventSubscriptionKey]) {
            LogDebug(`>>>>> MJServer.ResolverBase.EventSubscriptions: Creating new Map - this should only happen once per server instance <<<<<<`);
            g[ResolverBase._eventSubscriptionKey] = new Map();
        }
        return g[ResolverBase._eventSubscriptionKey];
    }
    async MapFieldNamesToCodeNames(entityName, dataObject, contextUser) {
        if (dataObject) {
            const md = new Metadata();
            const entityInfo = md.Entities.find((e) => e.Name === entityName);
            if (!entityInfo)
                throw new Error(`Entity ${entityName} not found in metadata`);
            const mapper = new FieldMapper();
            entityInfo.Fields.forEach((f) => {
                if (dataObject.hasOwnProperty(f.Name)) {
                    const mappedFieldName = mapper.MapFieldName(f.CodeName);
                    if (mappedFieldName !== f.Name) {
                        dataObject[mappedFieldName] = dataObject[f.Name];
                        delete dataObject[f.Name];
                    }
                }
            });
            const encryptedFields = entityInfo.EncryptedFields;
            if (encryptedFields.length > 0) {
                for (const field of encryptedFields) {
                    const fieldName = field.CodeName;
                    const value = dataObject[fieldName];
                    if (value === null || value === undefined)
                        continue;
                    const engine = EncryptionEngine.Instance;
                    await engine.Config(false, contextUser);
                    const keyMarker = field.EncryptionKeyID ? engine.GetKeyByID(field.EncryptionKeyID)?.Marker : undefined;
                    if (typeof value === 'string' && IsValueEncrypted(value, keyMarker)) {
                        if (field.AllowDecryptInAPI) {
                            if (contextUser) {
                                try {
                                    const decryptedValue = await engine.Decrypt(value, contextUser);
                                    dataObject[fieldName] = decryptedValue;
                                }
                                catch (err) {
                                    LogError(`Failed to decrypt field ${fieldName} for API response: ${err}`);
                                    dataObject[fieldName] = ENCRYPTED_SENTINEL;
                                }
                            }
                            else {
                                dataObject[fieldName] = ENCRYPTED_SENTINEL;
                            }
                        }
                        else if (!field.SendEncryptedValue) {
                            dataObject[fieldName] = ENCRYPTED_SENTINEL;
                        }
                    }
                }
            }
        }
        return dataObject;
    }
    async ArrayMapFieldNamesToCodeNames(entityName, dataObjectArray, contextUser) {
        if (dataObjectArray && dataObjectArray.length > 0) {
            for (const element of dataObjectArray) {
                await this.MapFieldNamesToCodeNames(entityName, element, contextUser);
            }
        }
        return dataObjectArray;
    }
    async FilterEncryptedFieldsForAPI(entityName, dataObject, contextUser) {
        if (!dataObject)
            return dataObject;
        const md = new Metadata();
        const entityInfo = md.Entities.find((e) => e.Name === entityName);
        if (!entityInfo)
            return dataObject;
        const encryptedFields = entityInfo.EncryptedFields;
        if (encryptedFields.length === 0)
            return dataObject;
        for (const field of encryptedFields) {
            const fieldName = field.CodeName;
            const value = dataObject[fieldName];
            if (value === null || value === undefined)
                continue;
            if (field.AllowDecryptInAPI)
                continue;
            const engine = EncryptionEngine.Instance;
            await engine.Config(false, contextUser);
            const keyMarker = field.EncryptionKeyID ? engine.GetKeyByID(field.EncryptionKeyID)?.Marker : undefined;
            if (field.SendEncryptedValue) {
                if (typeof value === 'string' && !IsValueEncrypted(value, keyMarker)) {
                    try {
                        const encryptedValue = await engine.Encrypt(value, field.EncryptionKeyID, contextUser);
                        dataObject[fieldName] = encryptedValue;
                    }
                    catch (err) {
                        LogError(`Failed to re-encrypt field ${fieldName} for API response: ${err}`);
                        dataObject[fieldName] = ENCRYPTED_SENTINEL;
                    }
                }
            }
            else {
                dataObject[fieldName] = ENCRYPTED_SENTINEL;
            }
        }
        return dataObject;
    }
    async ArrayFilterEncryptedFieldsForAPI(entityName, dataObjectArray, contextUser) {
        if (!dataObjectArray || dataObjectArray.length === 0)
            return dataObjectArray;
        const md = new Metadata();
        const entityInfo = md.Entities.find((e) => e.Name === entityName);
        if (!entityInfo)
            return dataObjectArray;
        const encryptedFields = entityInfo.Fields.filter(f => f.Encrypt && !f.AllowDecryptInAPI);
        if (encryptedFields.length === 0)
            return dataObjectArray;
        for (const element of dataObjectArray) {
            await this.FilterEncryptedFieldsForAPI(entityName, element, contextUser);
        }
        return dataObjectArray;
    }
    async findBy(provider, entity, params, contextUser) {
        const rv = provider;
        const e = provider.Entities.find((e) => e.Name === entity);
        if (!e)
            throw new Error(`Entity ${entity} not found in metadata`);
        let extraFilter = "";
        const keys = Object.keys(params);
        keys.forEach((k, i) => {
            if (i > 0)
                extraFilter += ' AND ';
            const field = e.Fields.find((f) => f.Name === k);
            if (!field)
                throw new Error(`Field ${k} not found in entity ${entity}`);
            const quotes = field.NeedsQuotes ? "'" : '';
            extraFilter += `${k} = ${quotes}${params[k]}${quotes}`;
        });
        const result = await rv.RunView({
            EntityName: entity,
            ExtraFilter: extraFilter,
        }, contextUser);
        if (result && result.Success && result.Results.length > 0) {
            return result.Results;
        }
        else {
            return [];
        }
    }
    async RunViewByNameGeneric(viewInput, provider, userPayload, pubSub) {
        try {
            const rv = provider;
            const result = await rv.RunView({
                EntityName: 'User Views',
                ExtraFilter: "Name='" + viewInput.ViewName + "'",
            }, userPayload.userRecord);
            if (result && result.Success && result.Results.length > 0) {
                const viewInfo = result.Results[0];
                return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, viewInput.SaveViewResults, viewInput.Fields, viewInput.IgnoreMaxRows, viewInput.ExcludeDataFromAllPriorViewRuns, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow);
            }
            else {
                LogError(`RunViewByNameGeneric: View ${viewInput.ViewName} not found or no results returned`);
                return null;
            }
        }
        catch (err) {
            console.log(err);
            return null;
        }
    }
    async RunViewByIDGeneric(viewInput, provider, userPayload, pubSub) {
        try {
            const contextUser = this.GetUserFromPayload(userPayload);
            const viewInfo = await provider.GetEntityObject('User Views', contextUser);
            await viewInfo.Load(viewInput.ViewID);
            return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, viewInput.SaveViewResults, viewInput.Fields, viewInput.IgnoreMaxRows, viewInput.ExcludeDataFromAllPriorViewRuns, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow);
        }
        catch (err) {
            console.log(err);
            return null;
        }
    }
    async RunDynamicViewGeneric(viewInput, provider, userPayload, pubSub) {
        try {
            const md = provider;
            const entity = md.Entities.find((e) => e.Name === viewInput.EntityName);
            if (!entity)
                throw new Error(`Entity ${viewInput.EntityName} not found in metadata`);
            const viewInfo = {
                ID: '',
                Entity: viewInput.EntityName,
                EntityID: entity.ID,
                EntityBaseView: entity.BaseView,
            };
            return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, false, viewInput.Fields, viewInput.IgnoreMaxRows, false, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow);
        }
        catch (err) {
            console.log(err);
            return null;
        }
    }
    async RunViewsGeneric(viewInputs, provider, userPayload) {
        const md = provider;
        const contextUser = this.GetUserFromPayload(userPayload);
        let params = [];
        for (const viewInput of viewInputs) {
            try {
                let viewInfo = null;
                if (viewInput.ViewName) {
                    viewInfo = this.safeFirstArrayElement(await this.findBy(provider, 'User Views', { Name: viewInput.ViewName }, userPayload.userRecord));
                }
                else if (viewInput.ViewID) {
                    viewInfo = await provider.GetEntityObject('User Views', contextUser);
                    await viewInfo.Load(viewInput.ViewID);
                }
                else if (viewInput.EntityName) {
                    const entity = md.Entities.find((e) => e.Name === viewInput.EntityName);
                    if (!entity) {
                        throw new Error(`Entity ${viewInput.EntityName} not found in metadata`);
                    }
                    viewInfo = {
                        ID: '',
                        Entity: viewInput.EntityName,
                        EntityID: entity.ID,
                        EntityBaseView: entity.BaseView,
                    };
                }
                else {
                    throw new Error('Unable to determine input type');
                }
                params.push({
                    viewInfo,
                    provider,
                    extraFilter: viewInput.ExtraFilter,
                    orderBy: viewInput.OrderBy,
                    userSearchString: viewInput.UserSearchString,
                    excludeUserViewRunID: viewInput.ExcludeUserViewRunID,
                    overrideExcludeFilter: viewInput.OverrideExcludeFilter,
                    saveViewResults: viewInput.EntityName ? false : viewInput.SaveViewResults,
                    fields: viewInput.Fields,
                    ignoreMaxRows: viewInput.IgnoreMaxRows,
                    maxRows: viewInput.MaxRows,
                    startRow: viewInput.StartRow,
                    excludeDataFromAllPriorViewRuns: viewInput.EntityName ? false : viewInput.ExcludeDataFromAllPriorViewRuns,
                    forceAuditLog: viewInput.ForceAuditLog,
                    auditLogDescription: viewInput.AuditLogDescription,
                    resultType: viewInput.ResultType,
                    userPayload,
                });
            }
            catch (err) {
                LogError(err);
                return null;
            }
        }
        let results = await this.RunViewsGenericInternal(params);
        return results;
    }
    static _priorEmittedData = [];
    async EmitCloudEvent({ component, event, eventCode, args }) {
        if (ResolverBase._emit && event === MJEventType.ComponentEvent && eventCode === BaseEntity.BaseEventCode) {
            const extendedType = args instanceof BaseEntityEvent ? `.${args.type}` : '';
            const type = `MemberJunction.${event}${extendedType}`;
            const source = `${process.env.CLOUDEVENTS_SOURCE ?? 'MemberJunction'}`;
            const subject = args instanceof BaseEntityEvent ? args.baseEntity.EntityInfo.CodeName : undefined;
            const data = args?.baseEntity?.GetAll() ?? {};
            const cloudEvent = new CloudEvent({ type, source, subject, data });
            try {
                const pkey = args.baseEntity.PrimaryKeys;
                const emittedData = { Entity: args.baseEntity.EntityInfo.Name, PKey: pkey };
                if (ResolverBase._priorEmittedData.find((e) => {
                    if (e.Entity !== emittedData.Entity)
                        return false;
                    const pkey2 = e.PKey;
                    if (pkey.KeyValuePairs.length !== pkey2.KeyValuePairs.length)
                        return false;
                    for (const kv of pkey.KeyValuePairs) {
                        const kv2 = pkey2.KeyValuePairs.find((k) => k.FieldName === kv.FieldName);
                        if (!kv2 || kv2.Value !== kv.Value)
                            return false;
                    }
                    return true;
                })) {
                    console.log(`IMPORTANT: CloudEvent already emitted for ${JSON.stringify(emittedData)}`);
                }
                LogStatus(`Emitting CloudEvent: ${JSON.stringify(cloudEvent)}`);
                const cloudeventTransportResponse = await ResolverBase._emit(cloudEvent, { headers: ResolverBase._cloudeventsHeaders });
                const cloudeventResponse = JSON.stringify(cloudeventTransportResponse);
                if (/error/i.test(cloudeventResponse)) {
                    console.error('CloudEvent ERROR', cloudeventResponse);
                }
            }
            catch (e) {
                console.error('CloudEvent ERROR', JSON.stringify(e));
            }
        }
    }
    CheckUserReadPermissions(entityName, userPayload) {
        const md = new Metadata();
        const entityInfo = md.Entities.find((e) => e.Name === entityName);
        if (!userPayload) {
            throw new Error(`userPayload is null`);
        }
        if (entityInfo) {
            const userInfo = UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload.email.toLowerCase().trim());
            if (!userInfo) {
                throw new Error(`User ${userPayload.email} not found in metadata`);
            }
            const userPermissions = entityInfo.GetUserPermisions(userInfo);
            if (!userPermissions.CanRead) {
                throw new Error(`User ${userPayload.email} does not have read permissions on ${entityInfo.Name}`);
            }
        }
        else {
            throw new Error(`Entity not found in metadata`);
        }
    }
    async RunViewGenericInternal(provider, viewInfo, extraFilter, orderBy, userSearchString, excludeUserViewRunID, overrideExcludeFilter, saveViewResults, fields, ignoreMaxRows, excludeDataFromAllPriorViewRuns, forceAuditLog, auditLogDescription, resultType, userPayload, maxRows, startRow) {
        try {
            if (!viewInfo || !userPayload)
                return null;
            const md = provider;
            const user = UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload?.email.toLowerCase().trim());
            if (!user)
                throw new Error(`User ${userPayload?.email} not found in metadata`);
            const entityInfo = md.Entities.find((e) => e.Name === viewInfo.Entity);
            if (!entityInfo)
                throw new Error(`Entity ${viewInfo.Entity} not found in metadata`);
            const rv = md;
            let rt = 'simple';
            if (resultType?.trim().toLowerCase() === 'count_only') {
                rt = 'count_only';
            }
            let optimizedFields = fields;
            if (fields?.length) {
                const primaryKeys = entityInfo.PrimaryKeys.map(pk => pk.Name);
                const missingPrimaryKeys = primaryKeys.filter(pk => !fields.find(f => f.toLowerCase() === pk.toLowerCase()));
                if (missingPrimaryKeys.length) {
                    optimizedFields = [...fields, ...missingPrimaryKeys];
                }
            }
            const result = await rv.RunView({
                ViewID: viewInfo.ID,
                ViewName: viewInfo.Name,
                EntityName: viewInfo.Entity,
                ExtraFilter: extraFilter,
                OrderBy: orderBy,
                Fields: optimizedFields,
                UserSearchString: userSearchString,
                ExcludeUserViewRunID: excludeUserViewRunID,
                OverrideExcludeFilter: overrideExcludeFilter,
                SaveViewResults: saveViewResults,
                ExcludeDataFromAllPriorViewRuns: excludeDataFromAllPriorViewRuns,
                IgnoreMaxRows: ignoreMaxRows,
                MaxRows: maxRows,
                StartRow: startRow,
                ForceAuditLog: forceAuditLog,
                AuditLogDescription: auditLogDescription,
                ResultType: rt,
            }, user);
            const mapper = new FieldMapper();
            if (result?.Success && result.Results?.length) {
                for (const r of result.Results) {
                    mapper.MapFields(r);
                }
                await this.ArrayFilterEncryptedFieldsForAPI(viewInfo.Entity, result.Results, user);
            }
            return result;
        }
        catch (err) {
            const error = err;
            LogError({
                service: 'RunView',
                operation: 'RunViewGenericInternal',
                error: error.message,
                entityName: viewInfo?.Entity,
                errorType: error.constructor.name,
                stack: error.message?.includes('not found in metadata') ? undefined : error.stack
            });
            throw err;
        }
    }
    async RunViewsGenericInternal(params) {
        try {
            if (!params.length)
                return [];
            let md = null;
            const rv = params[0].provider;
            let runViewParams = [];
            let contextUser = null;
            if (params[0]?.userPayload?.email) {
                const userEmail = params[0].userPayload.email.toLowerCase().trim();
                const user = UserCache.Users.find(u => u.Email.toLowerCase().trim() === userEmail);
                if (!user) {
                    throw new Error(`User ${userEmail} not found in metadata`);
                }
                contextUser = user;
            }
            const validatedEntities = new Set();
            md = new Metadata();
            for (const param of params) {
                if (param.viewInfo) {
                    const entityName = param.viewInfo.Entity;
                    if (!validatedEntities.has(entityName)) {
                        const entityInfo = md.Entities.find(e => e.Name === entityName);
                        if (!entityInfo) {
                            throw new Error(`Entity ${entityName} not found in metadata`);
                        }
                        validatedEntities.add(entityName);
                    }
                }
                let rt = 'simple';
                if (param.resultType?.trim().toLowerCase() === 'count_only') {
                    rt = 'count_only';
                }
                runViewParams.push({
                    ViewID: param.viewInfo.ID,
                    ViewName: param.viewInfo.Name,
                    EntityName: param.viewInfo.Entity,
                    ExtraFilter: param.extraFilter,
                    OrderBy: param.orderBy,
                    Fields: param.fields,
                    UserSearchString: param.userSearchString,
                    ExcludeUserViewRunID: param.excludeUserViewRunID,
                    OverrideExcludeFilter: param.overrideExcludeFilter,
                    SaveViewResults: param.saveViewResults,
                    ExcludeDataFromAllPriorViewRuns: param.excludeDataFromAllPriorViewRuns,
                    IgnoreMaxRows: param.ignoreMaxRows,
                    MaxRows: param.maxRows,
                    StartRow: param.startRow,
                    ForceAuditLog: param.forceAuditLog,
                    AuditLogDescription: param.auditLogDescription,
                    ResultType: rt,
                });
            }
            const runViewResults = await rv.RunViews(runViewParams, contextUser);
            const mapper = new FieldMapper();
            for (let i = 0; i < runViewResults.length; i++) {
                const runViewResult = runViewResults[i];
                if (runViewResult?.Success && runViewResult.Results?.length) {
                    for (const result of runViewResult.Results) {
                        mapper.MapFields(result);
                    }
                    const entityName = params[i]?.viewInfo?.Entity;
                    if (entityName && contextUser) {
                        await this.ArrayFilterEncryptedFieldsForAPI(entityName, runViewResult.Results, contextUser);
                    }
                }
            }
            return runViewResults;
        }
        catch (err) {
            console.log(err);
            throw err;
        }
    }
    async createRecordAccessAuditLogRecord(provider, userPayload, entityName, recordId) {
        try {
            const md = provider;
            const entityInfo = md.Entities.find((e) => e.Name.trim().toLowerCase() === entityName.trim().toLowerCase());
            if (!entityInfo)
                throw new Error(`Entity ${entityName} not found in metadata`);
            if (entityInfo.AuditRecordAccess) {
                const userInfo = UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload?.email.toLowerCase().trim());
                const auditLogTypeName = 'Record Accessed';
                const auditLogType = md.AuditLogTypes.find((a) => a.Name.trim().toLowerCase() === auditLogTypeName.trim().toLowerCase());
                if (!userInfo)
                    throw new Error(`User ${userPayload?.email} not found in metadata`);
                if (!auditLogType)
                    throw new Error(`Audit Log Type ${auditLogTypeName} not found in metadata`);
                return await this.createAuditLogRecord(provider, userPayload, null, auditLogTypeName, 'Success', null, entityInfo.ID, recordId);
            }
        }
        catch (e) {
            console.log(e);
        }
    }
    getRowLevelSecurityWhereClause(provider, entityName, userPayload, type, returnPrefix) {
        const md = provider;
        const entityInfo = md.Entities.find((e) => e.Name.trim().toLowerCase() === entityName.trim().toLowerCase());
        if (!entityInfo)
            throw new Error(`Entity ${entityName} not found in metadata`);
        const user = UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload?.email.toLowerCase().trim());
        if (!user)
            throw new Error(`User ${userPayload?.email} not found in metadata`);
        return entityInfo.GetUserRowLevelSecurityWhereClause(user, type, returnPrefix);
    }
    async createAuditLogRecord(provider, userPayload, authorizationName, auditLogTypeName, status, details, entityId, recordId) {
        try {
            const md = provider;
            const userInfo = UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload?.email.toLowerCase().trim());
            const authorization = authorizationName
                ? md.Authorizations.find((a) => a.Name.trim().toLowerCase() === authorizationName.trim().toLowerCase())
                : null;
            const auditLogType = md.AuditLogTypes.find((a) => a.Name.trim().toLowerCase() === auditLogTypeName.trim().toLowerCase());
            if (!userInfo)
                throw new Error(`User ${userPayload?.email} not found in metadata`);
            if (!auditLogType)
                throw new Error(`Audit Log Type ${auditLogTypeName} not found in metadata`);
            const auditLog = await md.GetEntityObject('Audit Logs', userInfo);
            auditLog.NewRecord();
            auditLog.UserID = userInfo.ID;
            auditLog.AuditLogTypeID = auditLogType.ID;
            if (authorization) {
                auditLog.AuthorizationID = authorization.ID;
            }
            if (status?.trim().toLowerCase() === 'success')
                auditLog.Status = 'Success';
            else
                auditLog.Status = 'Failed';
            if (details)
                auditLog.Details = details;
            auditLog.EntityID = entityId;
            if (recordId)
                auditLog.RecordID = recordId;
            if (await auditLog.Save())
                return auditLog;
            else
                throw new Error(`Error saving audit log record`);
        }
        catch (err) {
            console.log(err);
            return null;
        }
    }
    safeFirstArrayElement(arr) {
        if (arr && arr.length > 0) {
            return arr[0];
        }
        return null;
    }
    packageSPParam(paramValue, quoteString) {
        return paramValue === null || paramValue === undefined ? null : quoteString + paramValue + quoteString;
    }
    GetUserFromEmail(email) {
        return UserCache.Users.find((u) => u.Email.toLowerCase().trim() === email.toLowerCase().trim());
    }
    GetUserFromPayload(userPayload) {
        if (!userPayload)
            return undefined;
        if (userPayload.userRecord)
            return userPayload.userRecord;
        if (!userPayload.email)
            return undefined;
        return UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload.email.toLowerCase().trim());
    }
    get MJCoreSchema() {
        return Metadata.Provider.ConfigData.MJCoreSchemaName;
    }
    ListenForEntityMessages(entityObject, pubSub, userPayload) {
        const uniqueKey = entityObject.EntityInfo.Name;
        if (!this.EventSubscriptions.has(uniqueKey)) {
            LogDebug(`ResolverBase.ListenForEntityMessages: About to call MJGlobal.Instance.GetEventListener() to get the event listener subscription for ${uniqueKey}`);
            const theSub = MJGlobal.Instance.GetEventListener(false).subscribe(async (event) => {
                if (event) {
                    const baseEntity = event.args?.baseEntity;
                    if (baseEntity?.EntityInfo?.Name !== uniqueKey) {
                        return;
                    }
                    const baseEntityValues = baseEntity ? baseEntity.GetAll() : null;
                    const eventToLog = { entityName: entityObject.EntityInfo.Name, baseEntity: baseEntityValues, event: event.event, eventCode: event.eventCode };
                    LogDebug(`ResolverBase.ListenForEntityMessages: Received Event from within MJGlobal.Instance.GetEventListener() callback. Will call EmitCloudEvent() next\nEvent data:\n${JSON.stringify(eventToLog)}`);
                    await this.EmitCloudEvent(event);
                    LogDebug(`ResolverBase.ListenForEntityMessages: EmitCloudEvent() completed successfully`);
                    if (event.args && event.args instanceof BaseEntityEvent) {
                        const baseEntityEvent = event.args;
                        LogDebug('ResolverBase.ListenForEntityMessages: About to publish PUSH_STATUS_UPDATES_TOPIC');
                        pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
                            message: JSON.stringify({
                                status: 'OK',
                                type: 'EntityObjectStatusMessage',
                                entityName: baseEntityEvent.baseEntity.EntityInfo.Name,
                                primaryKey: baseEntityEvent.baseEntity.PrimaryKey,
                                message: event.args.payload,
                            }),
                            sessionId: userPayload.sessionId,
                        });
                    }
                }
            });
            this.EventSubscriptions.set(uniqueKey, theSub);
        }
    }
    async CreateRecord(entityName, input, provider, userPayload, pubSub) {
        if (await this.BeforeCreate(provider, input)) {
            const entityObject = await provider.GetEntityObject(entityName, this.GetUserFromPayload(userPayload));
            entityObject.NewRecord();
            entityObject.SetMany(input);
            this.ListenForEntityMessages(entityObject, pubSub, userPayload);
            if (await entityObject.Save()) {
                await this.AfterCreate(provider, input);
                const contextUser = this.GetUserFromPayload(userPayload);
                return await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), contextUser);
            }
            else
                throw entityObject.LatestResult?.Message;
        }
        else
            return null;
    }
    async BeforeCreate(provider, input) {
        return true;
    }
    async AfterCreate(provider, input) { }
    async UpdateRecord(entityName, input, provider, userPayload, pubSub) {
        if (await this.BeforeUpdate(provider, input)) {
            const userInfo = this.GetUserFromPayload(userPayload);
            const entityObject = await provider.GetEntityObject(entityName, userInfo);
            const entityInfo = entityObject.EntityInfo;
            const clientNewValues = {};
            Object.keys(input).forEach((key) => {
                if (key !== 'OldValues___') {
                    clientNewValues[key] = input[key];
                }
            });
            if (entityInfo.TrackRecordChanges || !input.OldValues___) {
                const cKey = new CompositeKey(entityInfo.PrimaryKeys.map((pk) => {
                    return {
                        FieldName: pk.Name,
                        Value: input[pk.CodeName],
                    };
                }));
                if (await entityObject.InnerLoad(cKey)) {
                    if (input.OldValues___) {
                        await this.TestAndSetClientOldValuesToDBValues(input, clientNewValues, entityObject, userInfo);
                    }
                    else {
                        entityObject.SetMany(input);
                    }
                }
                else {
                    throw new GraphQLError(`Record not found for ${entityName} with key ${JSON.stringify(cKey)}`, {
                        extensions: { code: 'LOAD_ENTITY_ERROR', entityName },
                    });
                }
            }
            else {
                const oldValues = {};
                input.OldValues___?.forEach((item) => (oldValues[item.Key] = item.Value));
                await entityObject.LoadFromData(oldValues);
                entityObject.SetMany(clientNewValues);
            }
            this.ListenForEntityMessages(entityObject, pubSub, userPayload);
            if (await entityObject.Save()) {
                await this.AfterUpdate(provider, input);
                return await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), userInfo);
            }
            else {
                throw new GraphQLError(entityObject.LatestResult?.Message ?? 'Unknown error', {
                    extensions: { code: 'SAVE_ENTITY_ERROR', entityName },
                });
            }
        }
        else
            throw new GraphQLError('Save Canceled by BeforeSave() handler in ResolverBase', {
                extensions: { code: 'SAVE_ENTITY_ERROR', entityName },
            });
    }
    async TestAndSetClientOldValuesToDBValues(input, clientNewValues, entityObject, contextUser) {
        const clientOldValues = {};
        input.OldValues___.forEach((item) => {
            const field = entityObject.EntityInfo.Fields.find((f) => f.CodeName === item.Key);
            let val = item.Value;
            if ((val === null || val === undefined) && field.DefaultValue !== null && field.DefaultValue !== undefined && !field.AllowsNull)
                val = field.DefaultValue;
            if (field) {
                switch (field.TSType) {
                    case EntityFieldTSType.Number:
                        if (val == null && val == undefined) {
                            val = null;
                        }
                        else {
                            let typeLowered = field.Type.toLowerCase();
                            switch (typeLowered) {
                                case 'int':
                                case 'smallint':
                                case 'bigint':
                                case 'tinyint':
                                    val = parseInt(val);
                                    break;
                                case 'money':
                                case 'smallmoney':
                                case 'decimal':
                                case 'numeric':
                                case 'float':
                                    val = parseFloat(val);
                                    break;
                                default:
                                    val = parseFloat(val);
                                    break;
                            }
                        }
                        break;
                    case EntityFieldTSType.Boolean:
                        val = val === null || val === undefined || val === 'false' || val === '0' || parseInt(val) === 0 ? false : true;
                        break;
                    case EntityFieldTSType.Date:
                        if (val !== null && val !== undefined && val.toString().trim() !== '' && !isNaN(val))
                            val = parseInt(val);
                        val = val !== null && val !== undefined ? new Date(val) : null;
                        break;
                    default:
                        break;
                }
            }
            clientOldValues[item.Key] = val;
        });
        const dbValues = entityObject.GetAll();
        const dbDifferences = [];
        Object.keys(clientOldValues).forEach((key) => {
            const f = entityObject.EntityInfo.Fields.find((f) => f.CodeName === key);
            let different = false;
            switch (typeof clientOldValues[key]) {
                case 'number':
                    different = clientOldValues[key] !== dbValues[key];
                    break;
                case 'boolean':
                    different = clientOldValues[key] !== dbValues[key];
                    break;
                case 'object':
                    if (clientOldValues[key] instanceof Date) {
                        const clientDate = clientOldValues[key];
                        const dbDate = dbValues[key];
                        if (dbDate == null || !(dbDate instanceof Date)) {
                            different = true;
                        }
                        else if (clientDate.getTime() !== dbDate.getTime()) {
                            const sqlType = f?.SQLFullType?.trim().toLowerCase() || '';
                            if (sqlType !== 'datetimeoffset' && IsOnlyTimezoneShift(clientDate, dbDate)) {
                                console.warn(`Timezone hour shift detected on field "${key}" (${sqlType || 'datetime'}). ` +
                                    `Client: ${clientDate.toISOString()}, DB: ${dbDate.toISOString()}. ` +
                                    `Consider using datetimeoffset to avoid timezone ambiguity.`);
                                different = false;
                            }
                            else {
                                different = true;
                            }
                        }
                    }
                    else if (clientOldValues[key] === null) {
                        different = dbValues[key] !== null;
                    }
                    break;
                default:
                    different = clientOldValues[key] !== dbValues[key];
                    break;
            }
            if (different && f && !f.ReadOnly) {
                dbDifferences.push({
                    FieldName: key,
                    ClientOldValue: clientOldValues[key],
                    DBValue: dbValues[key],
                });
            }
        });
        if (dbDifferences.length > 0) {
            const clientDifferences = [];
            Object.keys(clientOldValues).forEach((key) => {
                const f = entityObject.EntityInfo.Fields.find((f) => f.CodeName === key);
                if (clientOldValues[key] !== clientNewValues[key] && f && f.AllowUpdateAPI && !f.IsPrimaryKey) {
                    clientDifferences.push({
                        FieldName: key,
                        ClientOldValue: clientOldValues[key],
                        ClientNewValue: clientNewValues[key],
                    });
                }
            });
            const overlap = clientDifferences.filter((cd) => dbDifferences.find((dd) => dd.FieldName === cd.FieldName));
            if (overlap.length > 0) {
                const msg = {
                    Message: 'Inconsistency between old values provided for changed fields, and the values of one or more of those fields in the database. Save operation continued with warning.',
                    ClientDifferences: clientDifferences,
                    DBDifferences: dbDifferences,
                    Overlap: overlap,
                };
                console.warn('Entity save inconsistency detected but allowing save to continue:', JSON.stringify(msg));
                LogError({
                    service: 'ResolverBase',
                    operation: 'TestAndSetClientOldValuesToDBValues',
                    error: `Entity save inconsistency detected: ${JSON.stringify(msg)}`,
                    details: {
                        entityName: entityObject.EntityInfo.Name,
                        clientDifferences: clientDifferences,
                        dbDifferences: dbDifferences,
                        overlap: overlap
                    }
                });
                try {
                    const md = new Metadata();
                    const errorLogEntity = await md.GetEntityObject('Error Logs', contextUser);
                    errorLogEntity.Code = 'ENTITY_SAVE_INCONSISTENCY';
                    errorLogEntity.Message = `Entity save inconsistency detected for ${entityObject.EntityInfo.Name}: ${JSON.stringify(msg)}`;
                    errorLogEntity.Status = 'Warning';
                    errorLogEntity.Category = 'Entity Save';
                    errorLogEntity.CreatedBy = contextUser.Email || contextUser.Name;
                    const saveResult = await errorLogEntity.Save();
                    if (!saveResult) {
                        console.error('Failed to save ErrorLog record');
                    }
                }
                catch (errorLogError) {
                    console.error('Error creating ErrorLog record:', errorLogError);
                }
            }
        }
        entityObject.SetMany(clientNewValues);
    }
    async DeleteRecord(entityName, key, options, provider, userPayload, pubSub) {
        if (await this.BeforeDelete(provider, key)) {
            const entityObject = await provider.GetEntityObject(entityName, this.GetUserFromPayload(userPayload));
            await entityObject.InnerLoad(key);
            const returnValue = entityObject.GetAll();
            this.ListenForEntityMessages(entityObject, pubSub, userPayload);
            if (await entityObject.Delete(options)) {
                await this.AfterDelete(provider, key);
                return returnValue;
            }
            else {
                throw new GraphQLError(entityObject.LatestResult?.Message ?? 'Unknown error', {
                    extensions: { code: 'DELETE_ENTITY_ERROR', entityName },
                });
            }
        }
        else {
            throw new GraphQLError('Delete operation canceled by BeforeDelete() handler in ResolverBase', {
                extensions: { code: 'DELETE_ENTITY_ERROR', entityName },
            });
        }
    }
    async BeforeDelete(provider, key) {
        return true;
    }
    async AfterDelete(provider, key) { }
    async BeforeUpdate(provider, input) {
        return true;
    }
    async AfterUpdate(provider, input) { }
}
//# sourceMappingURL=ResolverBase.js.map