<?php

/*
 * Core PHP Framework
 *
 * Licensed under the European Union Public Licence (EUPL) v1.2.
 * See LICENSE file for details.
 */

declare(strict_types=1);

namespace Core\Actions;

use Core\ModuleScanner;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ReflectionClass;

/**
 * Scans directories for Action classes with the #[Scheduled] attribute.
 *
 * Unlike ModuleScanner (which scans Boot.php files), this scanner finds
 * any PHP class with the #[Scheduled] attribute in the given directories.
 *
 * It uses PHP's native reflection to read attributes — no file parsing.
 *
 * @see Scheduled The attribute this scanner discovers
 * @see ModuleScanner Similar pattern for Boot.php discovery
 */
class ScheduledActionScanner
{
    /**
     * Scan directories for classes with #[Scheduled] attribute.
     *
     * @param  array<string>  $paths  Directories to scan recursively
     * @return array<class-string, Scheduled> Map of class name to attribute instance
     */
    public function scan(array $paths): array
    {
        $results = [];

        foreach ($paths as $path) {
            if (! is_dir($path)) {
                continue;
            }

            $iterator = new RecursiveIteratorIterator(
                new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS)
            );

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

                // Skip test directories — test files extend base classes
                // that aren't available without dev dependencies.
                // Convention: module test dirs use capital "Tests/" (e.g. app/Mod/Lem/Tests/).
                if (preg_match('#[/\\\\]Tests[/\\\\]#', $file->getPathname())
                    || str_ends_with($file->getBasename(), 'Test.php')) {
                    continue;
                }

                $contents = file_get_contents($file->getPathname());

                if ($contents === false || ! str_contains($contents, '#[Scheduled')) {
                    continue;
                }

                $class = $this->classFromFile($file->getPathname(), $contents);

                if ($class === null) {
                    continue;
                }

                try {
                    if (! class_exists($class)) {
                        continue;
                    }
                } catch (\Throwable) {
                    // Class may reference unavailable dependencies (e.g. dev-only)
                    continue;
                }

                $attribute = $this->extractScheduled($class);

                if ($attribute !== null) {
                    $results[$class] = $attribute;
                }
            }
        }

        return $results;
    }

    /**
     * Extract the #[Scheduled] attribute from a class.
     */
    private function extractScheduled(string $class): ?Scheduled
    {
        try {
            $ref = new ReflectionClass($class);
            $attrs = $ref->getAttributes(Scheduled::class);

            if (empty($attrs)) {
                return null;
            }

            return $attrs[0]->newInstance();
        } catch (\ReflectionException) {
            return null;
        }
    }

    /**
     * Derive fully qualified class name from a PHP file.
     *
     * Reads the file's namespace declaration and class name.
     */
    private function classFromFile(string $file, ?string $contents = null): ?string
    {
        $contents ??= file_get_contents($file);

        if ($contents === false) {
            return null;
        }

        $tokens = token_get_all($contents);
        $namespace = null;
        $class = null;

        $count = count($tokens);

        for ($i = 0; $i < $count; $i++) {
            $token = $tokens[$i];

            if (! is_array($token)) {
                continue;
            }

            if ($token[0] === T_NAMESPACE) {
                $namespaceParts = [];

                for ($j = $i + 1; $j < $count; $j++) {
                    $t = $tokens[$j];

                    if (is_array($t) && in_array($t[0], [T_NAME_QUALIFIED, T_STRING, T_NS_SEPARATOR], true)) {
                        $namespaceParts[] = $t[1];
                    } elseif ($t === ';' || $t === '{') {
                        break;
                    }
                }

                $namespace = implode('', $namespaceParts) ?: null;
            }

            if ($token[0] === T_CLASS) {
                for ($j = $i + 1; $j < $count; $j++) {
                    $t = $tokens[$j];

                    if (is_array($t) && $t[0] === T_WHITESPACE) {
                        continue;
                    }
                    if (is_array($t) && $t[0] === T_STRING) {
                        $class = $t[1];
                    }

                    break;
                }

                break;
            }
        }

        if ($class === null) {
            return null;
        }

        return $namespace !== null ? "{$namespace}\\{$class}" : $class;
    }
}
