<?php

declare(strict_types=1);

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

namespace App\Services\Plugin;

use App\Exceptions\Plugin\HostPluginInstallException;
use App\Exceptions\SupportCenter\SupportCenterException;
use App\Services\DberpPermissionService;
use App\Services\SupportCenter\HostSystemUpdateArchiveExtractor;
use DBCore\Support\Results\OperationResult;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Throwable;

/**
 * 官方插件安装编排：独立于系统更新一键编排，仅安装至 plugins/{alias}。
 */
final class HostPluginInstallOrchestrator
{
    public function __construct(
        private readonly DberpPermissionService $permissionService,
        private readonly HostPluginEntitlementGate $entitlementGate,
        private readonly HostPluginPackageDownloadService $packageDownloadService,
        private readonly HostPluginReleasePackageVerifier $releasePackageVerifier,
        private readonly HostSystemUpdateArchiveExtractor $archiveExtractor,
        private readonly DberpOfficialPluginGuard $officialPluginGuard,
        private readonly HostPluginLifecycleInstallService $lifecycleInstallService,
        private readonly HostPluginInstallRollback $installRollback,
        private readonly HostPluginOperationLogger $operationLogger,
        private readonly HostPluginOperationLock $operationLock,
    ) {}

    public function install(string $pluginCode, ?string $version = null): void
    {
        $normalizedCode = trim($pluginCode);
        $normalizedVersion = trim((string) ($version ?? ''));

        if ($normalizedCode === '') {
            throw HostPluginInstallException::blocked(
                (string) __('dberp.plugin_center.install.invalid_plugin'),
                'invalid_plugin_code',
            );
        }

        if ($normalizedVersion === '') {
            throw HostPluginInstallException::blocked(
                (string) __('dberp.plugin_center.install.version_required'),
                'version_required',
            );
        }

        $this->assertLocalPermission();
        $this->assertOfficialEntitlement();

        if (! $this->operationLock->acquire($normalizedCode, HostPluginOperationLogger::OPERATION_INSTALLATION)) {
            throw HostPluginInstallException::blocked(
                (string) __('dberp.plugin_center.lifecycle.task_in_progress'),
                'task_in_progress',
            );
        }

        $this->operationLogger->logStarted(
            HostPluginOperationLogger::OPERATION_INSTALLATION,
            $normalizedCode,
            ['version' => $normalizedVersion, 'operator_id' => Auth::id()],
        );

        $stagingDirectory = null;
        $archivePath = null;

        try {
            $package = $this->packageDownloadService->downloadAndVerify($normalizedCode, $normalizedVersion);

            if (! $package->verified) {
                throw HostPluginInstallException::failed(
                    (string) __('dberp.plugin_center.install.verification_failed'),
                    $package->download_error_code ?? 'verification_failed',
                );
            }

            $archivePath = $package->local_path;

            if (! is_string($archivePath) || ! is_file($archivePath)) {
                throw HostPluginInstallException::failed(
                    (string) __('dberp.plugin_center.install.package_missing'),
                    'package_missing',
                );
            }

            HostPluginPackageStorage::ensureStagingDirectoryExists();
            $stagingDirectory = HostPluginPackageStorage::buildStagingDirectory($normalizedCode);

            $this->operationLogger->logStarted(
                HostPluginOperationLogger::OPERATION_EXTRACTION,
                $normalizedCode,
            );

            $this->archiveExtractor->extractToStaging($archivePath, $stagingDirectory);

            $this->operationLogger->logCompleted(
                HostPluginOperationLogger::OPERATION_EXTRACTION,
                $normalizedCode,
            );

            $this->operationLogger->logStarted(
                HostPluginOperationLogger::OPERATION_RELEASE_PACKAGE_VALIDATION,
                $normalizedCode,
            );

            $packageValidation = $this->releasePackageVerifier->verifyStaging(
                stagingDirectory: $stagingDirectory,
                expectedPluginCode: $normalizedCode,
                archivePath: $archivePath,
            );

            if (! $packageValidation->valid) {
                $errorCode = $packageValidation->errorCode ?? 'release_package_validation_failed';
                $message = $this->releasePackageValidationMessage($errorCode);

                $this->operationLogger->logFailed(
                    HostPluginOperationLogger::OPERATION_RELEASE_PACKAGE_VALIDATION,
                    $normalizedCode,
                    $message,
                    $errorCode,
                );

                throw HostPluginInstallException::failed($message, $errorCode);
            }

            $this->operationLogger->logCompleted(
                HostPluginOperationLogger::OPERATION_RELEASE_PACKAGE_VALIDATION,
                $normalizedCode,
            );

            /** @var array<string, mixed> $manifest */
            $manifest = $packageValidation->pluginManifest ?? [];
            $pluginRoot = $packageValidation->pluginRoot;
            $alias = $packageValidation->alias;
            $targetDirectory = $this->officialPluginGuard->officialRootPath().DIRECTORY_SEPARATOR.$alias;

            if (is_dir($targetDirectory)) {
                File::deleteDirectory($targetDirectory);
            }

            File::copyDirectory($pluginRoot, $targetDirectory);

            $this->operationLogger->logStarted(
                HostPluginOperationLogger::OPERATION_REGISTRATION,
                $normalizedCode,
            );

            $lifecycleResult = $this->lifecycleInstallService->registerInstalledPlugin(
                $alias,
                [
                    'display_name' => trim((string) ($manifest['display_name'] ?? $manifest['name'])),
                    'path' => $targetDirectory,
                    'source' => 'official_support_center',
                    'plugin_code' => $normalizedCode,
                ],
                $targetDirectory,
            );

            if ($lifecycleResult->getStatus() !== 'success') {
                $this->handleLifecycleRegistrationFailure(
                    $lifecycleResult,
                    $normalizedCode,
                    $alias,
                    $targetDirectory,
                );
            }

            $this->runPluginMigrationsIfPresent($targetDirectory, $alias);

            $this->operationLogger->logCompleted(
                HostPluginOperationLogger::OPERATION_REGISTRATION,
                $normalizedCode,
            );

            $this->operationLogger->logCompleted(
                HostPluginOperationLogger::OPERATION_INSTALLATION,
                $normalizedCode,
                (string) __('dberp.plugin_center.install.success'),
            );
        } catch (HostPluginInstallException|SupportCenterException $exception) {
            $message = $exception instanceof HostPluginInstallException
                ? $exception->userFacingMessage()
                : $exception->userFacingMessage();

            $this->operationLogger->logFailed(
                HostPluginOperationLogger::OPERATION_INSTALLATION,
                $normalizedCode,
                $message,
                $exception instanceof HostPluginInstallException ? $exception->errorCode() : $exception->errorCode(),
                exception: $exception,
            );

            throw $exception instanceof HostPluginInstallException
                ? $exception
                : HostPluginInstallException::failed($message, $exception->errorCode(), $exception);
        } catch (Throwable $exception) {
            Log::error('dberp.plugin_center.installation_failed', [
                'plugin_code' => $normalizedCode,
                'version' => $normalizedVersion,
                'exception_class' => $exception::class,
                'exception_message' => $exception->getMessage(),
            ]);

            $this->operationLogger->logFailed(
                HostPluginOperationLogger::OPERATION_INSTALLATION,
                $normalizedCode,
                (string) __('dberp.plugin_center.install.failed'),
                'installation_failed',
                exception: $exception,
            );

            throw HostPluginInstallException::failed(
                (string) __('dberp.plugin_center.install.failed'),
                'installation_failed',
                $exception,
            );
        } finally {
            $this->operationLock->release($normalizedCode);

            if (is_string($stagingDirectory) && is_dir($stagingDirectory)) {
                File::deleteDirectory($stagingDirectory);
            }
        }
    }

    private function handleLifecycleRegistrationFailure(
        OperationResult $lifecycleResult,
        string $pluginCode,
        string $alias,
        string $targetDirectory,
    ): never {
        $isEntryInstallFailure = (bool) ($lifecycleResult->getData()['entry_install_failed'] ?? false);

        if (! $isEntryInstallFailure) {
            $this->installRollback->rollbackFilesystem($targetDirectory);
            $this->installRollback->rollbackPersistence($alias);
        }

        $message = trim($lifecycleResult->getMessage());

        if ($message === '') {
            $message = (string) __('dberp.plugin_center.install.failed');
        }

        $errorCode = $isEntryInstallFailure ? 'entry_install_failed' : 'lifecycle_install_failed';

        $this->operationLogger->logFailed(
            HostPluginOperationLogger::OPERATION_REGISTRATION,
            $pluginCode,
            $message,
            $errorCode,
        );

        throw HostPluginInstallException::failed($message, $errorCode);
    }

    private function assertLocalPermission(): void
    {
        $user = Auth::user();

        if ($user === null || ! $this->permissionService->allows($user, DberpPermissionService::INSTALL_HOST_PLUGIN)) {
            throw HostPluginInstallException::blocked(
                (string) __('dberp.plugin_center.install.permission_denied'),
                'permission_denied',
            );
        }
    }

    private function assertOfficialEntitlement(): void
    {
        if ($this->entitlementGate->canInstallPlugin()) {
            return;
        }

        throw HostPluginInstallException::blocked(
            $this->entitlementGate->installReason(),
            'entitlement_denied',
        );
    }

    private function releasePackageValidationMessage(string $errorCode): string
    {
        $key = 'dberp.plugin_center.install.release_errors.'.$errorCode;
        $message = __($key);

        if ($message !== $key) {
            return (string) $message;
        }

        return (string) __('dberp.plugin_center.install.package_validation_failed');
    }

    private function runPluginMigrationsIfPresent(string $pluginDirectory, string $alias): void
    {
        $migrationsPath = $pluginDirectory.DIRECTORY_SEPARATOR.'database'.DIRECTORY_SEPARATOR.'migrations';

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

        try {
            Artisan::call('migrate', [
                '--path' => $this->relativeMigrationPath($migrationsPath),
                '--force' => true,
            ]);
        } catch (Throwable $exception) {
            Log::warning('dberp.plugin_center.plugin_migration_failed', [
                'alias' => $alias,
                'exception_class' => $exception::class,
                'exception_message' => $exception->getMessage(),
            ]);
        }
    }

    private function relativeMigrationPath(string $absolutePath): string
    {
        $base = rtrim(str_replace('\\', '/', base_path()), '/');
        $normalized = str_replace('\\', '/', $absolutePath);

        if (str_starts_with($normalized, $base.'/')) {
            return substr($normalized, strlen($base) + 1);
        }

        return $normalized;
    }
}
