/**
 * Copyright (c) 2020-present, Goldman Sachs
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { getMappingElementSource, getMappingElementTarget, generateMappingTestName, } from './MappingEditorState.js';
import { observable, action, flow, computed, makeObservable, flowResult, } from 'mobx';
import { assertErrorThrown, LogEvent, guaranteeNonNullable, assertTrue, IllegalStateError, UnsupportedOperationError, uuid, tryToMinifyJSONString, toGrammarString, isValidJSONString, createUrlStringFromData, stringifyLosslessJSON, guaranteeType, ContentType, generateEnumerableNameFromToken, tryToFormatLosslessJSONString, StopWatch, } from '@finos/legend-shared';
import { createMockDataForMappingElementSource } from '../../../utils/MockDataUtils.js';
import { DEFAULT_TEST_ASSERTION_PREFIX, DEFAULT_TEST_PREFIX, EqualToJson, ServiceTest, LAMBDA_PIPE, GRAPH_MANAGER_EVENT, Class, DEPRECATED__ObjectInputData, ObjectInputType, DEPRECATED__ExpectedOutputMappingTestAssert, IdentifiedConnection, EngineRuntime, JsonModelConnection, FlatDataConnection, FlatDataInputData, Service, PureSingleExecution, RootFlatDataRecordType, PackageableElementExplicitReference, DatabaseType, RelationalDatabaseConnection, LocalH2DatasourceSpecification, DefaultH2AuthenticationStrategy, RelationalInputData, RelationalInputType, OperationSetImplementation, buildSourceInformationSourceId, TableAlias, stub_RawLambda, isStubbed_RawLambda, generateIdentifiedConnectionId, ServiceTestSuite, TestData, ConnectionTestData, DEFAULT_TEST_SUITE_PREFIX, DEPRECATED__MappingTest, ModelStore, reportGraphAnalytics, } from '@finos/legend-graph';
import { ActionAlertActionType, ActionAlertType, DEFAULT_TAB_SIZE, } from '@finos/legend-application';
import { objectInputData_setData, runtime_addIdentifiedConnection, runtime_addMapping, } from '../../../../graph-modifier/DSL_Mapping_GraphModifierHelper.js';
import { flatData_setData } from '../../../../graph-modifier/STO_FlatData_GraphModifierHelper.js';
import { service_addTestSuite, service_initNewService, service_setExecution, } from '../../../../graph-modifier/DSL_Service_GraphModifierHelper.js';
import { localH2DatasourceSpecification_setTestDataSetupCsv, localH2DatasourceSpecification_setTestDataSetupSqls, relationalInputData_setData, } from '../../../../graph-modifier/STO_Relational_GraphModifierHelper.js';
import { createEmptyEqualToJsonAssertion, createBareExternalFormat, } from '../../../utils/TestableUtils.js';
import { SERIALIZATION_FORMAT } from '../service/testable/ServiceTestEditorState.js';
import { LambdaEditorState, QueryBuilderTelemetryHelper, QUERY_BUILDER_EVENT, ExecutionPlanState, } from '@finos/legend-query-builder';
import { MappingEditorTabState } from './MappingTabManagerState.js';
export class MappingExecutionQueryState extends LambdaEditorState {
    editorStore;
    isInitializingLambda = false;
    query;
    constructor(editorStore, query) {
        super('', LAMBDA_PIPE);
        makeObservable(this, {
            query: observable,
            isInitializingLambda: observable,
            setIsInitializingLambda: action,
            updateLamba: flow,
        });
        this.editorStore = editorStore;
        this.query = query;
    }
    get lambdaId() {
        return buildSourceInformationSourceId([this.uuid]);
    }
    setIsInitializingLambda(val) {
        this.isInitializingLambda = val;
    }
    *updateLamba(val) {
        this.query = val;
        yield flowResult(this.convertLambdaObjectToGrammarString({ pretty: true }));
    }
    *convertLambdaObjectToGrammarString(options) {
        if (!isStubbed_RawLambda(this.query)) {
            try {
                const lambdas = new Map();
                lambdas.set(this.lambdaId, this.query);
                const isolatedLambdas = (yield this.editorStore.graphManagerState.graphManager.lambdasToPureCode(lambdas, options?.pretty));
                const grammarText = isolatedLambdas.get(this.lambdaId);
                this.setLambdaString(grammarText !== undefined
                    ? this.extractLambdaString(grammarText)
                    : '');
                this.clearErrors();
            }
            catch (error) {
                assertErrorThrown(error);
                this.editorStore.applicationStore.logService.error(LogEvent.create(GRAPH_MANAGER_EVENT.PARSING_FAILURE), error);
            }
        }
        else {
            this.clearErrors();
            this.setLambdaString('');
        }
    }
    // NOTE: since we don't allow edition in text mode, we don't need to implement this
    *convertLambdaGrammarStringToObject() {
        throw new UnsupportedOperationError();
    }
}
class MappingExecutionInputDataState {
    uuid = uuid();
    editorStore;
    mapping;
    inputData;
    constructor(editorStore, mapping, inputData) {
        this.editorStore = editorStore;
        this.mapping = mapping;
        this.inputData = inputData;
    }
    createEmbeddedData() {
        return undefined;
    }
    createAssertion(executionResult) {
        return undefined;
    }
}
export const createRuntimeForExecution = (mapping, connection, editorStore) => {
    const runtime = new EngineRuntime();
    runtime_addMapping(runtime, PackageableElementExplicitReference.create(mapping));
    runtime_addIdentifiedConnection(runtime, new IdentifiedConnection(generateIdentifiedConnectionId(runtime), connection), editorStore.changeDetectionState.observerContext);
    return runtime;
};
export class MappingExecutionEmptyInputDataState extends MappingExecutionInputDataState {
    get isValid() {
        return false;
    }
    get runtime() {
        throw new IllegalStateError('Mapping execution runtime information is not specified');
    }
    buildInputDataForTest() {
        throw new IllegalStateError('Mapping execution runtime information is not specified');
    }
}
// TODO?: handle XML
export class MappingExecutionObjectInputDataState extends MappingExecutionInputDataState {
    constructor(editorStore, mapping, _class) {
        super(editorStore, mapping, new DEPRECATED__ObjectInputData(PackageableElementExplicitReference.create(guaranteeNonNullable(_class)), ObjectInputType.JSON, tryToMinifyJSONString('{}')));
        makeObservable(this, {
            isValid: computed,
        });
    }
    get isValid() {
        return isValidJSONString(this.inputData.data);
    }
    get runtime() {
        assertTrue(this.isValid, 'Model-to-model mapping execution test data is not a valid JSON string');
        const engineConfig = this.editorStore.graphManagerState.graphManager.TEMPORARY__getEngineConfig();
        return createRuntimeForExecution(this.mapping, new JsonModelConnection(PackageableElementExplicitReference.create(ModelStore.INSTANCE), PackageableElementExplicitReference.create(guaranteeNonNullable(this.inputData.sourceClass.value)), createUrlStringFromData(tryToMinifyJSONString(this.inputData.data), ContentType.APPLICATION_JSON, engineConfig.useBase64ForAdhocConnectionDataUrls)), this.editorStore);
    }
    createEmbeddedData() {
        const embeddedData = createBareExternalFormat();
        embeddedData.data = tryToFormatLosslessJSONString(tryToMinifyJSONString(this.inputData.data));
        return embeddedData;
    }
    createAssertion(executionResult) {
        const jsonAssertion = new EqualToJson();
        jsonAssertion.id = generateEnumerableNameFromToken([], DEFAULT_TEST_ASSERTION_PREFIX);
        const expected = createBareExternalFormat();
        expected.data = toGrammarString(executionResult);
        jsonAssertion.expected = expected;
        return jsonAssertion;
    }
    buildInputDataForTest() {
        return new DEPRECATED__ObjectInputData(PackageableElementExplicitReference.create(guaranteeNonNullable(this.inputData.sourceClass.value)), this.inputData.inputType, tryToMinifyJSONString(this.inputData.data));
    }
}
export class MappingExecutionFlatDataInputDataState extends MappingExecutionInputDataState {
    constructor(editorStore, mapping, rootFlatDataRecordType) {
        super(editorStore, mapping, new FlatDataInputData(PackageableElementExplicitReference.create(guaranteeNonNullable(rootFlatDataRecordType._OWNER._OWNER)), ''));
        makeObservable(this, {
            isValid: computed,
        });
    }
    get isValid() {
        return true;
    }
    get runtime() {
        const engineConfig = this.editorStore.graphManagerState.graphManager.TEMPORARY__getEngineConfig();
        return createRuntimeForExecution(this.mapping, new FlatDataConnection(PackageableElementExplicitReference.create(guaranteeNonNullable(this.inputData.sourceFlatData.value)), createUrlStringFromData(this.inputData.data, ContentType.TEXT_PLAIN, engineConfig.useBase64ForAdhocConnectionDataUrls)), this.editorStore);
    }
    buildInputDataForTest() {
        return new FlatDataInputData(PackageableElementExplicitReference.create(guaranteeNonNullable(this.inputData.sourceFlatData.value)), this.inputData.data);
    }
}
export class MappingExecutionRelationalInputDataState extends MappingExecutionInputDataState {
    constructor(editorStore, mapping, tableOrView) {
        super(editorStore, mapping, new RelationalInputData(PackageableElementExplicitReference.create(guaranteeNonNullable(tableOrView.schema._OWNER)), '', RelationalInputType.SQL));
        makeObservable(this, {
            isValid: computed,
        });
    }
    get isValid() {
        return true;
    }
    get runtime() {
        const datasourceSpecification = new LocalH2DatasourceSpecification();
        switch (this.inputData.inputType) {
            case RelationalInputType.SQL:
                localH2DatasourceSpecification_setTestDataSetupSqls(datasourceSpecification, 
                // NOTE: this is a gross simplification of handling the input for relational input data
                [this.inputData.data]);
                break;
            case RelationalInputType.CSV:
                localH2DatasourceSpecification_setTestDataSetupCsv(datasourceSpecification, this.inputData.data);
                break;
            default:
                throw new UnsupportedOperationError(`Invalid input data type`);
        }
        return createRuntimeForExecution(this.mapping, new RelationalDatabaseConnection(PackageableElementExplicitReference.create(guaranteeNonNullable(this.inputData.database.value)), DatabaseType.H2, datasourceSpecification, new DefaultH2AuthenticationStrategy()), this.editorStore);
    }
    buildInputDataForTest() {
        return new RelationalInputData(PackageableElementExplicitReference.create(guaranteeNonNullable(this.inputData.database.value)), this.inputData.data, this.inputData.inputType);
    }
}
export class MappingExecutionState extends MappingEditorTabState {
    editorStore;
    mappingEditorState;
    name;
    queryState;
    inputDataState;
    showServicePathModal = false;
    executionResultText; // NOTE: stored as lossless JSON text
    isExecuting = false;
    isGeneratingPlan = false;
    executionPlanState;
    planGenerationDebugText;
    executionRunPromise = undefined;
    constructor(editorStore, mappingEditorState, name) {
        super();
        makeObservable(this, {
            name: observable,
            queryState: observable,
            inputDataState: observable,
            showServicePathModal: observable,
            executionPlanState: observable,
            isExecuting: observable,
            isGeneratingPlan: observable,
            planGenerationDebugText: observable,
            executionRunPromise: observable,
            setExecutionRunPromise: action,
            setQueryState: action,
            setInputDataState: action,
            setExecutionResultText: action,
            setShowServicePathModal: action,
            setPlanGenerationDebugText: action,
            setInputDataStateBasedOnSource: action,
            reset: action,
            promoteToTest: flow,
            promoteToService: flow,
            cancelExecution: flow,
            executeMapping: flow,
            generatePlan: flow,
            buildQueryWithClassMapping: flow,
        });
        this.editorStore = editorStore;
        this.mappingEditorState = mappingEditorState;
        this.name = name;
        this.queryState = new MappingExecutionQueryState(editorStore, stub_RawLambda());
        this.inputDataState = new MappingExecutionEmptyInputDataState(editorStore, mappingEditorState.mapping, undefined);
        this.executionPlanState = new ExecutionPlanState(this.editorStore.applicationStore, this.editorStore.graphManagerState);
    }
    get label() {
        return this.name;
    }
    setExecutionRunPromise(promise) {
        this.executionRunPromise = promise;
    }
    setQueryState(val) {
        this.queryState = val;
    }
    setInputDataState(val) {
        this.inputDataState = val;
    }
    setExecutionResultText(val) {
        this.executionResultText = val;
    }
    setShowServicePathModal(val) {
        this.showServicePathModal = val;
    }
    setPlanGenerationDebugText(val) {
        this.planGenerationDebugText = val;
    }
    reset() {
        this.queryState = new MappingExecutionQueryState(this.editorStore, stub_RawLambda());
        this.inputDataState = new MappingExecutionEmptyInputDataState(this.editorStore, this.mappingEditorState.mapping, undefined);
        this.setExecutionResultText(undefined);
    }
    setInputDataStateBasedOnSource(source, populateWithMockData) {
        if (source instanceof Class) {
            const newRuntimeState = new MappingExecutionObjectInputDataState(this.editorStore, this.mappingEditorState.mapping, source);
            if (populateWithMockData) {
                objectInputData_setData(newRuntimeState.inputData, createMockDataForMappingElementSource(source, this.editorStore));
            }
            this.setInputDataState(newRuntimeState);
        }
        else if (source instanceof RootFlatDataRecordType) {
            const newRuntimeState = new MappingExecutionFlatDataInputDataState(this.editorStore, this.mappingEditorState.mapping, source);
            if (populateWithMockData) {
                flatData_setData(newRuntimeState.inputData, createMockDataForMappingElementSource(source, this.editorStore));
            }
            this.setInputDataState(newRuntimeState);
        }
        else if (source instanceof TableAlias) {
            const newRuntimeState = new MappingExecutionRelationalInputDataState(this.editorStore, this.mappingEditorState.mapping, source.relation.value);
            if (populateWithMockData) {
                relationalInputData_setData(newRuntimeState.inputData, createMockDataForMappingElementSource(source, this.editorStore));
            }
            this.setInputDataState(newRuntimeState);
        }
        else if (source === undefined) {
            this.setInputDataState(new MappingExecutionEmptyInputDataState(this.editorStore, this.mappingEditorState.mapping, undefined));
        }
        else {
            this.editorStore.applicationStore.notificationService.notifyWarning(new UnsupportedOperationError(`Can't build input data for the specified source`, source));
        }
    }
    *promoteToTest() {
        try {
            const query = this.queryState.query;
            if (!isStubbed_RawLambda(this.queryState.query) &&
                this.inputDataState.isValid &&
                this.inputDataState.inputData &&
                this.executionResultText) {
                const inputData = this.inputDataState.buildInputDataForTest();
                const assert = new DEPRECATED__ExpectedOutputMappingTestAssert(toGrammarString(this.executionResultText));
                const mappingTest = new DEPRECATED__MappingTest(generateMappingTestName(this.mappingEditorState.mapping), query, [inputData], assert);
                yield flowResult(this.mappingEditorState.addTest(mappingTest));
                this.mappingEditorState.closeTab(this); // after promoting to test, remove the execution state
            }
        }
        catch (error) {
            assertErrorThrown(error);
            this.editorStore.applicationStore.logService.error(LogEvent.create(GRAPH_MANAGER_EVENT.EXECUTION_FAILURE), error);
            this.editorStore.applicationStore.notificationService.notifyError(error);
        }
    }
    *promoteToService(packagePath, serviceName) {
        try {
            const query = this.queryState.query;
            if (!isStubbed_RawLambda(this.queryState.query) &&
                this.inputDataState.isValid &&
                this.executionResultText) {
                if (this.inputDataState instanceof MappingExecutionObjectInputDataState) {
                    const service = new Service(serviceName);
                    const engineRuntime = this.inputDataState.runtime;
                    service_initNewService(service);
                    const pureSingleExecution = new PureSingleExecution(query, service, PackageableElementExplicitReference.create(this.mappingEditorState.mapping), engineRuntime);
                    service_setExecution(service, pureSingleExecution, this.editorStore.changeDetectionState.observerContext);
                    const suite = new ServiceTestSuite();
                    suite.id = generateEnumerableNameFromToken([], DEFAULT_TEST_SUITE_PREFIX);
                    suite.testData = new TestData();
                    const embeddedData = this.inputDataState.createEmbeddedData();
                    const connection = engineRuntime.connections[0]?.storeConnections[0];
                    if (embeddedData && connection) {
                        const connectionTestData = new ConnectionTestData();
                        connectionTestData.connectionId = connection.id;
                        connectionTestData.testData = embeddedData;
                        suite.testData.connectionsTestData = [connectionTestData];
                    }
                    const test = new ServiceTest();
                    test.serializationFormat = SERIALIZATION_FORMAT.PURE;
                    test.id = generateEnumerableNameFromToken([], DEFAULT_TEST_PREFIX);
                    test.__parent = suite;
                    suite.tests = [test];
                    const assertion = this.inputDataState.createAssertion(this.executionResultText) ??
                        createEmptyEqualToJsonAssertion(test);
                    test.assertions = [assertion];
                    assertion.parentTest = test;
                    service_addTestSuite(service, suite, this.editorStore.changeDetectionState.observerContext);
                    yield flowResult(this.editorStore.graphEditorMode.addElement(service, packagePath, true));
                }
                else {
                    throw new UnsupportedOperationError(`Can't build service from input data state`, this.inputDataState);
                }
            }
        }
        catch (error) {
            assertErrorThrown(error);
            this.editorStore.applicationStore.logService.error(LogEvent.create(GRAPH_MANAGER_EVENT.EXECUTION_FAILURE), error);
            this.editorStore.applicationStore.notificationService.notifyError(error);
        }
    }
    *cancelExecution() {
        this.isExecuting = false;
        this.setExecutionRunPromise(undefined);
        try {
            yield this.editorStore.graphManagerState.graphManager.cancelUserExecutions(true);
        }
        catch (error) {
            // don't notify users about success or failure
            this.editorStore.applicationStore.logService.error(LogEvent.create(GRAPH_MANAGER_EVENT.EXECUTION_FAILURE), error);
        }
    }
    *executeMapping() {
        let promise;
        try {
            const query = this.queryState.query;
            const runtime = this.inputDataState.runtime;
            if (!isStubbed_RawLambda(this.queryState.query) &&
                this.inputDataState.isValid &&
                !this.isExecuting) {
                this.isExecuting = true;
                QueryBuilderTelemetryHelper.logEvent_QueryRunLaunched(this.editorStore.applicationStore.telemetryService);
                const stopWatch = new StopWatch();
                const report = reportGraphAnalytics(this.editorStore.graphManagerState.graph);
                promise = this.editorStore.graphManagerState.graphManager.runQuery(query, this.mappingEditorState.mapping, runtime, this.editorStore.graphManagerState.graph, {
                    useLosslessParse: true,
                }, report);
                this.setExecutionRunPromise(promise);
                const result = (yield promise);
                if (this.executionRunPromise === promise) {
                    this.setExecutionResultText(stringifyLosslessJSON(result.executionResult, undefined, DEFAULT_TAB_SIZE));
                    // report
                    report.timings =
                        this.editorStore.applicationStore.timeService.finalizeTimingsRecord(stopWatch, report.timings);
                    QueryBuilderTelemetryHelper.logEvent_QueryRunSucceeded(this.editorStore.applicationStore.telemetryService, report);
                }
            }
        }
        catch (error) {
            if (this.executionRunPromise === promise) {
                assertErrorThrown(error);
                this.editorStore.applicationStore.logService.error(LogEvent.create(GRAPH_MANAGER_EVENT.EXECUTION_FAILURE), error);
                this.editorStore.applicationStore.notificationService.notifyError(error);
                this.setExecutionResultText('');
            }
        }
        finally {
            this.isExecuting = false;
        }
    }
    *generatePlan(debug) {
        try {
            const query = this.queryState.query;
            const runtime = this.inputDataState.runtime;
            if (!isStubbed_RawLambda(this.queryState.query) &&
                this.inputDataState.isValid &&
                !this.isGeneratingPlan) {
                this.isGeneratingPlan = true;
                let rawPlan;
                const stopWatch = new StopWatch();
                const report = reportGraphAnalytics(this.editorStore.graphManagerState.graph);
                if (debug) {
                    QueryBuilderTelemetryHelper.logEvent_ExecutionPlanDebugLaunched(this.editorStore.applicationStore.telemetryService);
                    const debugResult = (yield this.editorStore.graphManagerState.graphManager.debugExecutionPlanGeneration(query, this.mappingEditorState.mapping, runtime, this.editorStore.graphManagerState.graph, undefined, report));
                    rawPlan = debugResult.plan;
                    this.executionPlanState.setDebugText(debugResult.debug);
                }
                else {
                    QueryBuilderTelemetryHelper.logEvent_ExecutionPlanGenerationLaunched(this.editorStore.applicationStore.telemetryService);
                    rawPlan =
                        (yield this.editorStore.graphManagerState.graphManager.generateExecutionPlan(query, this.mappingEditorState.mapping, runtime, this.editorStore.graphManagerState.graph, undefined, report));
                }
                stopWatch.record();
                try {
                    this.executionPlanState.setRawPlan(rawPlan);
                    const plan = this.editorStore.graphManagerState.graphManager.buildExecutionPlan(rawPlan, this.editorStore.graphManagerState.graph);
                    this.executionPlanState.initialize(plan);
                }
                catch {
                    // do nothing
                }
                stopWatch.record(QUERY_BUILDER_EVENT.BUILD_EXECUTION_PLAN__SUCCESS);
                // report
                report.timings =
                    this.editorStore.applicationStore.timeService.finalizeTimingsRecord(stopWatch, report.timings);
                if (debug) {
                    QueryBuilderTelemetryHelper.logEvent_ExecutionPlanDebugSucceeded(this.editorStore.applicationStore.telemetryService, report);
                }
                else {
                    QueryBuilderTelemetryHelper.logEvent_ExecutionPlanGenerationSucceeded(this.editorStore.applicationStore.telemetryService, report);
                }
            }
        }
        catch (error) {
            assertErrorThrown(error);
            this.editorStore.applicationStore.logService.error(LogEvent.create(GRAPH_MANAGER_EVENT.EXECUTION_FAILURE), error);
            this.editorStore.applicationStore.notificationService.notifyError(error);
        }
        finally {
            this.isGeneratingPlan = false;
        }
    }
    *buildQueryWithClassMapping(setImplementation) {
        // do all the necessary updates
        this.setExecutionResultText(undefined);
        yield flowResult(this.queryState.updateLamba(setImplementation
            ? this.editorStore.graphManagerState.graphManager.createGetAllRawLambda(guaranteeType(getMappingElementTarget(setImplementation), Class))
            : stub_RawLambda()));
        // Attempt to generate data for input data panel as we pick the class mapping:
        // - If the source panel is empty right now, automatically try to generate input data:
        //   - We generate based on the class mapping, if it's concrete
        //   - If the class mapping is operation, output a warning message
        // - If the source panel is non-empty (show modal), show an option to keep current input data
        if (setImplementation) {
            if (this.inputDataState instanceof MappingExecutionEmptyInputDataState) {
                if (setImplementation instanceof OperationSetImplementation) {
                    this.editorStore.applicationStore.notificationService.notifyWarning(`Can't auto-generate input data for operation class mapping. Please pick a concrete class mapping instead`);
                }
                else {
                    this.setInputDataStateBasedOnSource(getMappingElementSource(setImplementation, this.editorStore.pluginManager.getApplicationPlugins()), true);
                }
            }
            else {
                this.editorStore.applicationStore.alertService.setActionAlertInfo({
                    message: 'Mapping execution input data is already set',
                    prompt: 'Do you want to regenerate the input data?',
                    type: ActionAlertType.CAUTION,
                    actions: [
                        {
                            label: 'Regenerate',
                            type: ActionAlertActionType.PROCEED_WITH_CAUTION,
                            handler: () => this.setInputDataStateBasedOnSource(getMappingElementSource(setImplementation, this.editorStore.pluginManager.getApplicationPlugins()), true),
                        },
                        {
                            label: 'Keep my input data',
                            type: ActionAlertActionType.PROCEED,
                            default: true,
                        },
                    ],
                });
            }
        }
    }
}
//# sourceMappingURL=MappingExecutionState.js.map