<?php

declare(strict_types=1);

/**
 * 本文件属于 DBErp 进销存系统。
 *
 * 版权所有 © 2026 北京珑大钜商科技有限公司。保留所有权利。
 *
 * 未经官方授权，不得删除、篡改或隐藏本文件中的版权声明、产品标识与来源信息。
 * 商业授权、官方更新与技术支持服务，请以珑大官方支持中心记录为准。
 *
 * @link https://support.loongdom.cn
 * @see  https://www.dberp.com.cn
 */

namespace App\Services\Plugin;

use App\Services\Install\DBErpInstallStatus;
use DBCore\Contracts\PluginInterface;
use Illuminate\Config\Repository as ConfigRepository;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Foundation\CachesConfiguration;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use Throwable;

/**
 * DBErp 官方插件运行时引导：发现 manifest、按持久化状态启用并加载 Laravel 资源。
 */
final class DberpPluginBootstrapService
{
    private const LOG_CHANNEL_CONTEXT = 'dberp.plugin';

    private bool $discoveryCompleted = false;

    /**
     * @var list<array{identifier: string, path: string, manifest: array<string, mixed>}>
     */
    private array $bootablePlugins = [];

    public function __construct(
        private readonly DBErpInstallStatus $installStatus,
        private readonly DberpPluginDiscoveryManager $discoveryManager,
        private readonly DberpDatabasePluginRepository $repository,
        private readonly DberpOfficialPluginGuard $officialPluginGuard,
    ) {}

    public function resetBootablePluginCache(): void
    {
        $this->discoveryCompleted = false;
        $this->bootablePlugins = [];
    }

    public function isRuntimeEnabled(): bool
    {
        if (! config('dberp.plugin.center_enabled', true)) {
            return false;
        }

        if (! config('dbcore.enabled', true) || ! config('dbcore.modules.plugin', true)) {
            return false;
        }

        if (! $this->installStatus->isInstalled()) {
            return false;
        }

        if (Model::getConnectionResolver() === null) {
            return false;
        }

        try {
            return Schema::hasTable('core_plugins');
        } catch (Throwable) {
            return false;
        }
    }

    /**
     * 扫描并注册已启用插件的 ServiceProvider（register 阶段）。
     */
    public function registerEnabledPlugins(Application $app): void
    {
        if (! $this->isRuntimeEnabled()) {
            return;
        }

        foreach ($this->resolveBootablePlugins() as $plugin) {
            $this->registerPluginProviders($app, $plugin);
        }
    }

    /**
     * 加载已启用插件的路由、迁移、语言包与配置（boot 阶段）。
     */
    public function bootEnabledPlugins(): void
    {
        if (! $this->isRuntimeEnabled()) {
            return;
        }

        foreach ($this->resolveBootablePlugins() as $plugin) {
            $this->bootPluginResources($plugin);
        }
    }

    /**
     * @return list<array{identifier: string, path: string, manifest: array<string, mixed>}>
     */
    private function resolveBootablePlugins(): array
    {
        if ($this->bootablePlugins !== []) {
            return $this->bootablePlugins;
        }

        $this->discoverOfficialPlugins();

        $enabledIdentifiers = $this->repository->enabledIdentifiers();

        if ($enabledIdentifiers === []) {
            return [];
        }

        $this->discoveryManager->bootEnabled($enabledIdentifiers);

        $bootable = [];

        foreach ($enabledIdentifiers as $identifier) {
            $manifest = $this->discoveryManager->find($identifier);

            if ($manifest === null) {
                $this->logWarning('enabled_plugin_not_discovered', [
                    'identifier' => $identifier,
                ]);

                continue;
            }

            if (config('dberp.plugin.official_only', true)
                && ! $this->officialPluginGuard->isOfficialManifest($manifest)) {
                $this->logWarning('plugin_path_rejected', [
                    'identifier' => $identifier,
                ]);

                continue;
            }

            /** @var array<string, mixed> $extra */
            $extra = is_array($manifest['extra'] ?? null) ? $manifest['extra'] : [];
            $path = trim((string) ($extra['path'] ?? ''));

            if ($path === '' || ! is_dir($path)) {
                $this->logWarning('enabled_plugin_directory_missing', [
                    'identifier' => $identifier,
                ]);

                continue;
            }

            $bootable[] = [
                'identifier' => $identifier,
                'path' => $path,
                'manifest' => $manifest,
            ];
        }

        $this->bootablePlugins = $bootable;

        return $this->bootablePlugins;
    }

    private function discoverOfficialPlugins(): void
    {
        if ($this->discoveryCompleted) {
            return;
        }

        try {
            $this->discoveryManager->scan();
        } catch (Throwable $exception) {
            $this->logError('plugin_scan_failed', [], $exception);

            return;
        } finally {
            $this->discoveryCompleted = true;
        }

        foreach ($this->discoveryManager->scanErrors() as $pluginPath => $errors) {
            $this->logWarning('plugin_manifest_invalid', [
                'path' => $pluginPath,
                'errors' => $errors,
            ]);
        }
    }

    /**
     * @param  array{identifier: string, path: string, manifest: array<string, mixed>}  $plugin
     */
    private function registerPluginProviders(Application $app, array $plugin): void
    {
        foreach ($this->resolveProviderClasses($plugin['manifest']) as $providerClass) {
            try {
                if (! class_exists($providerClass)) {
                    $this->logWarning('plugin_provider_missing', [
                        'identifier' => $plugin['identifier'],
                        'provider' => $providerClass,
                    ]);

                    continue;
                }

                $app->register($providerClass);
            } catch (Throwable $exception) {
                $this->logError('plugin_provider_register_failed', [
                    'identifier' => $plugin['identifier'],
                    'provider' => $providerClass,
                ], $exception);
            }
        }

        $entryClass = trim((string) ($plugin['manifest']['entry_class'] ?? ''));

        if ($entryClass === '' || ! class_exists($entryClass)) {
            return;
        }

        if (! is_subclass_of($entryClass, PluginInterface::class)) {
            return;
        }

        try {
            $instance = $app->make($entryClass);

            if ($instance instanceof PluginInterface) {
                $instance->register();
            }
        } catch (Throwable $exception) {
            $this->logError('plugin_entry_register_failed', [
                'identifier' => $plugin['identifier'],
                'entry_class' => $entryClass,
            ], $exception);
        }
    }

    /**
     * @param  array{identifier: string, path: string, manifest: array<string, mixed>}  $plugin
     */
    private function bootPluginResources(array $plugin): void
    {
        $identifier = $plugin['identifier'];
        $path = rtrim($plugin['path'], '\\/');

        $this->loadOptionalRoutes($path);
        $this->loadOptionalMigrations($path);
        $this->loadOptionalTranslations($identifier, $path);
        $this->loadOptionalConfig($identifier, $path);

        $entryClass = trim((string) ($plugin['manifest']['entry_class'] ?? ''));

        if ($entryClass === '' || ! class_exists($entryClass) || ! is_subclass_of($entryClass, PluginInterface::class)) {
            return;
        }

        try {
            $instance = app()->make($entryClass);

            if ($instance instanceof PluginInterface) {
                $instance->boot();
            }
        } catch (Throwable $exception) {
            $this->logError('plugin_entry_boot_failed', [
                'identifier' => $identifier,
                'entry_class' => $entryClass,
            ], $exception);
        }
    }

    private function loadOptionalRoutes(string $path): void
    {
        $webRoutes = $path.DIRECTORY_SEPARATOR.'routes'.DIRECTORY_SEPARATOR.'web.php';
        $apiRoutes = $path.DIRECTORY_SEPARATOR.'routes'.DIRECTORY_SEPARATOR.'api.php';

        // 须在应用 booted 后加载，避免早于 RouteServiceProvider 注册导致路由被覆盖。
        app()->booted(function () use ($webRoutes, $apiRoutes, $path): void {
            try {
                if (is_file($webRoutes)) {
                    require $webRoutes;
                }

                if (is_file($apiRoutes)) {
                    require $apiRoutes;
                }
            } catch (Throwable $exception) {
                $this->logError('plugin_routes_load_failed', [
                    'path' => $path,
                ], $exception);
            }
        });
    }

    private function loadOptionalMigrations(string $path): void
    {
        $migrationsPath = $path.DIRECTORY_SEPARATOR.'database'.DIRECTORY_SEPARATOR.'migrations';

        if (! is_dir($migrationsPath)) {
            return;
        }

        try {
            $this->callAfterResolving('migrator', function ($migrator) use ($migrationsPath): void {
                $migrator->path($migrationsPath);
            });
        } catch (Throwable $exception) {
            $this->logError('plugin_migrations_load_failed', [
                'path' => $migrationsPath,
            ], $exception);
        }
    }

    private function loadOptionalTranslations(string $identifier, string $path): void
    {
        $langPath = $path.DIRECTORY_SEPARATOR.'lang';

        if (! is_dir($langPath)) {
            return;
        }

        $namespace = 'plugin-'.$identifier;

        try {
            $this->callAfterResolving('translator', function ($translator) use ($langPath, $namespace): void {
                $translator->addNamespace($namespace, $langPath);
            });
        } catch (Throwable $exception) {
            $this->logError('plugin_translations_load_failed', [
                'identifier' => $identifier,
            ], $exception);
        }
    }

    private function loadOptionalConfig(string $identifier, string $path): void
    {
        $configDirectory = $path.DIRECTORY_SEPARATOR.'config';

        if (! is_dir($configDirectory)) {
            return;
        }

        $configFiles = glob($configDirectory.DIRECTORY_SEPARATOR.'*.php') ?: [];

        foreach ($configFiles as $configFile) {
            if (! is_string($configFile) || ! is_file($configFile)) {
                continue;
            }

            $basename = basename($configFile, '.php');
            $configKey = $basename === 'plugin'
                ? 'dberp.plugin.extensions.'.$identifier
                : 'dberp.plugin.extensions.'.$identifier.'.'.$basename;

            try {
                $this->mergeOptionalConfigFrom($configFile, $configKey);
            } catch (Throwable $exception) {
                $this->logError('plugin_config_merge_failed', [
                    'identifier' => $identifier,
                    'config_key' => $configKey,
                ], $exception);
            }
        }
    }

    /**
     * @param  array<string, mixed>  $manifest
     * @return list<class-string>
     */
    private function resolveProviderClasses(array $manifest): array
    {
        /** @var array<string, mixed> $extra */
        $extra = is_array($manifest['extra'] ?? null) ? $manifest['extra'] : [];

        $providers = [];

        foreach ([$manifest['providers'] ?? null, $extra['providers'] ?? null] as $candidate) {
            if (! is_array($candidate)) {
                continue;
            }

            foreach ($candidate as $provider) {
                if (! is_string($provider)) {
                    continue;
                }

                $provider = trim($provider);

                if ($provider !== '') {
                    $providers[] = $provider;
                }
            }
        }

        return array_values(array_unique($providers));
    }

    /**
     * @param  callable(mixed, Application): void  $callback
     */
    private function callAfterResolving(string $name, callable $callback): void
    {
        app()->afterResolving($name, $callback);

        if (app()->resolved($name)) {
            $callback(app()->make($name), app());
        }
    }

    private function mergeOptionalConfigFrom(string $configFile, string $configKey): void
    {
        $app = app();

        if ($app instanceof CachesConfiguration && $app->configurationIsCached()) {
            return;
        }

        /** @var ConfigRepository $config */
        $config = $app->make('config');

        $config->set($configKey, array_merge(
            require $configFile,
            $config->get($configKey, []),
        ));
    }

    /**
     * @param  array<string, mixed>  $context
     */
    private function logWarning(string $event, array $context = []): void
    {
        Log::warning($this->formatLogMessage($event), array_merge([
            'channel' => self::LOG_CHANNEL_CONTEXT,
        ], $context));
    }

    /**
     * @param  array<string, mixed>  $context
     */
    private function logError(string $event, array $context, Throwable $exception): void
    {
        Log::error($this->formatLogMessage($event), array_merge([
            'channel' => self::LOG_CHANNEL_CONTEXT,
            'exception_class' => $exception::class,
            'exception_message' => $exception->getMessage(),
        ], $context));
    }

    private function formatLogMessage(string $event): string
    {
        return '['.self::LOG_CHANNEL_CONTEXT.'] '.$event;
    }
}
