<?php

declare(strict_types=1);

namespace App\Framework\Database\Migration;

use App\Framework\Database\Exception\DatabaseException;

/**
 * Manages dependencies between migrations and determines the correct execution order.
 */
final class MigrationDependencyGraph
{
    /**
     * @var array<string, array<string>> Adjacency list representation of the dependency graph
     */
    private array $dependencies = [];

    /**
     * @var array<string, Migration> Map of version strings to migration instances
     */
    private array $migrations = [];

    /**
     * Build a dependency graph from a collection of migrations
     */
    public function buildGraph(MigrationCollection $migrations): void
    {
        $this->dependencies = [];
        $this->migrations = [];

        // First, add all migrations to the graph
        foreach ($migrations as $migration) {
            $version = $migration->getVersion()->toString();
            $this->migrations[$version] = $migration;
            $this->dependencies[$version] = [];
        }

        // Then, add dependencies for migrations that implement DependentMigration
        foreach ($migrations as $migration) {
            if ($migration instanceof DependentMigration) {
                $version = $migration->getVersion()->toString();

                foreach ($migration->getDependencies() as $dependencyVersion) {
                    $dependencyVersionString = $dependencyVersion->toString();

                    // Check if the dependency exists
                    if (! isset($this->migrations[$dependencyVersionString])) {
                        throw new DatabaseException(
                            "Migration {$version} depends on {$dependencyVersionString}, but that migration does not exist"
                        );
                    }

                    // Add the dependency to the graph
                    $this->dependencies[$version][] = $dependencyVersionString;
                }
            }
        }

        // Detect circular dependencies
        $this->detectCircularDependencies();
    }

    /**
     * Get the migrations in the correct execution order based on dependencies
     */
    public function getExecutionOrder(): MigrationCollection
    {
        $visited = [];
        $order = [];

        // Perform a topological sort using depth-first search
        foreach (array_keys($this->migrations) as $version) {
            if (! isset($visited[$version])) {
                $this->topologicalSort($version, $visited, $order);
            }
        }

        // Convert the ordered version strings back to Migration instances
        $orderedMigrations = [];
        foreach ($order as $version) {
            $orderedMigrations[] = $this->migrations[$version];
        }

        return new MigrationCollection(...$orderedMigrations);
    }

    /**
     * Get the dependencies for a specific migration
     *
     * @param string $version The migration version
     * @return array<string> The versions this migration depends on
     */
    public function getDependencies(string $version): array
    {
        return $this->dependencies[$version] ?? [];
    }

    /**
     * Check if a migration has dependencies
     */
    public function hasDependencies(string $version): bool
    {
        return ! empty($this->dependencies[$version] ?? []);
    }

    /**
     * Check if a migration is a dependency of another migration
     */
    public function isDependencyOf(string $version, string $potentialDependant): bool
    {
        return in_array($version, $this->dependencies[$potentialDependant] ?? []);
    }

    /**
     * Get all migrations that depend on a specific migration
     *
     * @param string $version The migration version
     * @return array<string> The versions that depend on this migration
     */
    public function getDependants(string $version): array
    {
        $dependants = [];

        foreach ($this->dependencies as $migrationVersion => $dependencies) {
            if (in_array($version, $dependencies)) {
                $dependants[] = $migrationVersion;
            }
        }

        return $dependants;
    }

    /**
     * Detect circular dependencies in the graph
     *
     * @throws DatabaseException If a circular dependency is detected
     */
    private function detectCircularDependencies(): void
    {
        $visited = [];
        $recursionStack = [];

        foreach (array_keys($this->migrations) as $version) {
            if ($this->detectCircularDependenciesUtil($version, $visited, $recursionStack)) {
                $cycle = implode(' -> ', array_keys(array_filter($recursionStack)));

                throw new DatabaseException("Circular dependency detected in migrations: {$cycle}");
            }
        }
    }

    /**
     * Utility function for circular dependency detection using DFS
     * @param array<string, bool> $visited
     * @param array<string, bool> $recursionStack
     */
    private function detectCircularDependenciesUtil(string $version, array &$visited, array &$recursionStack): bool
    {
        // If the node is not visited yet, mark it as visited and add to recursion stack
        if (! isset($visited[$version])) {
            $visited[$version] = true;
            $recursionStack[$version] = true;

            // Visit all dependencies
            foreach ($this->dependencies[$version] as $dependency) {
                if (! isset($visited[$dependency]) && $this->detectCircularDependenciesUtil($dependency, $visited, $recursionStack)) {
                    return true;
                } elseif (isset($recursionStack[$dependency])) {
                    return true; // Circular dependency found
                }
            }
        }

        // Remove the node from recursion stack
        unset($recursionStack[$version]);

        return false;
    }

    /**
     * Perform a topological sort using depth-first search
     * @param array<string, bool> $visited
     * @param array<int, string> $order
     */
    private function topologicalSort(string $version, array &$visited, array &$order): void
    {
        $visited[$version] = true;

        // Visit all dependencies first
        foreach ($this->dependencies[$version] as $dependency) {
            if (! isset($visited[$dependency])) {
                $this->topologicalSort($dependency, $visited, $order);
            }
        }

        // Add the current node to the order
        $order[] = $version;
    }
}
