<?php

declare(strict_types=1);

namespace App\Framework\Discovery\ValueObjects;

use App\Framework\Core\ValueObjects\ClassName;

final readonly class DependencyGraph
{
    /** @var array<string, DependencyNode> */
    private array $nodes;

    /** @var array<DependencyEdge> */
    private array $edges;

    /**
     * @param array<string, DependencyNode> $nodes
     * @param array<DependencyEdge> $edges
     */
    public function __construct(array $nodes = [], array $edges = [])
    {
        $this->nodes = $nodes;
        $this->edges = $edges;
    }

    /**
     * Create an empty dependency graph
     */
    public static function empty(): self
    {
        return new self();
    }

    /**
     * Add a node to the graph
     */
    public function addNode(DependencyNode $node): self
    {
        $nodes = $this->nodes;
        $nodes[$node->getClassName()->toString()] = $node;

        return new self($nodes, $this->edges);
    }

    /**
     * Add an edge to the graph
     */
    public function addEdge(DependencyEdge $edge): self
    {
        // Ensure both nodes exist first
        $graph = $this->ensureNodesExist($edge->getSource(), $edge->getTarget());

        $edges = $graph->edges;
        $edges[] = $edge;

        return new self($graph->nodes, $edges);
    }

    /**
     * Get a node by class name
     */
    public function getNode(ClassName $className): ?DependencyNode
    {
        return $this->nodes[$className->toString()] ?? null;
    }

    /**
     * Check if a node exists
     */
    public function hasNode(ClassName $className): bool
    {
        return isset($this->nodes[$className->toString()]);
    }

    /**
     * Get all nodes
     *
     * @return array<string, DependencyNode>
     */
    public function getNodes(): array
    {
        return $this->nodes;
    }

    /**
     * Get all edges
     *
     * @return array<DependencyEdge>
     */
    public function getEdges(): array
    {
        return $this->edges;
    }

    /**
     * Get the number of nodes
     */
    public function getNodeCount(): int
    {
        return count($this->nodes);
    }

    /**
     * Get the number of edges
     */
    public function getEdgeCount(): int
    {
        return count($this->edges);
    }

    /**
     * Find circular dependencies in the graph
     *
     * @return array<array<string>>
     */
    public function findCircularDependencies(): array
    {
        $visited = [];
        $recursionStack = [];
        $cycles = [];

        foreach ($this->nodes as $node) {
            $className = $node->getClassName()->toString();
            if (! isset($visited[$className])) {
                $this->dfsForCycles($node, $visited, $recursionStack, $cycles, []);
            }
        }

        return $cycles;
    }

    /**
     * Get nodes with highest dependency count
     *
     * @return array<DependencyNode>
     */
    public function getHighestDependencyNodes(int $limit = 10): array
    {
        $nodes = array_values($this->nodes);
        usort($nodes, fn ($a, $b) => $b->getDependencyCount() <=> $a->getDependencyCount());

        return array_slice($nodes, 0, $limit);
    }

    /**
     * Get nodes with highest dependent count (most used)
     *
     * @return array<DependencyNode>
     */
    public function getMostUsedNodes(int $limit = 10): array
    {
        $nodes = array_values($this->nodes);
        usort($nodes, fn ($a, $b) => $b->getDependentCount() <=> $a->getDependentCount());

        return array_slice($nodes, 0, $limit);
    }

    /**
     * Get leaf nodes (no dependencies)
     *
     * @return array<DependencyNode>
     */
    public function getLeafNodes(): array
    {
        return array_filter($this->nodes, fn (DependencyNode $node) => $node->isLeaf());
    }

    /**
     * Get root nodes (no dependents)
     *
     * @return array<DependencyNode>
     */
    public function getRootNodes(): array
    {
        return array_filter($this->nodes, fn (DependencyNode $node) => $node->isRoot());
    }

    /**
     * Get nodes by type
     *
     * @return array<DependencyNode>
     */
    public function getNodesByType(DependencyType $type): array
    {
        return array_filter(
            $this->nodes,
            fn (DependencyNode $node) => $node->getType() === $type
        );
    }

    /**
     * Calculate dependency depth for a node
     */
    public function getDependencyDepth(ClassName $className): int
    {
        $node = $this->getNode($className);
        if ($node === null) {
            return 0;
        }

        $visited = [];

        return $this->calculateDepth($node, $visited);
    }

    /**
     * Get dependency path from one node to another
     *
     * @return array<string>|null
     */
    public function getDependencyPath(ClassName $from, ClassName $to): ?array
    {
        $fromNode = $this->getNode($from);
        $toNode = $this->getNode($to);

        if ($fromNode === null || $toNode === null) {
            return null;
        }

        $visited = [];
        $path = [];

        if ($this->findPath($fromNode, $toNode, $visited, $path)) {
            return $path;
        }

        return null;
    }

    /**
     * Get graph statistics
     *
     * @return array<string, mixed>
     */
    public function getStatistics(): array
    {
        $totalComplexity = 0;
        $typeDistribution = [];
        $circularDependencies = $this->findCircularDependencies();

        foreach ($this->nodes as $node) {
            $totalComplexity += $node->getComplexityScore();
            $type = $node->getType()->value;
            $typeDistribution[$type] = ($typeDistribution[$type] ?? 0) + 1;
        }

        $avgComplexity = count($this->nodes) > 0 ? $totalComplexity / count($this->nodes) : 0;

        return [
            'node_count' => count($this->nodes),
            'edge_count' => count($this->edges),
            'circular_dependencies' => count($circularDependencies),
            'leaf_nodes' => count($this->getLeafNodes()),
            'root_nodes' => count($this->getRootNodes()),
            'average_complexity' => round($avgComplexity, 2),
            'total_complexity' => round($totalComplexity, 2),
            'type_distribution' => $typeDistribution,
            'max_dependency_count' => $this->getMaxDependencyCount(),
            'max_dependent_count' => $this->getMaxDependentCount(),
        ];
    }

    /**
     * Convert to array for serialization
     *
     * @return array<string, mixed>
     */
    public function toArray(): array
    {
        return [
            'statistics' => $this->getStatistics(),
            'nodes' => array_map(
                fn (DependencyNode $node) => $node->toArray(),
                array_values($this->nodes)
            ),
            'edges' => array_map(
                fn (DependencyEdge $edge) => $edge->toArray(),
                $this->edges
            ),
            'circular_dependencies' => $this->findCircularDependencies(),
        ];
    }

    /**
     * Ensure both source and target nodes exist in the graph
     */
    private function ensureNodesExist(ClassName $source, ClassName $target): self
    {
        $graph = $this;

        if (! $this->hasNode($source)) {
            $sourceType = DependencyType::fromClassName($source->toString());
            $sourceNode = DependencyNode::create($source, $sourceType);
            $graph = $graph->addNode($sourceNode);
        }

        if (! $this->hasNode($target)) {
            $targetType = DependencyType::fromClassName($target->toString());
            $targetNode = DependencyNode::create($target, $targetType);
            $graph = $graph->addNode($targetNode);
        }

        return $graph;
    }

    /**
     * DFS algorithm for cycle detection
     *
     * @param array<string, bool> $visited
     * @param array<string, bool> $recursionStack
     * @param array<array<string>> $cycles
     * @param array<string> $currentPath
     */
    private function dfsForCycles(
        DependencyNode $node,
        array &$visited,
        array &$recursionStack,
        array &$cycles,
        array $currentPath
    ): void {
        $className = $node->getClassName()->toString();
        $visited[$className] = true;
        $recursionStack[$className] = true;
        $currentPath[] = $className;

        foreach ($node->getDependencies() as $edge) {
            $targetClassName = $edge->getTarget()->toString();

            if (isset($recursionStack[$targetClassName]) && $recursionStack[$targetClassName]) {
                // Found a cycle
                $cycleStart = array_search($targetClassName, $currentPath);
                if ($cycleStart !== false) {
                    $cycles[] = array_slice($currentPath, $cycleStart);
                }
            } elseif (! isset($visited[$targetClassName])) {
                $targetNode = $this->getNode($edge->getTarget());
                if ($targetNode !== null) {
                    $this->dfsForCycles($targetNode, $visited, $recursionStack, $cycles, $currentPath);
                }
            }
        }

        $recursionStack[$className] = false;
        array_pop($currentPath);
    }

    /**
     * Calculate dependency depth recursively
     *
     * @param array<string, bool> $visited
     */
    private function calculateDepth(DependencyNode $node, array &$visited): int
    {
        $className = $node->getClassName()->toString();

        if (isset($visited[$className])) {
            return 0; // Avoid infinite recursion
        }

        $visited[$className] = true;
        $maxDepth = 0;

        foreach ($node->getDependencies() as $edge) {
            $dependencyNode = $this->getNode($edge->getTarget());
            if ($dependencyNode !== null) {
                $depth = 1 + $this->calculateDepth($dependencyNode, $visited);
                $maxDepth = max($maxDepth, $depth);
            }
        }

        unset($visited[$className]);

        return $maxDepth;
    }

    /**
     * Find path between two nodes
     *
     * @param array<string, bool> $visited
     * @param array<string> $path
     */
    private function findPath(
        DependencyNode $from,
        DependencyNode $to,
        array &$visited,
        array &$path
    ): bool {
        $fromClassName = $from->getClassName()->toString();
        $toClassName = $to->getClassName()->toString();

        $visited[$fromClassName] = true;
        $path[] = $fromClassName;

        if ($fromClassName === $toClassName) {
            return true;
        }

        foreach ($from->getDependencies() as $edge) {
            $nextClassName = $edge->getTarget()->toString();
            if (! isset($visited[$nextClassName])) {
                $nextNode = $this->getNode($edge->getTarget());
                if ($nextNode !== null && $this->findPath($nextNode, $to, $visited, $path)) {
                    return true;
                }
            }
        }

        array_pop($path);

        return false;
    }

    /**
     * Get maximum dependency count among all nodes
     */
    private function getMaxDependencyCount(): int
    {
        $max = 0;
        foreach ($this->nodes as $node) {
            $max = max($max, $node->getDependencyCount());
        }

        return $max;
    }

    /**
     * Get maximum dependent count among all nodes
     */
    private function getMaxDependentCount(): int
    {
        $max = 0;
        foreach ($this->nodes as $node) {
            $max = max($max, $node->getDependentCount());
        }

        return $max;
    }
}
