<?php

namespace App\Services\System;

use App\Models\Module;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;

class ModuleLoader
{
    /**
     * Boot all active modules by registering their Service Providers.
     *
     * Respects module_requires: if a module declares dependencies via the
     * 'module_requires' key in module.json, all required module slugs must
     * already be registered before this module is booted. Modules are processed
     * in dependency order (dependencies first) using a topological sort.
     *
     * This is called once per request from AppServiceProvider::boot(),
     * after the DB connection is confirmed available.
     */
    public static function boot(Application $app): void
    {
        // Guard: modules table may not exist on a brand-new fresh install
        if (! Schema::hasTable('modules')) {
            return;
        }

        try {
            $modules = Module::getActive()->keyBy('slug');

            // Build a dependency graph and topologically sort active modules
            // so dependencies are always registered before their dependants.
            $sorted = static::topologicalSort($modules);

            $registered = []; // slugs successfully registered this boot

            foreach ($sorted as $module) {
                $provider = $module->provider_class;

                if (! class_exists($provider)) {
                    Log::warning("ModuleLoader: Provider class [{$provider}] for module [{$module->slug}] not found. Skipping.");
                    continue;
                }

                // Check module_requires — all required slugs must be in $registered
                $requires = $module->meta['module_requires'] ?? [];
                $missing  = array_filter((array) $requires, fn ($slug) => ! in_array($slug, $registered, true));

                if (! empty($missing)) {
                    Log::warning("ModuleLoader: Module [{$module->slug}] requires [" . implode(', ', $missing) . "] which are not active. Skipping.");
                    continue;
                }

                $app->register($provider);
                $registered[] = $module->slug;
            }
        } catch (\Throwable $e) {
            // Never let a faulty module crash the entire application.
            Log::error('ModuleLoader: Failed to load modules.', [
                'error' => $e->getMessage(),
            ]);
        }
    }

    /**
     * Topologically sort a collection of modules so dependencies come first.
     *
     * Uses Kahn's algorithm. Modules with no dependencies are processed first.
     * Circular dependencies or unresolvable graphs fall back to the original order.
     *
     * @param  \Illuminate\Support\Collection<string, Module>  $modules  Keyed by slug
     * @return Module[]
     */
    private static function topologicalSort(\Illuminate\Support\Collection $modules): array
    {
        $inDegree  = [];
        $adjList   = [];

        foreach ($modules as $slug => $module) {
            $inDegree[$slug] = $inDegree[$slug] ?? 0;
            $requires        = $module->meta['module_requires'] ?? [];

            foreach ((array) $requires as $dep) {
                if (! $modules->has($dep)) {
                    continue; // inactive/missing dependency — boot() will skip this module
                }
                $adjList[$dep][] = $slug;
                $inDegree[$slug] = ($inDegree[$slug] ?? 0) + 1;
            }
        }

        // Start with nodes that have no unmet dependencies
        $queue  = array_keys(array_filter($inDegree, fn ($d) => $d === 0));
        $sorted = [];

        while (! empty($queue)) {
            $current  = array_shift($queue);
            $sorted[] = $modules[$current];

            foreach ($adjList[$current] ?? [] as $dependent) {
                $inDegree[$dependent]--;
                if ($inDegree[$dependent] === 0) {
                    $queue[] = $dependent;
                }
            }
        }

        // If not all modules were sorted (cycle or fully external dependency),
        // append the remainder in original order so nothing is silently dropped.
        $sortedSlugs = array_map(fn ($m) => $m->slug, $sorted);
        foreach ($modules as $slug => $module) {
            if (! in_array($slug, $sortedSlugs, true)) {
                $sorted[] = $module;
            }
        }

        return $sorted;
    }

    /**
     * Install a module from its extracted directory path.
     * Reads `module.json`, registers it in the DB, and optionally activates it.
     *
     * @throws \RuntimeException
     */
    public static function install(string $modulePath): Module
    {
        $manifestPath = rtrim($modulePath, '/') . '/module.json';

        if (! file_exists($manifestPath)) {
            throw new \RuntimeException("Invalid module: module.json not found in [{$modulePath}].");
        }

        $manifest = json_decode(file_get_contents($manifestPath), true);

        if (json_last_error() !== JSON_ERROR_NONE || empty($manifest['slug'])) {
            throw new \RuntimeException("Invalid module.json in [{$modulePath}]: malformed or missing 'slug'.");
        }

        $module = Module::updateOrCreate(
            ['slug' => $manifest['slug']],
            [
                'name'           => $manifest['name'] ?? $manifest['slug'],
                'version'        => $manifest['version'] ?? '1.0.0',
                'description'    => $manifest['description'] ?? null,
                'author'         => $manifest['author'] ?? null,
                'provider_class' => $manifest['provider'] ?? '',
                'status'         => 'inactive',
                'meta'           => $manifest,
                'installed_at'   => now(),
            ]
        );

        return $module;
    }

    /**
     * Get the base path for the modules directory.
     */
    public static function modulesPath(string $path = ''): string
    {
        return base_path('modules' . ($path ? DIRECTORY_SEPARATOR . $path : ''));
    }

    /**
     * Create a public symlink for a module's assets.
     *
     * If the module has a `public/` directory, a symlink is created at
     * `public/modules/{directory}` pointing to `modules/{directory}/public`.
     * This makes module assets accessible from the browser via:
     *   asset('modules/{directory}/js/file.js')
     *
     * Safe to call even if the module has no public directory (no-op).
     */
    public static function linkAssets(string $directory): void
    {
        $modulePublic = base_path('modules' . DIRECTORY_SEPARATOR . $directory . DIRECTORY_SEPARATOR . 'public');

        if (! is_dir($modulePublic)) {
            return; // Module has no public assets — nothing to link
        }

        $publicModulesDir = public_path('modules');
        if (! is_dir($publicModulesDir)) {
            mkdir($publicModulesDir, 0755, true);
        }

        $link = $publicModulesDir . DIRECTORY_SEPARATOR . $directory;

        if (is_link($link) || file_exists($link)) {
            return; // Already linked
        }

        // Try symlink first; fall back to a directory junction on Windows
        // (junctions don't require elevated privileges unlike symlinks).
        if (! @symlink($modulePublic, $link)) {
            if (PHP_OS_FAMILY === 'Windows') {
                exec('mklink /J "' . $link . '" "' . $modulePublic . '"');
            }
        }
    }

    /**
     * Remove the public symlink for a module's assets.
     *
     * Called when a module is uninstalled. Only removes the symlink —
     * the module's actual public directory is removed with the module itself.
     */
    public static function unlinkAssets(string $directory): void
    {
        $link = public_path('modules' . DIRECTORY_SEPARATOR . $directory);

        if (is_link($link)) {
            unlink($link);
        }
    }
}
