# Copyright 2026 Q-CTRL. All rights reserved.
#
# Licensed under the Q-CTRL Terms of service (the "License"). Unauthorized
# copying or use of this file, via any medium, is strictly prohibited.
# Proprietary and confidential. You may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
#    https://q-ctrl.com/terms
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS. See the
# License for the specific language.

from dataclasses import dataclass
from functools import cached_property
from typing import Self

from boulderopalscaleupsdk.runtime.manifest import Manifest, Node


@dataclass
class ManifestGraph:
    nodes: dict[str, Node]
    predecessors: dict[str, list[str]]  # node_id -> [dependency_node_id]

    @cached_property
    def successors(self) -> dict[str, list[str]]:
        ret: dict[str, list[str]] = {nn: [] for nn in self.predecessors}
        for node, parents in self.predecessors.items():
            for pp in parents:
                if pp not in ret:
                    ret[pp] = []
                ret[pp].append(node)
        return ret

    def find_paths(self, src: str, dst: str | None = None) -> list[list[str]]:
        all_paths: list[list[str]] = []
        successors = self.successors

        def dfs(curr_node: str, curr_path: list[str]):
            curr_path.append(curr_node)
            is_target = dst is not None and curr_node == dst
            is_sink = dst is None and not successors.get(curr_node)

            if is_target or is_sink:
                all_paths.append(list(curr_path))
            else:
                for neighbour in successors.get(curr_node, []):
                    dfs(neighbour, curr_path)

            curr_path.pop()

        dfs(src, [])

        return all_paths

    @classmethod
    def from_manifest(cls, manifest: Manifest) -> Self:
        nodes = {}
        predecessors = {}
        for node in manifest.nodes:
            node_id = node.id
            nodes[node_id] = node
            predecessors[node_id] = node.depends_on

        return cls(nodes, predecessors)
