import {JsonClientResponse} from "../client/JsonClientResponse";
import {storage as clientStorage} from "../storage";
import {Base64} from "../util/Base64";
import {Log} from "../util/Log";
import {StringDictionary} from "../util/StringDictionary";
import {FetchClient} from "../ws/FetchClient";
import {ContentRedirectionVisitor} from "./ContentRedirectionVisitor";
import {DialogRedirectionVisitor} from "./DialogRedirectionVisitor";
import {DialogRequest} from "./DialogRequest";
import {DialogVisitor} from "./DialogVisitor";
import {LargePropertyVisitor} from "./LargePropertyVisitor";
import {ReadLargePropertyParametersVisitor} from "./ReadLargePropertyParametersVisitor";
import {RecordSetVisitor} from "./RecordSetVisitor";
import {RecordVisitor} from "./RecordVisitor";
import {WriteLargePropertyParametersVisitor} from "./WriteLargePropertyParametersVisitor";

// @FIX_THIS

// remove this:
const storage = clientStorage;

// uncomment this
// const storage = clientStorage.getSecureInstance();
// Set the secret key like this:
// storage.secretKey = 'secret_key';

/**
 *
 */
export class DialogProxyTools {

    // Model Types
    private static ACTION_PARAMETERS_MODEL_TYPE = 'hxgn.api.dialog.ActionParameters';
    private static ANNOTATION_MODEL_TYPE = 'hxgn.api.dialog.Annotation';
    private static DIALOG_MESSAGE_MODEL_TYPE = 'hxgn.api.dialog.DialogMessage';
    private static EDITOR_DIALOG_MODEL_TYPE = 'hxgn.api.dialog.EditorDialog';
    private static LOGIN_MODEL_TYPE = 'hxgn.api.dialog.Login';
    private static PROPERTY_MODEL_TYPE = 'hxgn.api.dialog.Property';
    private static PROPERTY_DEF_MODEL_TYPE = 'hxgn.api.dialog.PropertyDef';
    private static QUERY_DIALOG_MODEL_TYPE = 'hxgn.api.dialog.QueryDialog';
    private static RECORD_MODEL_TYPE = 'hxgn.api.dialog.Record';
    private static RECORD_SET_MODEL_TYPE = 'hxgn.api.dialog.RecordSet';
    private static REFERRING_DIALOG_MODEL_TYPE = 'hxgn.api.dialog.ReferringDialog';
    private static REFERRING_WORKBENCH_MODEL_TYPE = 'hxgn.api.dialog.ReferringWorkbench';
    private static SESSION_MODEL_TYPE = 'hxgn.api.dialog.Session';

    // Storage Keys
    private static CONTENT_STORAGE_KEY =            '${userId}.${tenantId}.${contentId}.${sequence}.content';
    private static DIALOG_STORAGE_KEY =             '${userId}.${tenantId}.${dialogId}.dialog';
    private static PROPERTY_COMMIT_STORAGE_KEY =    '${userId}.${tenantId}.${dialogId}.${propertyName}.propertycommit';
    private static RECORD_COMMIT_STORAGE_KEY =      '${userId}.${tenantId}.${dialogId}.recordcommit';
    private static RECORD_SET_STORAGE_KEY =         '${userId}.${tenantId}.${dialogId}.recordset';
    private static RECORD_STORAGE_KEY =             '${userId}.${tenantId}.${dialogId}.record';
    private static REDIRECTION_STORAGE_KEY =        '${userId}.${tenantId}.${stateId}.${actionId}.redirection';
    private static STORAGE_KEY_PREFIX =             '${userId}.${tenantId}.';

    private static COMMON_FETCH_CLIENT = new FetchClient();

    public static async captureDialog(userId: string, baseUrl: string, tenantId: string, sessionId: string, dialogId: string, referringDialogId): Promise<object> {
        const thisMethod = 'DialogProxyTools::captureDialog';
        // GET DIALOG //
        const resourcePath = `tenants/${tenantId}/sessions/${sessionId}/dialogs/${dialogId}`;
        // Log.info(`${thisMethod} -- capturing online dialog: ${resourcePath}`);
        const dialogJcr = await DialogProxyTools.commonFetchClient().getJson(baseUrl, resourcePath);
        if (dialogJcr.statusCode !== 200) {
            throw new Error(`Unexpected result when getting dialog ${dialogId}: ${dialogJcr.statusCode}`);
        }
        // Log.info(`${thisMethod} -- dialog: ${JSON.stringify(dialogJcr.value)}`);
        // WRITE DIALOG //
        const dialogVisitor = new DialogVisitor(dialogJcr.value);
        const beforeDialog = dialogVisitor.copyAsJsonObject();
        dialogVisitor.deriveDialogIdsFromDialogNameAndRecordId();
        // Not all referring objects are dialogs, so we must check that a dialog id is present
        if (referringDialogId) {
            dialogVisitor.visitAndSetReferringDialogId(referringDialogId);
        }
        // Log.info(`${thisMethod} -- writing online dialog to offline dialog id: ${dialogVisitor.visitId()}`);
        // Log.info(`${thisMethod} -- writing online dialog to offline storage: ${dialogVisitor.copyAsJsonString()}`);
        await this.writeDialog(userId, tenantId, dialogVisitor);
        return {beforeDialog, afterDialog: dialogVisitor.enclosedJsonObject()};
    }

    public static async captureMenuActionRedirectionAndDialog(userId: string, baseUrl: string, tenantId: string, sessionId: string, dialogId: string, offlineDialogId: string, actionId: string, targetId: string): Promise<any> {
        const thisMethod = 'DialogProxyTools::captureMenuActionRedirectionAndDialog';
        const resourcePath = `tenants/${tenantId}/sessions/${sessionId}/dialogs/${dialogId}/actions/${actionId}`;
        // Log.info(`${thisMethod} -- capturing menu redirection and dialog: ${resourcePath}`);
        // GET REDIRECTION //
        const actionParameters = {
            targets: [targetId],
            type: "hxgn.api.dialog.ActionParameters"
        };
        // Log.info(`${thisMethod} -- capturing online dialog redirection: ${resourcePath}`);
        const dialogRedirectionJcr = await DialogProxyTools.commonFetchClient().postJson(baseUrl, resourcePath, actionParameters);
        if (dialogRedirectionJcr.statusCode !== 303) {
            throw new Error(`Unexpected result when posting menu dialog ${dialogId} action ${actionId}: ${dialogRedirectionJcr.statusCode}`);
        }
        // Log.info(`${thisMethod} -- menu action redirection: ${JSON.stringify(dialogRedirectionJcr.value)}`);
        // WRITE REDIRECTION //
        const dialogRedirectionVisitor = new DialogRedirectionVisitor(dialogRedirectionJcr.value);
        const beforeDialogRedirection = dialogRedirectionVisitor.copyAsJsonObject();
        dialogRedirectionVisitor.deriveDialogIdsFromDialogNameAndRecordId();
        dialogRedirectionVisitor.visitAndSetReferringDialogId(offlineDialogId);
        let actionIdAtTargetId = actionId;
        if (targetId) {
            const targetIdEncoded = Base64.encodeUrlSafeString(targetId);
            actionIdAtTargetId = actionIdAtTargetId + '@' + targetIdEncoded;
        }
        // Log.info(`${thisMethod} -- writing online dialog redirection with dialog id: ${dialogRedirectionVisitor.visitDialogId()}`);
        // Log.info(`${thisMethod} -- writing online dialog redirection with referring dialog id: ${dialogRedirectionVisitor.visitReferringDialogId()}`);
        // Log.info(`${thisMethod} -- writing online dialog redirection with record id: ${dialogRedirectionVisitor.visitRecordId()}`);
        // Log.info(`${thisMethod} -- writing online dialog redirection to offline redirection id: ${dialogRedirectionVisitor.visitId()}`);
        // Log.info(`${thisMethod} -- writing online dialog redirection to offline storage: ${dialogRedirectionVisitor.copyAsJsonString()}`);
        const referringDialogId = dialogRedirectionVisitor.visitReferringDialogId();
        await this.writeDialogRedirection(userId, tenantId, referringDialogId, actionIdAtTargetId, dialogRedirectionVisitor);
        // CAPTURE DIALOG //
        const beforeDialogId = beforeDialogRedirection['dialogId'];
        const beforeAndAfterDialog = await this.captureDialog(userId, baseUrl, tenantId, sessionId, beforeDialogId, referringDialogId);
        return {beforeDialogRedirection, afterDialogRedirection: dialogRedirectionVisitor.enclosedJsonObject(),
            beforeDialog: beforeAndAfterDialog['beforeDialog'], afterDialog: beforeAndAfterDialog['afterDialog']};
    }

    public static async captureRecord(userId: string, baseUrl: string, tenantId: string, sessionId: string, beforeAndAfterValues: any, listDialogName: string): Promise<RecordVisitor> {
        const thisMethod = 'DialogProxyTools::captureRecord';
        // ONLINE
        const onlineRootDialogVisitor = new DialogVisitor(beforeAndAfterValues.beforeDialog);
        const onlineEditorDialogVisitor = onlineRootDialogVisitor.visitChildAtName(listDialogName);
        const onlineEditorDialogId = onlineEditorDialogVisitor.visitId();
        const onlineEditorRecordPath = `tenants/${tenantId}/sessions/${sessionId}/dialogs/${onlineEditorDialogId}/record`;
        // Log.info(`${thisMethod} -- capturing online record: ${onlineEditorRecordPath}`);
        const onlineEditorRecordJcr = await DialogProxyTools.commonFetchClient().getJson(baseUrl, onlineEditorRecordPath);
        if (onlineEditorRecordJcr.statusCode !== 200) {
            throw new Error(`Unexpected result when getting record: ${onlineEditorRecordJcr.statusCode}`);
        }
        const onlineEditorRecordVisitor = new RecordVisitor(onlineEditorRecordJcr.value);
        // OFFLINE
        const offlineRootDialogVisitor = new DialogVisitor(beforeAndAfterValues.afterDialog);
        const offlineEditorDialogVisitor = offlineRootDialogVisitor.visitChildAtName(listDialogName);
        const offlineEditorDialogId = offlineEditorDialogVisitor.visitId();
        // WRITE TO STORAGE
        // Log.info(`${thisMethod} -- writing online record to offline editor dialog id: ${offlineEditorDialogId}`);
        // Log.info(`${thisMethod} -- writing online record to offline storage: ${onlineEditorRecordVisitor.copyAsJsonString()}`);
        await DialogProxyTools.writeRecord(userId, tenantId, offlineEditorDialogId, onlineEditorRecordVisitor);
        return onlineEditorRecordVisitor;
    }

    public static async captureRecordSet(userId: string, baseUrl: string, tenantId: string, sessionId: string, beforeAndAfterValues: any, listDialogName: string): Promise<RecordSetVisitor> {
        const thisMethod = 'DialogProxyTools::captureRecordSet';
        // ONLINE
        const onlineRootDialogVisitor = new DialogVisitor(beforeAndAfterValues.beforeDialog);
        const onlineQueryDialogVisitor = onlineRootDialogVisitor.visitChildAtName(listDialogName);
        const onlineQueryDialogId = onlineQueryDialogVisitor.visitId();
        const onlineQueryRecordsPath = `tenants/${tenantId}/sessions/${sessionId}/dialogs/${onlineQueryDialogId}/records`;
        const onlineQueryParameters = {
            fetchDirection: "FORWARD",
            fetchMaxRecords: 999,
            type: "hxgn.api.dialog.QueryParameters"
        };
        // Log.info(`${thisMethod} -- capturing online record set: ${onlineQueryRecordsPath}`);
        const onlineQueryRecordsJcr = await DialogProxyTools.commonFetchClient().postJson(baseUrl, onlineQueryRecordsPath, onlineQueryParameters);
        if (onlineQueryRecordsJcr.statusCode !== 200) {
            throw new Error(`Unexpected result when getting records: ${onlineQueryRecordsJcr.statusCode}`);
        }
        const onlineQueryRecordSetVisitor = new RecordSetVisitor(onlineQueryRecordsJcr.value);
        // OFFLINE
        const offlineRootDialogVisitor = new DialogVisitor(beforeAndAfterValues.afterDialog);
        const offlineQueryDialogVisitor = offlineRootDialogVisitor.visitChildAtName(listDialogName);
        const offlineQueryDialogId = offlineQueryDialogVisitor.visitId();
        // WRITE TO STORAGE
        // Log.info(`${thisMethod} -- writing online record set to offline query dialog id: ${offlineQueryDialogId}`);
        // Log.info(`${thisMethod} -- writing online record set to offline storage: ${onlineQueryRecordSetVisitor.copyAsJsonString()}`);
        await DialogProxyTools.writeRecordSet(userId, tenantId, offlineQueryDialogId, onlineQueryRecordSetVisitor);
        return onlineQueryRecordSetVisitor;
    }

    public static async captureWorkbenchActionRedirectionAndDialog(userId: string, baseUrl: string, tenantId: string, sessionId: string, workbenchId: string, actionId: string): Promise<any> {
        const thisMethod = 'DialogProxyTools::captureWorkbenchActionRedirectionAndDialog';
        const resourcePath = `tenants/${tenantId}/sessions/${sessionId}/workbenches/${workbenchId}/actions/${actionId}`;
        // Log.info(`${thisMethod} -- capturing workbench redirection and dialog: ${resourcePath}`);
        // GET REDIRECTION //
        // Log.info(`${thisMethod} -- capturing online dialog redirection: ${resourcePath}`);
        const dialogRedirectionJcr = await DialogProxyTools.commonFetchClient().postJson(baseUrl, resourcePath, {});
        if (dialogRedirectionJcr.statusCode !== 303) {
            throw new Error(`Unexpected result when posting workbench ${workbenchId} action ${actionId}: ${dialogRedirectionJcr.statusCode}`);
        }
        // Log.info(`${thisMethod} -- workbench action redirection: ${JSON.stringify(dialogRedirectionJcr.value)}`);
        // WRITE REDIRECTION //
        const dialogRedirectionVisitor = new DialogRedirectionVisitor(dialogRedirectionJcr.value);
        const beforeDialogRedirection = dialogRedirectionVisitor.copyAsJsonObject();
        dialogRedirectionVisitor.deriveDialogIdsFromDialogNameAndRecordId();
        // Log.info(`${thisMethod} -- writing online dialog redirection to offline redirection id: ${dialogRedirectionVisitor.visitId()}`);
        // Log.info(`${thisMethod} -- writing online dialog redirection to offline storage: ${dialogRedirectionVisitor.copyAsJsonString()}`);
        await this.writeDialogRedirection(userId, tenantId, workbenchId, actionId, dialogRedirectionVisitor);
        // CAPTURE DIALOG //
        const beforeDialogId = beforeDialogRedirection['dialogId'];
        const beforeAndAfterDialog = await this.captureDialog(userId, baseUrl, tenantId, sessionId, beforeDialogId, null);
        return {beforeDialogRedirection, afterDialogRedirection: dialogRedirectionVisitor.enclosedJsonObject(),
            beforeDialog: beforeAndAfterDialog['beforeDialog'], afterDialog: beforeAndAfterDialog['afterDialog']};
    }

    public static async clearAllStorageAt(userId: string, tenantId: string): Promise<void> {
        const thisMethod = 'DialogProxyTools::clearAllStorageAt';
        let prefix = this.STORAGE_KEY_PREFIX.replace('${userId}', userId);
        prefix = prefix.replace('${tenantId}', tenantId);
        // Log.info(`${thisMethod} -- ************** BEGIN CLEAR ALL STORAGE AT **************`);
        const allKeys = await storage.getAllKeys();
        for (const k of allKeys) {
            if (k.startsWith(prefix)) {
                await storage.removeItem(k);
                // Log.info(`${thisMethod} -- Removed from storage: ${k}`);
            }
        }
        // Log.info(`${thisMethod} -- ************** END CLEAR ALL STORAGE AT **************`);
    }

    public static commonFetchClient(): FetchClient {
        return this.COMMON_FETCH_CLIENT;
    }

    public static constructDialogMessageModel(message: string) {
        return {type: this.DIALOG_MESSAGE_MODEL_TYPE, message};
    }

    public static constructLoginModel(userId: string, password: string): object {
        return {
            "userId": userId,
            "password": password,
            "clientType": "MOBILE",
            "deviceProperties": {},
            "type": this.LOGIN_MODEL_TYPE
        };
    }

    public static constructRedirectionStorageKey(userId: string, tenantId: string, stateId: string, actionId: string): string {
        let key = this.REDIRECTION_STORAGE_KEY.replace('${userId}', userId);
        key = key.replace('${tenantId}', tenantId);
        key = key.replace('${stateId}', stateId);
        key = key.replace('${actionId}', actionId);
        return key;
    }

    public static constructRequestNotValidDuringOfflineMode(action: string, resourcePath: string): JsonClientResponse {
        return new JsonClientResponse(this.constructDialogMessageModel(`${action} at ${resourcePath} is not valid during offline mode: `), 400);
    }

    /**
     * Construct an empty null redirection with the following defaults:
     * referringObject will be a type of "hxgn.api.dialog.ReferringDialog"
     *      dialogAlias=null
     *      dialogName=null
     *      dialogMode='READ'
     *      actionId=null
     *      dialogId=null
     * refreshNeeded=true
     * id=randomly generated value
     */
    public static constructNullRedirection(tenantId: string, sessionId: string): StringDictionary {
        const nullRedirectionId = DialogProxyTools.constructNullRedirectionId();
        return {
            "tenantId": tenantId,
            "referringObject": {
                "dialogMode": "READ",
                "dialogAlias": null,
                "dialogName": null,
                "actionId": null,
                "type": "hxgn.api.dialog.ReferringDialog",
                "dialogId": null
            },
            "refreshNeeded": true,
            "sessionId": sessionId,
            "id": nullRedirectionId,
            "type": "hxgn.api.dialog.NullRedirection"
        };
    }

    public static constructNullRedirectionId(): string {
        return `null_redirection__offline_${Date.now()}`;
    }

    // ----- MODEL QUERY METHODS ----- //

    public static isActionParametersModel(jsonObject: object): boolean {
        if (!jsonObject || !jsonObject['type']) {
            return false;
        }
        return jsonObject['type'] === this.ACTION_PARAMETERS_MODEL_TYPE;
    }

    public static isAnnotationModel(jsonObject: object): boolean {
        if (!jsonObject || !jsonObject['type']) {
            return false;
        }
        return jsonObject['type'] === this.ANNOTATION_MODEL_TYPE;
    }

    public static isDialogModel(jsonObject: object): boolean {
        if (!jsonObject || !jsonObject['type']) {
            return false;
        }
        return jsonObject['type'] === this.EDITOR_DIALOG_MODEL_TYPE ||
            jsonObject['type'] === this.QUERY_DIALOG_MODEL_TYPE;
    }

    public static isLoginModel(jsonObject: object): boolean {
        if (!jsonObject || !jsonObject['type']) {
            return false;
        }
        return jsonObject['type'] === this.LOGIN_MODEL_TYPE;
    }

    public static isPropertyModel(jsonObject: object): boolean {
        if (!jsonObject || !jsonObject['type']) {
            return false;
        }
        return jsonObject['type'] === this.PROPERTY_MODEL_TYPE;
    }

    public static isPropertyDefModel(jsonObject: object): boolean {
        if (!jsonObject || !jsonObject['type']) {
            return false;
        }
        return jsonObject['type'] === this.PROPERTY_DEF_MODEL_TYPE;
    }

    public static isRecordModel(jsonObject: object): boolean {
        if (!jsonObject || !jsonObject['type']) {
            return false;
        }
        return jsonObject['type'] === this.RECORD_MODEL_TYPE;
    }

    public static isRecordSetModel(jsonObject: object): boolean {
        if (!jsonObject || !jsonObject['type']) {
            return false;
        }
        return jsonObject['type'] === this.RECORD_SET_MODEL_TYPE;
    }

    public static isReferringDialogModel(jsonObject: object): boolean {
        if (!jsonObject || !jsonObject['type']) {
            return false;
        }
        return jsonObject['type'] === this.REFERRING_DIALOG_MODEL_TYPE;
    }

    public static isReferringWorkbenchModel(jsonObject: object): boolean {
        if (!jsonObject || !jsonObject['type']) {
            return false;
        }
        return jsonObject['type'] === this.REFERRING_WORKBENCH_MODEL_TYPE;
    }

    public static isSessionModel(jsonObject: object): boolean {
        if (!jsonObject || !jsonObject['type']) {
            return false;
        }
        return jsonObject['type'] === this.SESSION_MODEL_TYPE;
    }

    // ----- PATH QUERY METHODS ----- //

    public static readDialogAsOfflineResponse(userId: string, request: DialogRequest): Promise<JsonClientResponse> {
        return this.readDialogAsVisitor(userId, request).then(dialogVisitor => {
            return dialogVisitor ?
                new JsonClientResponse(dialogVisitor.enclosedJsonObject(), 200) :
                this.constructRequestNotValidDuringOfflineMode('readDialogAsOfflineResponse', request.resourcePath());
        });
    }

    public static readDialogAsVisitor(userId: string, request: DialogRequest): Promise<DialogVisitor> {
        const thisMethod = 'DialogProxyTools::readDialogAsVisitor';
        const pathFields = request.deconstructGetDialogPath();
        const tenantId = pathFields.tenantId;
        const sessionId = pathFields.sessionId;
        const dialogId = pathFields.dialogId;
        let key = this.DIALOG_STORAGE_KEY.replace('${tenantId}', tenantId);
        key = key.replace('${userId}', userId);
        key = key.replace('${dialogId}', dialogId);
        // Log.info(`${thisMethod} -- reading for dialog at key: ${key}`);
        return storage.getJson(key).then(jsonObject => jsonObject ? new DialogVisitor(jsonObject) : null);
    }

    public static readDialogRedirectionAsVisitor(userId: string, tenantId: string, stateId: string, actionId: string): Promise<DialogRedirectionVisitor> {
        const thisMethod = 'DialogProxyTools::readDialogRedirectionAsVisitor';
        const key = this.constructRedirectionStorageKey(userId, tenantId, stateId, actionId);
        // Log.info(`${thisMethod} -- reading for redirection at key: ${key}`);
        return storage.getJson(key).then(jsonObject => jsonObject ? new DialogRedirectionVisitor(jsonObject) : null);
    }

    public static readMenuActionRedirectionAsOfflineResponse(userId: string, request: DialogRequest): Promise<JsonClientResponse> {
        return this.readMenuActionRedirectionAsVisitor(userId, request).then(dialogRedirectionVisitor => {
            return dialogRedirectionVisitor ?
                new JsonClientResponse(dialogRedirectionVisitor.enclosedJsonObject(), 303) :
                this.constructRequestNotValidDuringOfflineMode('readMenuActionRedirectionAsOfflineResponse', request.resourcePath());
        });
    }

    public static readMenuActionRedirectionAsVisitor(userId: string, request: DialogRequest): Promise<DialogRedirectionVisitor> {
        const pathFields = request.deconstructPostMenuActionPath();
        let actionIdAtTargetId = request.actionId();
        const targetId = request.targetId();
        if (targetId) {
            const targetIdEncoded = Base64.encodeUrlSafeString(targetId);
            actionIdAtTargetId = `${request.actionId()}@${targetIdEncoded}`;
        }
        return this.readDialogRedirectionAsVisitor(userId, pathFields.tenantId, pathFields.dialogId, actionIdAtTargetId);
    }

    public static readPropertyCommit(userId: string, tenantId: string, dialogId: string, propertyName: string): Promise<any> {
        let key = this.PROPERTY_COMMIT_STORAGE_KEY.replace('${userId}', userId);
        key = key.replace('${tenantId}', tenantId);
        key = key.replace('${dialogId}', dialogId);
        key = key.replace('${propertyName}', propertyName);
        return storage.getJson(key);
    }

    public static readRecordAsOfflineResponse(userId: string, request: DialogRequest): Promise<JsonClientResponse> {
        return this.readRecordAsVisitorFromRequest(userId, request).then(recordVisitor => {
            return recordVisitor ?
                new JsonClientResponse(recordVisitor.enclosedJsonObject(), 200) :
                this.constructRequestNotValidDuringOfflineMode('readRecordAsOfflineResponse', request.resourcePath());
        });
    }

    public static readRecordAsVisitor(userId: string, tenantId: string, dialogId: string): Promise<RecordVisitor> {
        let key = this.RECORD_STORAGE_KEY.replace('${userId}', userId);
        key = key.replace('${tenantId}', tenantId);
        key = key.replace('${dialogId}', dialogId);
        return storage.getJson(key).then(jsonObject => jsonObject ? new RecordVisitor(jsonObject) : null);
    }

    public static readRecordAsVisitorFromRequest(userId: string, request: DialogRequest): Promise<RecordVisitor> {
        const pathFields = request.deconstructGetRecordPath();
        const tenantId = pathFields.tenantId;
        const dialogId = pathFields.dialogId;
        return this.readRecordAsVisitor(userId, tenantId, dialogId);
    }

    public static readRecordCommitAsVisitor(userId: string, tenantId: string, dialogId: string): Promise<RecordVisitor> {
        let key = this.RECORD_COMMIT_STORAGE_KEY.replace('${userId}', userId);
        key = key.replace('${tenantId}', tenantId);
        key = key.replace('${dialogId}', dialogId);
        return storage.getJson(key).then(jsonObject => jsonObject ? new RecordVisitor(jsonObject) : null);
    }

    public static readRecordSetAsOfflineResponse(userId: string, request: DialogRequest): Promise<JsonClientResponse> {
        return this.readRecordSetAsVisitor(userId, request).then(recordSetVisitor => {
            return recordSetVisitor ?
                new JsonClientResponse(recordSetVisitor.enclosedJsonObject(), 200) :
                this.constructRequestNotValidDuringOfflineMode('readRecordSetAsOfflineResponse', request.resourcePath());
        });
    }

    public static async readRecordSetAsVisitor(userId: string, request: DialogRequest): Promise<RecordSetVisitor> {
        const pathFields = request.deconstructPostRecordsPath();
        const tenantId = pathFields.tenantId;
        const sessionId = pathFields.sessionId;
        const dialogId = pathFields.dialogId;
        let key = this.RECORD_SET_STORAGE_KEY.replace('${tenantId}', tenantId);
        key = key.replace('${userId}', userId);
        key = key.replace('${dialogId}', dialogId);
        const jsonObject = await storage.getJson(key);
        if (!jsonObject) {
            return null;
        }
        const recordSetVisitor = new RecordSetVisitor(jsonObject);
        if (request.body().fromRecordId) {
            recordSetVisitor.fromRecordId(request.body().fromRecordId);
        }
        return recordSetVisitor;
    }

    public static readSessionContentAsOfflineResponse(userId: string, request: DialogRequest): Promise<JsonClientResponse> {
        const pathFields = request.deconstructPostSessionContentPath();
        const tenantId = pathFields.tenantId;
        const contentId = pathFields.contentId;
        const parametersVisitor = new ReadLargePropertyParametersVisitor(request.body());
        const sequence = parametersVisitor.visitSequence();
        return this.readSessionContentAsVisitor(userId, tenantId, contentId, sequence).then(largePropertyVisitor => {
            return largePropertyVisitor ?
                new JsonClientResponse(largePropertyVisitor.enclosedJsonObject(), 200) :
                this.constructRequestNotValidDuringOfflineMode('readSessionContentAsOfflineResponse', request.resourcePath());
        });
    }

    public static readSessionContentAsVisitor(userId: string, tenantId: string, contentId: string, sequence: number): Promise<LargePropertyVisitor> {
        let key = this.CONTENT_STORAGE_KEY.replace('${userId}', userId);
        key = key.replace('${tenantId}', tenantId);
        key = key.replace('${contentId}', contentId);
        key = key.replace('${sequence}', sequence.toString());
        return storage.getJson(key).then(jsonObject => jsonObject ? new LargePropertyVisitor(jsonObject) : null);
    }

    public static readWorkbenchActionRedirectionAsOfflineResponse(userId: string, request: DialogRequest): Promise<JsonClientResponse> {
        const pathFields = request.deconstructPostWorkbenchActionPath();
        return this.readDialogRedirectionAsVisitor(userId, pathFields.tenantId, pathFields.workbenchId, pathFields.actionId).then(dialogRedirectionVisitor => {
            return dialogRedirectionVisitor ?
                new JsonClientResponse(dialogRedirectionVisitor.enclosedJsonObject(), 303) :
                this.constructRequestNotValidDuringOfflineMode('readWorkbenchActionRedirectionAsOfflineResponse', request.resourcePath());
        });
    }

    public static async showStoredDialogNavigation(): Promise<void> {
        const thisMethod = 'DialogProxyTools::showAllRedirectionStorageKeys';
        // Log.info(`${thisMethod} -- ************** BEGIN SHOW ALL REDIRECTION STORAGE KEYS **************`);
        const allKeys = await storage.getAllKeys();
        for (const k of allKeys) {
            if (k.endsWith('.redirection')) {
                const redirection = await storage.getJson(k);
                // Log.info(`${thisMethod} -- ${k} => \n${redirection.dialogName}(id=${redirection.id})\n`);
            }
        }
        // Log.info(`${thisMethod} -- ************** END SHOW ALL REDIRECTION STORAGE KEYS **************`);
    }

    public static async showAllStorageKeys(): Promise<void> {
        const thisMethod = 'DialogProxyTools::showAllStorageKeys';
        // Log.info(`${thisMethod} -- ************** BEGIN SHOW ALL STORAGE KEYS **************`);
        const allKeys = await storage.getAllKeys();
        for (const k of allKeys) {
            const v = await clientStorage.getItem(k);
            // Log.info(`${thisMethod} -- ${k}`);
        }
        // Log.info(`${thisMethod} -- ************** END SHOW ALL STORAGE KEYS **************`);
    }

    public static async showAllStorageKeysAndValues(): Promise<void> {
        const thisMethod = 'DialogProxyTools::showAllStorageKeysAndValues';
        // Log.info(`${thisMethod} -- ************** BEGIN SHOW ALL STORAGE KEYS AND VALUES **************`);
        const allKeys = await storage.getAllKeys();
        for (const k of allKeys) {
            const v = await clientStorage.getItem(k);
            // Log.info(`${thisMethod} -- ${k}: ${v}`);
        }
        // Log.info(`${thisMethod} -- ************** END SHOW ALL STORAGE KEYS AND VALUES **************`);
    }

    public static writeContentChunk(userId: string, tenantId: string, contentId: string, sequence: number, largePropertyVisitor: LargePropertyVisitor): Promise<void> {
        let key = this.CONTENT_STORAGE_KEY.replace('${userId}', userId);
        key = key.replace('${tenantId}', tenantId);
        key = key.replace('${contentId}', contentId);
        key = key.replace('${sequence}', sequence.toString());
        return storage.setJson(key, largePropertyVisitor.enclosedJsonObject());
    }

    public static writeContentRedirection(userId: string, tenantId: string, stateId: string, actionId: string,
                                          contentRedirectionVistor: ContentRedirectionVisitor): Promise<void>
    {
        const key = this.constructRedirectionStorageKey(userId, tenantId, stateId, actionId);
        return storage.setJson(key, contentRedirectionVistor.enclosedJsonObject());
    }

    public static writeDialog(userId: string, tenantId: string, dialogVisitor: DialogVisitor): Promise<void> {
        let key = this.DIALOG_STORAGE_KEY.replace('${userId}', userId);
        key = key.replace('${tenantId}', tenantId);
        key = key.replace('${dialogId}', dialogVisitor.visitId());
        return storage.setJson(key, dialogVisitor.enclosedJsonObject());
    }

    public static writeDialogRedirection(userId: string, tenantId: string, stateId: string, actionId: string,
                                         dialogRedirectionVistor: DialogRedirectionVisitor): Promise<void>
    {
        const key = this.constructRedirectionStorageKey(userId, tenantId, stateId, actionId);
        return storage.setJson(key, dialogRedirectionVistor.enclosedJsonObject());
    }

    public static async writePropertyCommit(userId: string, tenantId: string, dialogId: string, propertyName: string, writeLargePropertyParametersVisitor: WriteLargePropertyParametersVisitor): Promise<void> {
        let key = this.PROPERTY_COMMIT_STORAGE_KEY.replace('${userId}', userId);
        key = key.replace('${tenantId}', tenantId);
        key = key.replace('${dialogId}', dialogId);
        key = key.replace('${propertyName}', propertyName);
        let writeHistory = [];
        if (writeLargePropertyParametersVisitor.visitAppend()) {
            const jsonObject = await storage.getJson(key);
            if (jsonObject) {
                writeHistory = jsonObject;
            }
        }
        writeHistory.push(writeLargePropertyParametersVisitor.enclosedJsonObject());
        return storage.setJson(key, writeHistory);
    }

    public static writeRecord(userId: string, tenantId: string, dialogId: string, recordVisitor: RecordVisitor): Promise<void> {
        let key = this.RECORD_STORAGE_KEY.replace('${userId}', userId);
        key = key.replace('${tenantId}', tenantId);
        key = key.replace('${dialogId}', dialogId);
        return storage.setJson(key, recordVisitor.enclosedJsonObject());
    }

    public static writeRecordCommit(userId: string, tenantId: string, dialogId: string, recordVisitor: RecordVisitor): Promise<void> {
        let key = this.RECORD_COMMIT_STORAGE_KEY.replace('${userId}', userId);
        key = key.replace('${tenantId}', tenantId);
        key = key.replace('${dialogId}', dialogId);
        return storage.setJson(key, recordVisitor.enclosedJsonObject());
    }

    public static writeRecordSet(userId: string, tenantId: string, dialogId: string, recordSetVisitor: RecordSetVisitor): Promise<void> {
        let key = this.RECORD_SET_STORAGE_KEY.replace('${userId}', userId);
        key = key.replace('${tenantId}', tenantId);
        key = key.replace('${dialogId}', dialogId);
        return storage.setJson(key, recordSetVisitor.enclosedJsonObject());
    }

}
