<?php

declare(strict_types=1);

namespace App\Framework\Discovery\Enhanced\ValueObjects;

/**
 * Dependency Graph representation
 *
 * Represents the complete dependency graph of discovered components.
 */
final readonly class DependencyGraph
{
    public function __construct(
        private array $nodes,
        private array $edges
    ) {
    }

    /**
     * Get all nodes in the graph
     */
    public function getNodes(): array
    {
        return $this->nodes;
    }

    /**
     * Get all edges in the graph
     */
    public function getEdges(): array
    {
        return $this->edges;
    }

    /**
     * Get node by ID
     */
    public function getNode(string $nodeId): ?DependencyNode
    {
        foreach ($this->nodes as $node) {
            if ($node->id === $nodeId) {
                return $node;
            }
        }

        return null;
    }

    /**
     * Get edges originating from a specific node
     */
    public function getEdgesFrom(string $nodeId): array
    {
        return array_filter($this->edges, fn ($edge) => $edge->from === $nodeId);
    }

    /**
     * Get edges pointing to a specific node
     */
    public function getEdgesTo(string $nodeId): array
    {
        return array_filter($this->edges, fn ($edge) => $edge->to === $nodeId);
    }

    /**
     * Get direct dependencies of a node
     */
    public function getDependencies(string $nodeId): array
    {
        $edges = $this->getEdgesFrom($nodeId);

        return array_map(fn ($edge) => $edge->to, $edges);
    }

    /**
     * Get nodes that depend on the given node
     */
    public function getDependents(string $nodeId): array
    {
        $edges = $this->getEdgesTo($nodeId);

        return array_map(fn ($edge) => $edge->from, $edges);
    }

    /**
     * Check if there's a path between two nodes
     */
    public function hasPath(string $from, string $to): bool
    {
        if ($from === $to) {
            return true;
        }

        $visited = [];

        return $this->dfsHasPath($from, $to, $visited);
    }

    /**
     * Get all paths between two nodes
     */
    public function getAllPaths(string $from, string $to): array
    {
        $paths = [];
        $visited = [];
        $currentPath = [];

        $this->dfsAllPaths($from, $to, $visited, $currentPath, $paths);

        return $paths;
    }

    /**
     * Get shortest path between two nodes
     */
    public function getShortestPath(string $from, string $to): ?array
    {
        if ($from === $to) {
            return [$from];
        }

        $queue = [[$from]];
        $visited = [$from => true];

        while (! empty($queue)) {
            $path = array_shift($queue);
            $node = end($path);

            foreach ($this->getDependencies($node) as $neighbor) {
                if ($neighbor === $to) {
                    return array_merge($path, [$neighbor]);
                }

                if (! isset($visited[$neighbor])) {
                    $visited[$neighbor] = true;
                    $queue[] = array_merge($path, [$neighbor]);
                }
            }
        }

        return null;
    }

    /**
     * Calculate graph statistics
     */
    public function getStatistics(): array
    {
        $nodeCount = count($this->nodes);
        $edgeCount = count($this->edges);

        // Calculate in-degree and out-degree distributions
        $inDegrees = [];
        $outDegrees = [];

        foreach ($this->nodes as $node) {
            $inDegrees[$node->id] = count($this->getEdgesTo($node->id));
            $outDegrees[$node->id] = count($this->getEdgesFrom($node->id));
        }

        return [
            'node_count' => $nodeCount,
            'edge_count' => $edgeCount,
            'density' => $nodeCount > 1 ? $edgeCount / ($nodeCount * ($nodeCount - 1)) : 0,
            'average_in_degree' => $nodeCount > 0 ? array_sum($inDegrees) / $nodeCount : 0,
            'average_out_degree' => $nodeCount > 0 ? array_sum($outDegrees) / $nodeCount : 0,
            'max_in_degree' => ! empty($inDegrees) ? max($inDegrees) : 0,
            'max_out_degree' => ! empty($outDegrees) ? max($outDegrees) : 0,
            'isolated_nodes' => $this->countIsolatedNodes(),
            'strongly_connected' => $this->isStronglyConnected(),
        ];
    }

    /**
     * Get graph hash for caching
     */
    public function getHash(): string
    {
        return md5(serialize([
            'nodes' => array_map(fn ($node) => $node->toArray(), $this->nodes),
            'edges' => array_map(fn ($edge) => $edge->toArray(), $this->edges),
        ]));
    }

    /**
     * Convert to array representation
     */
    public function toArray(): array
    {
        return [
            'nodes' => array_map(fn ($node) => $node->toArray(), $this->nodes),
            'edges' => array_map(fn ($edge) => $edge->toArray(), $this->edges),
            'statistics' => $this->getStatistics(),
        ];
    }

    /**
     * Create subgraph containing only specified nodes
     */
    public function createSubgraph(array $nodeIds): self
    {
        $subNodes = array_filter($this->nodes, fn ($node) => in_array($node->id, $nodeIds));
        $subEdges = array_filter(
            $this->edges,
            fn ($edge) =>
            in_array($edge->from, $nodeIds) && in_array($edge->to, $nodeIds)
        );

        return new self(array_values($subNodes), array_values($subEdges));
    }

    /**
     * Merge with another graph
     */
    public function mergeWith(self $other): self
    {
        $mergedNodes = array_merge($this->nodes, $other->getNodes());
        $mergedEdges = array_merge($this->edges, $other->getEdges());

        // Remove duplicates
        $uniqueNodes = [];
        $uniqueEdges = [];
        $seenNodeIds = [];
        $seenEdgeKeys = [];

        foreach ($mergedNodes as $node) {
            if (! in_array($node->id, $seenNodeIds)) {
                $uniqueNodes[] = $node;
                $seenNodeIds[] = $node->id;
            }
        }

        foreach ($mergedEdges as $edge) {
            $edgeKey = $edge->from . '->' . $edge->to;
            if (! in_array($edgeKey, $seenEdgeKeys)) {
                $uniqueEdges[] = $edge;
                $seenEdgeKeys[] = $edgeKey;
            }
        }

        return new self($uniqueNodes, $uniqueEdges);
    }

    /**
     * DFS helper for path checking
     */
    private function dfsHasPath(string $current, string $target, array &$visited): bool
    {
        if ($current === $target) {
            return true;
        }

        $visited[$current] = true;

        foreach ($this->getDependencies($current) as $neighbor) {
            if (! isset($visited[$neighbor]) && $this->dfsHasPath($neighbor, $target, $visited)) {
                return true;
            }
        }

        return false;
    }

    /**
     * DFS helper for finding all paths
     */
    private function dfsAllPaths(
        string $current,
        string $target,
        array &$visited,
        array &$currentPath,
        array &$paths
    ): void {
        $visited[$current] = true;
        $currentPath[] = $current;

        if ($current === $target) {
            $paths[] = $currentPath;
        } else {
            foreach ($this->getDependencies($current) as $neighbor) {
                if (! isset($visited[$neighbor])) {
                    $this->dfsAllPaths($neighbor, $target, $visited, $currentPath, $paths);
                }
            }
        }

        array_pop($currentPath);
        unset($visited[$current]);
    }

    /**
     * Count isolated nodes (no incoming or outgoing edges)
     */
    private function countIsolatedNodes(): int
    {
        $count = 0;
        foreach ($this->nodes as $node) {
            if (empty($this->getEdgesFrom($node->id)) && empty($this->getEdgesTo($node->id))) {
                $count++;
            }
        }

        return $count;
    }

    /**
     * Check if graph is strongly connected
     */
    private function isStronglyConnected(): bool
    {
        if (count($this->nodes) <= 1) {
            return true;
        }

        // Simplified check - in practice would use proper SCC algorithm
        $firstNode = $this->nodes[0]->id;

        foreach ($this->nodes as $node) {
            if ($node->id !== $firstNode && ! $this->hasPath($firstNode, $node->id)) {
                return false;
            }
        }

        return true;
    }
}
