/**
 * 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 { flow, observable, makeObservable, flowResult, action, computed, } from 'mobx';
import { ActionState, assertErrorThrown, LogEvent, isNonNullable, guaranteeNonNullable, uuid, } from '@finos/legend-shared';
import { EngineError } from '@finos/legend-graph';
import { ProjectDependencyCoordinates, buildConflictsPaths, buildDependencyReport, RawProjectDependencyReport, } from '@finos/legend-server-depot';
import { LEGEND_STUDIO_APP_EVENT } from '../../../../__lib__/LegendStudioEvent.js';
import { ProjectDependencyExclusion, } from '@finos/legend-server-sdlc';
import { generateGAVCoordinates, } from '@finos/legend-storage';
export class ProjectDependencyConflictTreeNodeData {
    id;
    label;
    childrenIds;
    isSelected;
    isOpen;
    constructor(id) {
        this.id = id;
        this.label = id;
    }
}
export class ConflictTreeNodeData extends ProjectDependencyConflictTreeNodeData {
    conflict;
    constructor(conflict) {
        super(`${conflict.groupId}:${conflict.artifactId}`);
        this.conflict = conflict;
    }
    get description() {
        return this.id;
    }
}
export class ConflictVersionNodeData extends ProjectDependencyConflictTreeNodeData {
    versionConflict;
    constructor(conflict) {
        super(`${conflict.version.groupId}:${conflict.version.artifactId}.${conflict.version.versionId}`);
        this.versionConflict = conflict;
        this.label = this.versionConflict.version.versionId;
    }
    get description() {
        return this.id;
    }
}
export class ProjectDependencyTreeNodeData extends ProjectDependencyConflictTreeNodeData {
    value;
    constructor(id, value) {
        super(id);
        this.value = value;
        this.label = value.artifactId;
    }
    get description() {
        return `${this.value.groupId}:${this.value.artifactId}:${this.value.versionId}`;
    }
}
export const buildDependencyNodeChildren = (parentNode, treeNodes) => {
    if (!parentNode.childrenIds) {
        const value = parentNode.value;
        const childrenNodes = value.dependencies.map((projectVersion) => {
            const childId = `${parentNode.id}.${projectVersion.id}`;
            const childNode = new ProjectDependencyTreeNodeData(childId, projectVersion);
            treeNodes.set(childId, childNode);
            return childNode;
        });
        parentNode.childrenIds = childrenNodes.map((c) => c.id);
    }
};
const findRootNode = (versionNode, treeData) => {
    if (!treeData.rootIds.includes(versionNode.id)) {
        return undefined;
    }
    return Array.from(treeData.nodes.values()).find((node) => node.id === versionNode.id && node.value === versionNode);
};
const walkNode = (node, visited, treeData) => {
    if (!visited.has(node.value)) {
        node.isOpen = true;
        buildDependencyNodeChildren(node, treeData.nodes);
        visited.add(node.value);
        node.childrenIds
            ?.map((nodeId) => treeData.nodes.get(nodeId))
            .filter(isNonNullable)
            .forEach((n) => walkNode(n, visited, treeData));
    }
    else {
        buildDependencyNodeChildren(node, treeData.nodes);
    }
};
export const openAllDependencyNodesInTree = (treeData, graph) => {
    const visited = new Set();
    graph.rootNodes
        .map((node) => findRootNode(node, treeData))
        .filter(isNonNullable)
        .forEach((node) => walkNode(node, visited, treeData));
};
const buildDependencyTreeData = (report) => {
    const nodes = new Map();
    const rootNodes = report.graph.rootNodes.map((versionNode) => {
        const node = new ProjectDependencyTreeNodeData(versionNode.id, versionNode);
        nodes.set(node.id, node);
        buildDependencyNodeChildren(node, nodes);
        return node;
    });
    const rootIds = rootNodes.map((node) => node.id);
    return { rootIds, nodes };
};
const buildFlattenDependencyTreeData = (report) => {
    const nodes = new Map();
    const rootIds = [];
    Array.from(report.graph.nodes.entries()).forEach(([key, value]) => {
        const id = value.id;
        const node = new ProjectDependencyTreeNodeData(id, value);
        nodes.set(id, node);
        rootIds.push(id);
        buildDependencyNodeChildren(node, nodes);
    });
    return { rootIds, nodes };
};
export var DEPENDENCY_REPORT_TAB;
(function (DEPENDENCY_REPORT_TAB) {
    DEPENDENCY_REPORT_TAB["EXPLORER"] = "EXPLORER";
    DEPENDENCY_REPORT_TAB["CONFLICTS"] = "CONFLICTS";
    DEPENDENCY_REPORT_TAB["RESOLUTION"] = "RESOLUTION";
})(DEPENDENCY_REPORT_TAB || (DEPENDENCY_REPORT_TAB = {}));
const buildTreeDataFromConflictVersion = (conflictVersionNode, nodes) => conflictVersionNode.versionConflict.pathsToVersion
    .map((path, idx) => {
    if (!path.length) {
        return undefined;
    }
    const pathIterator = path.values();
    let rootNode;
    let parentNode;
    let currentVersion;
    while ((currentVersion = pathIterator.next().value)) {
        const id = parentNode
            ? `${parentNode.id}.${currentVersion.id}`
            : `path${idx}_${currentVersion.id}`;
        const node = new ProjectDependencyTreeNodeData(id, currentVersion);
        node.childrenIds = [];
        nodes.set(id, node);
        if (parentNode) {
            parentNode.childrenIds = [node.id];
        }
        else {
            rootNode = node;
        }
        parentNode = node;
    }
    return rootNode;
})
    .filter(isNonNullable);
const buildTreeDataFromConflict = (conflict, paths) => {
    const rootNode = new ConflictTreeNodeData(conflict);
    const rootIds = [rootNode.id];
    const nodes = new Map();
    nodes.set(rootNode.id, rootNode);
    const versionConflictNodes = paths.map((versionConflict) => {
        const projectVersionNode = new ConflictVersionNodeData(versionConflict);
        nodes.set(projectVersionNode.id, projectVersionNode);
        const pathNodes = buildTreeDataFromConflictVersion(projectVersionNode, nodes);
        projectVersionNode.childrenIds = pathNodes.map((n) => n.id);
        return projectVersionNode;
    });
    rootNode.childrenIds = versionConflictNodes.map((n) => n.id);
    return { rootIds, nodes };
};
export class ProjectDependencyConflictState {
    uuid = uuid();
    report;
    conflict;
    paths;
    treeData;
    constructor(report, conflict, paths) {
        makeObservable(this, {
            treeData: observable.ref,
            setTreeData: action,
        });
        this.report = report;
        this.conflict = conflict;
        this.paths = paths;
        this.treeData = buildTreeDataFromConflict(conflict, paths);
    }
    setTreeData(treeData) {
        this.treeData = treeData;
    }
    get versionNodes() {
        return this.paths.map((e) => e.version);
    }
}
export class ProjectDependencyEditorState {
    configState;
    editorStore;
    isReadOnly;
    reportTab;
    fetchingDependencyInfoState = ActionState.create();
    dependencyReport;
    dependencyTreeData;
    flattenDependencyTreeData;
    conflictStates;
    expandConflictsState = ActionState.create();
    buildConflictPathState = ActionState.create();
    validatingDependenciesState = ActionState.create();
    resolvingCompatibleDependenciesState = ActionState.create();
    resolutionResult;
    // Exclusions management
    selectedDependencyForExclusions;
    constructor(configState, editorStore) {
        makeObservable(this, {
            dependencyReport: observable,
            fetchingDependencyInfoState: observable,
            dependencyTreeData: observable.ref,
            flattenDependencyTreeData: observable.ref,
            conflictStates: observable,
            reportTab: observable,
            expandConflictsState: observable,
            buildConflictPathState: observable,
            validatingDependenciesState: observable,
            resolvingCompatibleDependenciesState: observable,
            resolutionResult: observable,
            selectedDependencyForExclusions: observable,
            hasAnyExclusions: computed,
            hasDependencyChanges: computed,
            setReportTab: action,
            expandAllConflicts: action,
            setFlattenDependencyTreeData: action,
            clearTrees: action,
            setTreeData: action,
            setDependencyTreeData: action,
            buildConflictPaths: action,
            setConflictStates: action,
            setSelectedDependencyForExclusions: action,
            addExclusion: action,
            addExclusionByCoordinate: action,
            removeExclusion: action,
            removeExclusionByCoordinate: action,
            clearExclusions: action,
            getExclusions: action,
            getExclusionCoordinates: action,
            clearResolutionResult: action,
            applyResolvedDependencies: flow,
            fetchDependencyReport: flow,
            validateAndFetchDependencyReport: flow,
            resolveCompatibleDependencies: flow,
        });
        this.configState = configState;
        this.editorStore = editorStore;
        this.isReadOnly = editorStore.isInViewerMode;
    }
    expandAllConflicts() {
        if (this.conflictStates) {
            this.expandConflictsState.inProgress();
            this.conflictStates.forEach((c) => {
                const treeData = c.treeData;
                Array.from(treeData.nodes.values()).forEach((n) => (n.isOpen = true));
            });
            this.conflictStates.forEach((c) => {
                c.setTreeData({ ...c.treeData });
            });
            this.expandConflictsState.complete();
        }
    }
    setTreeData(treeData, flattenView) {
        if (flattenView) {
            this.setFlattenDependencyTreeData(treeData);
        }
        else {
            this.setDependencyTreeData(treeData);
        }
    }
    setReportTab(tab) {
        this.reportTab = tab;
    }
    setDependencyTreeData(tree) {
        this.dependencyTreeData = tree;
    }
    setConflictStates(val) {
        this.conflictStates = val;
    }
    setFlattenDependencyTreeData(tree) {
        this.flattenDependencyTreeData = tree;
    }
    get projectConfiguration() {
        return this.configState.projectConfiguration;
    }
    setSelectedDependencyForExclusions(dependency) {
        this.selectedDependencyForExclusions = dependency;
    }
    findProjectDependency(dependencyId) {
        return this.projectConfiguration?.projectDependencies.find((dep) => dep.projectId === dependencyId);
    }
    addExclusion(dependencyId, exclusion) {
        const projectDependency = this.findProjectDependency(dependencyId);
        if (!projectDependency) {
            return;
        }
        const existingExclusion = this.findExistingExclusion(dependencyId, generateGAVCoordinates(guaranteeNonNullable(exclusion.groupId), guaranteeNonNullable(exclusion.artifactId), undefined));
        if (!existingExclusion) {
            const currentExclusions = projectDependency.exclusions ?? [];
            projectDependency.setExclusions([...currentExclusions, exclusion]);
        }
    }
    addExclusionByCoordinate(dependencyId, exclusionCoordinate) {
        const exclusion = ProjectDependencyExclusion.fromCoordinate(exclusionCoordinate);
        this.addExclusion(dependencyId, exclusion);
    }
    removeExclusion(dependencyId, exclusion) {
        const projectDependency = this.findProjectDependency(dependencyId);
        if (!projectDependency?.exclusions) {
            return;
        }
        const coordinate = generateGAVCoordinates(guaranteeNonNullable(exclusion.groupId), guaranteeNonNullable(exclusion.artifactId), undefined);
        const index = this.findExclusionIndex(dependencyId, coordinate);
        if (index > -1) {
            const updatedExclusions = [...projectDependency.exclusions];
            updatedExclusions.splice(index, 1);
            projectDependency.setExclusions(updatedExclusions);
        }
    }
    removeExclusionByCoordinate(dependencyId, exclusionCoordinate) {
        const projectDependency = this.findProjectDependency(dependencyId);
        if (!projectDependency?.exclusions) {
            return;
        }
        const index = this.findExclusionIndex(dependencyId, exclusionCoordinate);
        if (index > -1) {
            const updatedExclusions = [...projectDependency.exclusions];
            updatedExclusions.splice(index, 1);
            projectDependency.setExclusions(updatedExclusions);
        }
    }
    clearExclusions(dependencyId) {
        if (dependencyId) {
            const projectDependency = this.findProjectDependency(dependencyId);
            if (projectDependency) {
                projectDependency.setExclusions([]);
            }
        }
        else {
            this.projectConfiguration?.projectDependencies.forEach((dep) => {
                dep.setExclusions([]);
            });
        }
    }
    getExclusions(dependencyId) {
        const projectDependency = this.findProjectDependency(dependencyId);
        return projectDependency?.exclusions ?? [];
    }
    get hasAnyExclusions() {
        return (this.projectConfiguration?.projectDependencies.some((dep) => dep.exclusions && dep.exclusions.length > 0) ?? false);
    }
    get hasDependencyChanges() {
        if (!this.configState.originalProjectConfiguration) {
            return false;
        }
        const originalDeps = this.configState.originalProjectConfiguration.projectDependencies;
        const currentDeps = this.configState.currentProjectConfiguration.projectDependencies;
        return (currentDeps.some((currentDep) => !originalDeps.find((origDep) => origDep.hashCode === currentDep.hashCode)) ||
            originalDeps.some((origDep) => !currentDeps.find((currentDep) => currentDep.hashCode === origDep.hashCode)));
    }
    getExclusionCoordinates(dependencyId) {
        const exclusions = this.getExclusions(dependencyId);
        return exclusions.map((e) => generateGAVCoordinates(guaranteeNonNullable(e.groupId), guaranteeNonNullable(e.artifactId), undefined));
    }
    findExistingExclusion(dependencyId, coordinate) {
        const projectDependency = this.findProjectDependency(dependencyId);
        if (!projectDependency?.exclusions) {
            return undefined;
        }
        for (let i = 0; i < projectDependency.exclusions.length; i++) {
            const exclusion = guaranteeNonNullable(projectDependency.exclusions[i]);
            const exclusionCoordinate = generateGAVCoordinates(guaranteeNonNullable(exclusion.groupId), guaranteeNonNullable(exclusion.artifactId), undefined);
            if (exclusionCoordinate === coordinate) {
                return projectDependency.exclusions[i];
            }
        }
        return undefined;
    }
    findExclusionIndex(dependencyId, coordinate) {
        const projectDependency = this.findProjectDependency(dependencyId);
        if (!projectDependency?.exclusions) {
            return -1;
        }
        for (let i = 0; i < projectDependency.exclusions.length; i++) {
            const exclusion = guaranteeNonNullable(projectDependency.exclusions[i]);
            const exclusionCoordinate = generateGAVCoordinates(guaranteeNonNullable(exclusion.groupId), guaranteeNonNullable(exclusion.artifactId), undefined);
            if (exclusionCoordinate === coordinate) {
                return i;
            }
        }
        return -1;
    }
    *fetchDependencyReport() {
        try {
            this.fetchingDependencyInfoState.inProgress();
            this.dependencyReport = undefined;
            this.clearTrees();
            this.setConflictStates(undefined);
            if (this.projectConfiguration?.projectDependencies) {
                const dependencyCoordinates = (yield flowResult(this.editorStore.graphState.buildProjectDependencyCoordinates(this.projectConfiguration.projectDependencies)));
                const dependencyInfoRaw = (yield this.editorStore.depotServerClient.analyzeDependencyTree(dependencyCoordinates.map((e) => ProjectDependencyCoordinates.serialization.toJson(e))));
                const rawdependencyReport = RawProjectDependencyReport.serialization.fromJson(dependencyInfoRaw);
                const report = buildDependencyReport(rawdependencyReport);
                this.dependencyReport = report;
                this.setDependencyTreeData(buildDependencyTreeData(report));
                this.setFlattenDependencyTreeData(buildFlattenDependencyTreeData(report));
            }
            this.fetchingDependencyInfoState.complete();
        }
        catch (error) {
            assertErrorThrown(error);
            this.fetchingDependencyInfoState.fail();
            this.dependencyReport = undefined;
            this.editorStore.applicationStore.logService.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.DEPOT_MANAGER_FAILURE), error);
        }
    }
    *validateAndFetchDependencyReport() {
        try {
            this.validatingDependenciesState.inProgress();
            yield flowResult(this.fetchDependencyReport());
            try {
                const dependencyEntitiesIndex = (yield this.editorStore.graphState.getIndexedDependencyEntities(this.dependencyReport));
                const dependencyEntities = Array.from(dependencyEntitiesIndex.values()).flatMap((entitiesWithOrigin) => entitiesWithOrigin.entities);
                const projectElements = this.editorStore.graphManagerState.graph.allOwnElements;
                const projectEntities = projectElements.map((element) => this.editorStore.graphManagerState.graphManager.elementToEntity(element));
                const allEntities = [...dependencyEntities, ...projectEntities];
                yield this.editorStore.graphManagerState.graphManager.compileEntities(allEntities);
                this.validatingDependenciesState.complete();
                this.editorStore.applicationStore.notificationService.notifySuccess('Dependencies validated successfully - no compilation errors');
            }
            catch (error) {
                assertErrorThrown(error);
                this.validatingDependenciesState.fail();
                if (error instanceof EngineError) {
                    const errorLines = error.message
                        .split('\n')
                        .filter((line) => line.trim().length > 0);
                    const errorPreview = errorLines.slice(0, 5).join('; ');
                    const remainingCount = Math.max(0, errorLines.length - 5);
                    const errorMessage = remainingCount > 0
                        ? `Dependencies cause compilation errors: ${errorPreview} and ${remainingCount} more`
                        : `Dependencies cause compilation errors: ${errorPreview}`;
                    this.editorStore.applicationStore.notificationService.notifyError(errorMessage);
                }
                else {
                    this.editorStore.applicationStore.notificationService.notifyError(`Failed to validate dependencies: ${error.message}`);
                }
            }
        }
        catch (error) {
            assertErrorThrown(error);
            this.validatingDependenciesState.fail();
            this.editorStore.applicationStore.notificationService.notifyError(`Failed to validate dependencies: ${error.message}`);
        }
    }
    buildConflictPaths() {
        const report = this.dependencyReport;
        if (report) {
            this.setConflictStates(undefined);
            this.buildConflictPathState.inProgress();
            try {
                report.conflictInfo = buildConflictsPaths(report);
                const conflictStates = Array.from(report.conflictInfo.entries()).map(([conflict, paths]) => new ProjectDependencyConflictState(report, conflict, paths));
                this.setConflictStates(conflictStates);
                this.buildConflictPathState.complete();
            }
            catch (error) {
                assertErrorThrown(error);
                this.setConflictStates([]);
                this.buildConflictPathState.fail();
                this.editorStore.applicationStore.notificationService.notifyError(`Unable to build conflict paths ${error.message}`);
            }
        }
    }
    clearTrees() {
        this.flattenDependencyTreeData = undefined;
        this.dependencyTreeData = undefined;
        this.selectedDependencyForExclusions = undefined;
    }
    clearResolutionResult() {
        this.resolutionResult = undefined;
    }
    *applyResolvedDependencies() {
        if (this.resolutionResult &&
            this.resolutionResult.success &&
            this.projectConfiguration?.projectDependencies) {
            const resolvedDeps = this.resolutionResult.resolvedVersions;
            // Update the configuration
            this.projectConfiguration.projectDependencies.forEach((dep) => {
                const resolved = resolvedDeps.find((r) => r.groupId === dep.groupId && r.artifactId === dep.artifactId);
                if (resolved) {
                    dep.setVersionId(resolved.versionId);
                }
            });
            const configState = this.configState;
            for (const dep of this.projectConfiguration.projectDependencies) {
                if (!configState.versions.has(dep.projectId)) {
                    try {
                        const _versions = (yield this.editorStore.depotServerClient.getVersions(guaranteeNonNullable(dep.groupId), guaranteeNonNullable(dep.artifactId), true));
                        configState.versions.set(dep.projectId, _versions);
                    }
                    catch (error) {
                        assertErrorThrown(error);
                        this.editorStore.applicationStore.logService.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.DEPOT_MANAGER_FAILURE), `Failed to fetch versions for ${dep.projectId}: ${error.message}`);
                    }
                }
            }
            // Refresh the dependency report
            try {
                yield flowResult(this.fetchDependencyReport());
                this.editorStore.applicationStore.notificationService.notifySuccess(`Successfully applied ${resolvedDeps.length} resolved dependencies`);
                this.clearResolutionResult();
            }
            catch (error) {
                assertErrorThrown(error);
                this.editorStore.applicationStore.notificationService.notifyError(`Failed to refresh dependency report: ${error.message}`);
            }
        }
    }
    *resolveCompatibleDependencies(backtrackVersions) {
        this.resolvingCompatibleDependenciesState.inProgress();
        try {
            if (this.projectConfiguration?.projectDependencies) {
                const dependencyCoordinates = (yield flowResult(this.editorStore.graphState.buildProjectDependencyCoordinates(this.projectConfiguration.projectDependencies)));
                const rawResponse = (yield this.editorStore.depotServerClient.resolveCompatibleDependencies(dependencyCoordinates.map((e) => ProjectDependencyCoordinates.serialization.toJson(e)), backtrackVersions));
                const resolvedVersions = rawResponse.resolvedVersions.map((coord) => ProjectDependencyCoordinates.serialization.fromJson(coord));
                const conflicts = rawResponse.conflicts.map((conflict) => {
                    const result = {
                        groupId: conflict.groupId,
                        artifactId: conflict.artifactId,
                        conflictingVersions: conflict.conflictingVersions,
                    };
                    if (conflict.suggestedOverride) {
                        result.suggestedOverride =
                            ProjectDependencyCoordinates.serialization.fromJson(conflict.suggestedOverride);
                    }
                    return result;
                });
                const suggestedOverrides = rawResponse.suggestedOverrides?.map((coord) => ProjectDependencyCoordinates.serialization.fromJson(coord));
                this.resolutionResult = {
                    success: rawResponse.success,
                    resolvedVersions,
                    conflicts,
                    failureReason: rawResponse.failureReason ?? null,
                    ...(suggestedOverrides ? { suggestedOverrides } : {}),
                };
                this.setReportTab(DEPENDENCY_REPORT_TAB.RESOLUTION);
            }
            this.resolvingCompatibleDependenciesState.complete();
        }
        catch (error) {
            assertErrorThrown(error);
            this.resolvingCompatibleDependenciesState.fail();
            this.editorStore.applicationStore.notificationService.notifyError(`Failed to resolve dependencies: ${error.message}`);
        }
    }
}
//# sourceMappingURL=ProjectDependencyEditorState.js.map