/**
 * 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 {
  guaranteeNonNullable,
  isNonNullable,
  uniq,
} from '@finos/legend-shared';
import {
  ProjectDependencyConflict,
  ProjectDependencyVersionConflictInfo,
  ProjectDependencyGraph,
  ProjectDependencyGraphReport,
  ProjectDependencyVersionNode,
} from './models/ProjectDependencyGraph.js';
import type { RawProjectDependencyReport } from './models/RawProjectDependencyReport.js';

// max depth is to prevent cycles from breaking tree
const MAX_DEPTH = 100;

const isRootNode = (
  node: ProjectDependencyVersionNode,
  graph: ProjectDependencyGraph,
): boolean => graph.rootNodes.includes(node);

const walkBackWardEdges = (
  node: ProjectDependencyVersionNode,
  graph: ProjectDependencyGraph,
  paths: ProjectDependencyVersionNode[][],
  depth: number,
  maxDepth: number,
): ProjectDependencyVersionNode[][] => {
  if (isRootNode(node, graph) || depth > maxDepth || !node.dependants.length) {
    return [[node]];
  }
  const dependantPaths = node.dependants
    .map((dependant) =>
      walkBackWardEdges(dependant, graph, paths, depth + 1, maxDepth),
    )
    .flat();
  dependantPaths.forEach((r) => r.push(node));
  return dependantPaths;
};

// Here we are rebuilding all the paths to said project dependency version which are causing conflicts
export const buildConflictsPaths = (
  report: ProjectDependencyGraphReport,
): Map<ProjectDependencyConflict, ProjectDependencyVersionConflictInfo[]> => {
  const result: Map<
    ProjectDependencyConflict,
    ProjectDependencyVersionConflictInfo[]
  > = new Map();
  report.conflicts.forEach((conflict) => {
    const versionReportPath: ProjectDependencyVersionConflictInfo[] = [];
    conflict.versions.forEach((conflictNode) => {
      const conflictVersionPath = new ProjectDependencyVersionConflictInfo(
        conflict,
        conflictNode,
      );
      conflictVersionPath.pathsToVersion = walkBackWardEdges(
        conflictNode,
        report.graph,
        [],
        0,
        MAX_DEPTH,
      );
      versionReportPath.push(conflictVersionPath);
    });
    result.set(conflict, versionReportPath);
  });
  return result;
};

export const buildDependencyReport = (
  rawReport: RawProjectDependencyReport,
): ProjectDependencyGraphReport => {
  // process graph
  const graph = new ProjectDependencyGraph();
  const allNodes = Array.from(rawReport.graph.nodes.values());
  allNodes.forEach((node) => {
    graph.nodes.set(
      node.id,
      new ProjectDependencyVersionNode(
        node.projectId,
        node.groupId,
        node.artifactId,
        node.versionId,
        node.id,
      ),
    );
  });
  graph.rootNodes = rawReport.graph.rootNodes
    .map((rootNodeId) => graph.nodes.get(rootNodeId))
    .filter(isNonNullable);
  // process forward/back
  allNodes.forEach((n) => {
    const node = guaranteeNonNullable(graph.nodes.get(n.id));
    node.dependencies = n.forwardEdges
      .map((nodeId) => graph.nodes.get(nodeId))
      .filter(isNonNullable);
    node.dependants = n.backEdges
      .map((nodeId) => graph.nodes.get(nodeId))
      .filter(isNonNullable);
  });
  const _report = new ProjectDependencyGraphReport(graph);
  // conflicts
  _report.conflicts = rawReport.conflicts.map((_conflict) => {
    const conflict = new ProjectDependencyConflict(
      _conflict.groupId,
      _conflict.artifactId,
    );
    conflict.versions = uniq(_conflict.versions)
      .sort()
      .map((v) => guaranteeNonNullable(graph.nodes.get(v)));
    return conflict;
  });
  return _report;
};
