<?php

declare(strict_types=1);

namespace App\Support\Ui\ActionSurface;

use App\Support\Ui\ActionSurface\Enums\ActionSurfaceComponentType;
use App\Support\Ui\ActionSurface\Enums\ActionSurfacePanelScope;
use ReflectionClass;
use Filament\Tables\Contracts\HasTable;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use SplFileInfo;
use Throwable;

final class ActionSurfaceDiscovery
{
    private string $basePath;

    private string $appPath;

    private string $routesPath;

    private string $adminPanelProviderPath;

    public function __construct(
        ?string $basePath = null,
        ?string $appPath = null,
        ?string $routesPath = null,
        ?string $adminPanelProviderPath = null,
    ) {
        $this->basePath = $basePath ?? base_path();
        $this->appPath = $appPath ?? app_path();
        $this->routesPath = $routesPath ?? base_path('routes/web.php');
        $this->adminPanelProviderPath = $adminPanelProviderPath ?? app_path('Providers/Filament/AdminPanelProvider.php');
    }

    /**
     * @return array<int, ActionSurfaceDiscoveredComponent>
     */
    public function discover(): array
    {
        $adminScopedClasses = $this->discoverAdminScopedClasses();

        /** @var array<string, ActionSurfaceDiscoveredComponent> $components */
        $components = [];

        foreach ($this->resourceFiles() as $path) {
            $className = $this->classNameFromPath($path);

            $components[$className] = new ActionSurfaceDiscoveredComponent(
                className: $className,
                componentType: ActionSurfaceComponentType::Resource,
                panelScopes: $this->panelScopesFor($className, $adminScopedClasses),
            );
        }

        foreach ($this->pageFiles() as $path) {
            $className = $this->classNameFromPath($path);

            $components[$className] = new ActionSurfaceDiscoveredComponent(
                className: $className,
                componentType: ActionSurfaceComponentType::Page,
                panelScopes: $this->panelScopesFor($className, $adminScopedClasses),
            );
        }

        foreach ($this->systemPageFiles() as $path) {
            $className = $this->classNameFromPath($path);

            if (! $this->isDeclaredSystemTablePage($className)) {
                continue;
            }

            $components[$className] = new ActionSurfaceDiscoveredComponent(
                className: $className,
                componentType: ActionSurfaceComponentType::Page,
                panelScopes: $this->panelScopesFor($className, $adminScopedClasses),
            );
        }

        foreach ($this->relationManagerFiles() as $path) {
            $className = $this->classNameFromPath($path);

            $components[$className] = new ActionSurfaceDiscoveredComponent(
                className: $className,
                componentType: ActionSurfaceComponentType::RelationManager,
                panelScopes: $this->panelScopesFor($className, $adminScopedClasses),
            );
        }

        ksort($components);

        return array_values($components);
    }

    /**
     * @param  array<int, string>  $adminScopedClasses
     * @return array<int, ActionSurfacePanelScope>
     */
    private function panelScopesFor(string $className, array $adminScopedClasses): array
    {
        $scopes = [ActionSurfacePanelScope::ManagedEnvironment];

        if (
            in_array($className, $adminScopedClasses, true)
            || $this->inheritsAdminScopeFromResource($className, $adminScopedClasses)
        ) {
            $scopes[] = ActionSurfacePanelScope::Admin;
        }

        return $scopes;
    }

    /**
     * @return array<int, string>
     */
    private function resourceFiles(): array
    {
        return $this->collectPhpFiles($this->appPath.'/Filament/Resources', function (string $path): bool {
            if (! str_ends_with($path, 'Resource.php')) {
                return false;
            }

            if (str_contains($path, '/Pages/')) {
                return false;
            }

            if (str_contains($path, '/RelationManagers/')) {
                return false;
            }

            return true;
        });
    }

    /**
     * @return array<int, string>
     */
    private function pageFiles(): array
    {
        return $this->collectPhpFiles($this->appPath.'/Filament/Pages', static function (string $path): bool {
            return str_ends_with($path, '.php');
        });
    }

    /**
     * @return array<int, string>
     */
    private function systemPageFiles(): array
    {
        return $this->collectPhpFiles($this->appPath.'/Filament/System/Pages', static function (string $path): bool {
            return str_ends_with($path, '.php');
        });
    }

    /**
     * @return array<int, string>
     */
    private function relationManagerFiles(): array
    {
        return $this->collectPhpFiles($this->appPath.'/Filament/Resources', function (string $path): bool {
            if (! str_contains($path, '/RelationManagers/')) {
                return false;
            }

            return str_ends_with($path, 'RelationManager.php');
        });
    }

    /**
     * @param  callable(string): bool  $filter
     * @return array<int, string>
     */
    private function collectPhpFiles(string $directory, callable $filter): array
    {
        if (! is_dir($directory)) {
            return [];
        }

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

        $paths = [];

        /** @var SplFileInfo $file */
        foreach ($iterator as $file) {
            if (! $file->isFile()) {
                continue;
            }

            $path = str_replace('\\', '/', $file->getPathname());

            if (! $filter($path)) {
                continue;
            }

            $paths[] = $path;
        }

        sort($paths);

        return $paths;
    }

    /**
     * @return array<int, string>
     */
    private function discoverAdminScopedClasses(): array
    {
        $classes = array_merge(
            $this->parseFilamentClassReferences($this->adminPanelProviderPath),
            $this->parseFilamentClassReferences($this->routesPath),
        );

        $classes = array_values(array_unique(array_filter($classes, static function (string $className): bool {
            return str_starts_with($className, 'App\\Filament\\');
        })));

        sort($classes);

        return $classes;
    }

    private function isDeclaredSystemTablePage(string $className): bool
    {
        if (! class_exists($className)) {
            return false;
        }

        return is_subclass_of($className, HasTable::class)
            && method_exists($className, 'actionSurfaceDeclaration');
    }

    /**
     * Resource-owned Filament pages can live under app/Filament/Pages and be routed
     * from the resource instead of being panel-registered directly. When that happens,
     * inherit admin scope from the owning resource so discovery stays truthful.
     *
     * @param  array<int, string>  $adminScopedClasses
     */
    private function inheritsAdminScopeFromResource(string $className, array $adminScopedClasses): bool
    {
        if (! class_exists($className)) {
            return false;
        }

        try {
            $reflection = new ReflectionClass($className);

            if (! $reflection->hasProperty('resource')) {
                return false;
            }

            $defaults = $reflection->getDefaultProperties();
            $resourceClass = $defaults['resource'] ?? null;

            return is_string($resourceClass)
                && $resourceClass !== ''
                && in_array($resourceClass, $adminScopedClasses, true);
        } catch (Throwable) {
            return false;
        }
    }

    /**
     * @return array<int, string>
     */
    private function parseFilamentClassReferences(string $filePath): array
    {
        if (! is_file($filePath)) {
            return [];
        }

        $contents = file_get_contents($filePath);

        if (! is_string($contents) || $contents === '') {
            return [];
        }

        $imports = $this->parseUseStatements($contents);

        preg_match_all('/\\\\?([A-Z][A-Za-z0-9_\\\\]*)::(?:class|registerRoutes)\b/', $contents, $matches);

        $classes = [];

        foreach ($matches[1] as $token) {
            $resolved = $this->resolveClassToken($token, $imports);

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

            $classes[] = $resolved;
        }

        return $classes;
    }

    /**
     * @return array<string, string>
     */
    private function parseUseStatements(string $contents): array
    {
        preg_match_all('/^use\s+([^;]+);/m', $contents, $matches);

        $imports = [];

        foreach ($matches[1] as $importExpression) {
            $normalized = trim($importExpression);

            if (! str_contains($normalized, '\\')) {
                continue;
            }

            $parts = preg_split('/\s+as\s+/i', $normalized);
            $fqcn = ltrim($parts[0], '\\');

            $alias = $parts[1] ?? null;

            if (! is_string($alias) || trim($alias) === '') {
                $segments = explode('\\', $fqcn);
                $alias = end($segments);
            }

            if (! is_string($alias) || trim($alias) === '') {
                continue;
            }

            $imports[trim($alias)] = $fqcn;
        }

        return $imports;
    }

    /**
     * @param  array<string, string>  $imports
     */
    private function resolveClassToken(string $token, array $imports): ?string
    {
        $token = ltrim(trim($token), '\\');

        if ($token === '') {
            return null;
        }

        if (str_contains($token, '\\')) {
            return $token;
        }

        return $imports[$token] ?? null;
    }

    private function classNameFromPath(string $path): string
    {
        $normalizedPath = str_replace('\\', '/', $path);
        $normalizedAppPath = str_replace('\\', '/', $this->appPath);
        $relative = ltrim(substr($normalizedPath, strlen($normalizedAppPath)), '/');

        return 'App\\'.str_replace('/', '\\', substr($relative, 0, -4));
    }
}
