<?php

namespace MintyPHP\Console\Commands\Module;

use MintyPHP\App\Container\ContainerRegistrar;
use MintyPHP\App\Module\Contracts\EventListener;
use MintyPHP\App\Module\Contracts\LayoutContextProvider;
use MintyPHP\App\Module\Contracts\ModuleDeactivationHandler;
use MintyPHP\App\Module\Contracts\SearchResourceProvider;
use MintyPHP\App\Module\Contracts\SessionProvider;
use MintyPHP\App\Module\ModuleEvents;
use MintyPHP\App\Module\ModuleManifestLoader;
use MintyPHP\Console\Command;
use MintyPHP\Service\Access\AuthorizationPolicyInterface;

/**
 * Validates a module's manifest, class contracts, and structure.
 *
 * Catches errors that would otherwise only surface at boot time.
 */
final class ValidateCommand extends Command
{
    public function name(): string
    {
        return 'module:validate';
    }

    public function description(): string
    {
        return 'Validate a module\'s manifest, classes, and structure';
    }

    public function usage(): string
    {
        return <<<'USAGE'
        Usage: php bin/console module:validate <module-id>
               php bin/console module:validate --all

        Validates:
          - Manifest validates against .agents/contracts/module-manifest.schema.json
          - Manifest parses without error
          - All declared classes exist and implement correct interfaces
          - PHP classes use MintyPHP\Module\* namespace
          - Permission keys are well-formed
          - Required directories exist
          - Route paths don't contain suspicious patterns
        USAGE;
    }

    public function execute(array $args, array $options): int
    {
        $this->bootstrapApp('cli', 'bin/console module:validate');

        $modulesDir = $this->projectRoot() . '/modules';
        $validateAll = $options['all'] ?? false;
        $moduleId = trim($args[0] ?? '');

        if (!$validateAll && $moduleId === '') {
            fwrite(STDERR, $this->usage() . "\n");
            return 1;
        }

        $moduleIds = $validateAll
            ? $this->discoverModuleIds($modulesDir)
            : [$moduleId];

        if ($moduleIds === []) {
            fwrite(STDERR, "No modules found in modules/\n");
            return 1;
        }

        $totalErrors = 0;
        $totalWarnings = 0;

        foreach ($moduleIds as $id) {
            [$errors, $warnings] = $this->validateModule($modulesDir, $id);
            $totalErrors += count($errors);
            $totalWarnings += count($warnings);

            if ($errors === [] && $warnings === []) {
                fwrite(STDOUT, "[OK] {$id}\n");
                continue;
            }

            foreach ($errors as $error) {
                fwrite(STDOUT, "[FAIL] {$id}: {$error}\n");
            }
            foreach ($warnings as $warning) {
                fwrite(STDOUT, "[WARN] {$id}: {$warning}\n");
            }
        }

        fwrite(STDOUT, sprintf(
            "\nSummary: %d module(s), %d error(s), %d warning(s)\n",
            count($moduleIds),
            $totalErrors,
            $totalWarnings
        ));

        return $totalErrors > 0 ? 1 : 0;
    }

    /**
     * @return list<string>
     */
    private function discoverModuleIds(string $modulesDir): array
    {
        if (!is_dir($modulesDir)) {
            return [];
        }
        $ids = [];
        $entries = scandir($modulesDir) ?: [];
        foreach ($entries as $entry) {
            if ($entry === '.' || $entry === '..' || !is_dir($modulesDir . '/' . $entry)) {
                continue;
            }
            if (is_file($modulesDir . '/' . $entry . '/module.php')) {
                $ids[] = $entry;
            }
        }
        sort($ids);
        return $ids;
    }

    /**
     * @return array{list<string>, list<string>} [errors, warnings]
     */
    private function validateModule(string $modulesDir, string $moduleId): array
    {
        $errors = [];
        $warnings = [];
        $moduleDir = $modulesDir . '/' . $moduleId;
        $manifestFile = $moduleDir . '/module.php';

        // 1. Directory exists
        if (!is_dir($moduleDir)) {
            return [["Module directory not found: modules/{$moduleId}/"], []];
        }

        // 2. Manifest exists
        if (!is_file($manifestFile)) {
            return [["Manifest not found: modules/{$moduleId}/module.php"], []];
        }

        // 3+4. Load and validate manifest
        try {
            $manifest = ModuleManifestLoader::loadFromDisk($modulesDir, $moduleId);
        } catch (\Throwable $e) {
            return [['Manifest load/validation error: ' . $e->getMessage()], []];
        }

        // Register module lib/ with autoloader so we can resolve classes
        // even if the module is not in the enabled list
        $libDir = $moduleDir . '/lib';
        if (is_dir($libDir)) {
            $composerLoader = null;
            foreach (spl_autoload_functions() as $autoloader) {
                if (is_array($autoloader) && $autoloader[0] instanceof \Composer\Autoload\ClassLoader) {
                    $composerLoader = $autoloader[0];
                    break;
                }
            }
            $composerLoader?->addPsr4('MintyPHP\\', [$libDir]);
        }

        // 5. ID matches directory
        if ($manifest->id !== $moduleId) {
            $errors[] = "Manifest id '{$manifest->id}' does not match directory '{$moduleId}'";
        }

        // 6. Version is semver
        if (!preg_match('/^\d+\.\d+\.\d+$/', $manifest->version)) {
            $warnings[] = "Version '{$manifest->version}' is not semver (expected x.y.z)";
        }

        // 7. lib/ directory exists
        if (!is_dir($moduleDir . '/lib')) {
            $warnings[] = 'No lib/ directory (module has no PHP classes)';
        }

        // 8. Container registrar classes exist and implement interface
        foreach ($manifest->containerRegistrars as $class) {
            if (!class_exists($class)) {
                $errors[] = "Container registrar class not found: {$class}";
            } elseif (!is_subclass_of($class, ContainerRegistrar::class)) {
                $errors[] = "Container registrar does not implement ContainerRegistrar: {$class}";
            }
        }

        // 9. Authorization policy classes
        foreach ($manifest->authorizationPolicies as $class) {
            if (!class_exists($class)) {
                $errors[] = "Authorization policy class not found: {$class}";
            } elseif (!in_array(AuthorizationPolicyInterface::class, class_implements($class) ?: [], true)) {
                $errors[] = "Authorization policy does not implement AuthorizationPolicyInterface: {$class}";
            }
        }

        // 10. Session provider classes
        foreach ($manifest->sessionProviders as $class) {
            if (!class_exists($class)) {
                $errors[] = "Session provider class not found: {$class}";
            } elseif (!in_array(SessionProvider::class, class_implements($class) ?: [], true)) {
                $errors[] = "Session provider does not implement SessionProvider: {$class}";
            }
        }

        // 11. Layout context provider classes
        foreach ($manifest->layoutContextProviders as $class) {
            if (!class_exists($class)) {
                $errors[] = "Layout context provider class not found: {$class}";
            } elseif (!in_array(LayoutContextProvider::class, class_implements($class) ?: [], true)) {
                $errors[] = "Layout context provider does not implement LayoutContextProvider: {$class}";
            }
        }

        // 12. Search resource provider classes
        foreach ($manifest->searchResources as $class) {
            if (!class_exists($class)) {
                $errors[] = "Search resource provider class not found: {$class}";
            } elseif (!in_array(SearchResourceProvider::class, class_implements($class) ?: [], true)) {
                $errors[] = "Search resource provider does not implement SearchResourceProvider: {$class}";
            }
        }

        // 13. Event listener classes
        foreach ($manifest->eventListeners as $eventName => $handlers) {
            foreach ($handlers as $i => $handler) {
                $class = $handler['class'];
                if (!class_exists($class)) {
                    $errors[] = "Event listener class not found: {$class} (event: {$eventName})";
                } elseif (!in_array(EventListener::class, class_implements($class) ?: [], true)) {
                    $errors[] = "Event listener does not implement EventListener: {$class} (event: {$eventName})";
                }
            }
        }

        // 14. Deactivation handler class
        if ($manifest->deactivationHandler !== null) {
            $class = $manifest->deactivationHandler;
            if (!class_exists($class)) {
                $errors[] = "Deactivation handler class not found: {$class}";
            } elseif (!in_array(ModuleDeactivationHandler::class, class_implements($class) ?: [], true)) {
                $errors[] = "Deactivation handler does not implement ModuleDeactivationHandler: {$class}";
            }
        }

        // 15. Permission keys are well-formed
        foreach ($manifest->permissions as $i => $perm) {
            $key = $perm['key'];
            if (!preg_match('/^[a-z][a-z0-9._-]+$/', $key)) {
                $errors[] = "Permission key has invalid format: '{$key}'";
            }
        }

        // 16. Namespace compliance — PHP files in lib/ must use MintyPHP\Module\*
        if (is_dir($moduleDir . '/lib')) {
            $this->checkNamespaceCompliance($moduleDir . '/lib', $moduleId, $errors);
        }

        // 17. Routes have pages directory
        if ($manifest->routes !== [] && !is_dir($moduleDir . '/pages')) {
            $errors[] = 'Module declares routes but has no pages/ directory';
        }

        // 18. UI slot templates exist
        foreach ($manifest->uiSlots as $slotName => $contributions) {
            $contributionList = is_array($contributions) ? $contributions : [$contributions];
            foreach ($contributionList as $idx => $contribution) {
                if (!is_array($contribution)) {
                    continue;
                }
                foreach (['panel_template', 'template'] as $templateKey) {
                    $template = $contribution[$templateKey] ?? null;
                    if ($template === null) {
                        continue;
                    }
                    $templatePath = (string) $template;
                    if (!str_starts_with($templatePath, '/')) {
                        $templatePath = $moduleDir . '/' . $templatePath;
                    }
                    if (!is_file($templatePath)) {
                        $errors[] = "UI slot '{$slotName}' {$templateKey} not found: {$template}";
                    }
                }
            }
        }

        // 19. Required modules exist on disk
        foreach ($manifest->requires as $requiredId) {
            if (!is_dir($modulesDir . '/' . $requiredId)) {
                $errors[] = "Required module '{$requiredId}' not found on disk";
            }
        }

        // 20. AuthorizationPolicy classes referenced in ContainerRegistrar source
        $registrarSources = $this->loadRegistrarSources($moduleDir, $manifest->containerRegistrars);
        foreach ($manifest->authorizationPolicies as $policyClass) {
            if (!class_exists($policyClass)) {
                continue; // Already reported by check 9
            }
            $shortName = (new \ReflectionClass($policyClass))->getShortName();
            $found = false;
            foreach ($registrarSources as $source) {
                if (str_contains($source, $shortName)) {
                    $found = true;
                    break;
                }
            }
            if (!$found) {
                $warnings[] = "AuthorizationPolicy '{$shortName}' is not referenced in any ContainerRegistrar — may not be registered in the container";
            }
        }

        // 21. Event listener keys reference known events
        $knownEvents = ModuleEvents::all();
        foreach ($manifest->eventListeners as $eventName => $handlers) {
            if (!in_array($eventName, $knownEvents, true)) {
                $warnings[] = "Event listener subscribes to unknown event '{$eventName}' — not defined in ModuleEvents";
            }
        }

        return [$errors, $warnings];
    }

    /**
     * @param list<class-string> $registrarClasses
     * @return list<string> Source code of each registrar file
     */
    private function loadRegistrarSources(string $moduleDir, array $registrarClasses): array
    {
        $sources = [];
        foreach ($registrarClasses as $class) {
            if (!class_exists($class)) {
                continue;
            }
            try {
                $refClass = new \ReflectionClass($class);
                $file = $refClass->getFileName();
                if ($file !== false && is_file($file)) {
                    $content = file_get_contents($file);
                    if ($content !== false) {
                        $sources[] = $content;
                    }
                }
            } catch (\ReflectionException) {
                // Skip unresolvable classes
            }
        }
        return $sources;
    }

    /**
     * @param list<string> &$errors
     */
    private function checkNamespaceCompliance(string $libDir, string $moduleId, array &$errors): void
    {
        $coreNamespacePrefixes = [
            'MintyPHP\\Service\\',
            'MintyPHP\\Repository\\',
            'MintyPHP\\Support\\',
            'MintyPHP\\Http\\',
            'MintyPHP\\App\\Container\\',
        ];

        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($libDir, \FilesystemIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::LEAVES_ONLY
        );

        foreach ($iterator as $file) {
            if ($file->getExtension() !== 'php') {
                continue;
            }

            $content = file_get_contents($file->getPathname());
            if ($content === false) {
                continue;
            }

            if (preg_match('/^namespace\s+(.+?);/m', $content, $match)) {
                $namespace = $match[1];
                foreach ($coreNamespacePrefixes as $corePrefix) {
                    if (str_starts_with($namespace . '\\', $corePrefix)) {
                        $errors[] = "{$file->getFilename()} uses core namespace '{$namespace}' — must use MintyPHP\\Module\\*";
                    }
                }
            }
        }
    }
}
