<?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\Plugin\HostPluginOperationException;
use App\Exceptions\SupportCenter\SupportCenterException;
use App\Models\CorePlugin;
use App\Services\DberpPermissionService;
use App\Services\SupportCenter\HostSystemUpdateArchiveExtractor;
use DBCore\Plugin\Data\PluginState;
use DBCore\Plugin\Enums\PluginStatus;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Throwable;

/**
 * 官方插件一键更新：独立于系统更新编排，替换前备份并在失败时尽量恢复原版本。
 */
final class HostPluginUpdateOrchestrator
{
    public function __construct(
        private readonly DberpPermissionService $permissionService,
        private readonly HostPluginEntitlementGate $entitlementGate,
        private readonly HostPluginPackageDownloadService $packageDownloadService,
        private readonly HostPluginUpdateCheckService $updateCheckService,
        private readonly HostPluginReleasePackageVerifier $releasePackageVerifier,
        private readonly HostSystemUpdateArchiveExtractor $archiveExtractor,
        private readonly DberpOfficialPluginGuard $officialPluginGuard,
        private readonly DberpDatabasePluginRepository $pluginRepository,
        private readonly DberpPluginDiscoveryManager $discoveryManager,
        private readonly HostPluginOperationLock $operationLock,
        private readonly HostPluginRuntimeCacheCleaner $runtimeCacheCleaner,
        private readonly HostPluginOperationLogger $operationLogger,
    ) {}

    public function update(string $pluginCode, ?string $targetVersion = null): void
    {
        $normalizedCode = trim($pluginCode);

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

        $record = CorePlugin::query()->where('identifier', $normalizedCode)->first();

        if ($record === null) {
            throw HostPluginOperationException::blocked(
                (string) __('dberp.plugin_center.update.not_installed'),
                'not_installed',
            );
        }

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

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

        $stagingDirectory = null;
        $backupDirectory = null;
        $previousStatus = (string) ($record->status ?? PluginStatus::Installed->value);
        $previousVersion = (string) ($record->version ?? '');

        try {
            $resolvedVersion = $this->resolveTargetVersion($normalizedCode, $previousVersion, $targetVersion);

            $this->operationLogger->logStarted(
                HostPluginOperationLogger::OPERATION_UPDATE,
                $normalizedCode,
                ['from_version' => $previousVersion, 'to_version' => $resolvedVersion],
            );

            $package = $this->packageDownloadService->downloadAndVerify(
                $normalizedCode,
                $resolvedVersion,
                HostPluginRemotePackagePurpose::Update,
            );

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

            $archivePath = $package->local_path;

            if (! is_string($archivePath) || ! is_file($archivePath)) {
                throw HostPluginOperationException::failed(
                    (string) __('dberp.plugin_center.update.package_missing'),
                    'package_missing',
                    preservedPreviousVersion: true,
                );
            }

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

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

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

            $this->operationLogger->logCompleted(
                HostPluginOperationLogger::OPERATION_UPDATE_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';

                throw HostPluginOperationException::failed(
                    $this->releasePackageValidationMessage($errorCode),
                    $errorCode,
                    preservedPreviousVersion: true,
                );
            }

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

            $staged = [
                'manifest' => $packageValidation->pluginManifest ?? [],
                'plugin_root' => $packageValidation->pluginRoot,
                'alias' => $packageValidation->alias,
            ];

            $targetDirectory = $this->officialPluginGuard->officialRootPath().DIRECTORY_SEPARATOR.$staged['alias'];
            $backupDirectory = $this->createBackupDirectory($staged['alias']);

            $this->operationLogger->logStarted(
                HostPluginOperationLogger::OPERATION_UPDATE_REPLACEMENT,
                $normalizedCode,
                ['backup_directory' => basename($backupDirectory)],
            );

            if (is_dir($targetDirectory)) {
                File::copyDirectory($targetDirectory, $backupDirectory);
            }

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

            File::copyDirectory($staged['plugin_root'], $targetDirectory);

            /** @var array<string, mixed> $manifest */
            $manifest = $staged['manifest'];

            /** @var array<string, mixed> $meta */
            $meta = is_array($record->meta) ? $record->meta : [];
            /** @var array<string, mixed> $extra */
            $extra = is_array($meta['extra'] ?? null) ? $meta['extra'] : [];

            $this->pluginRepository->save(new PluginState(
                name: $staged['alias'],
                status: $this->normalizeStatusAfterUpdate($previousStatus),
                version: trim((string) $manifest['version']),
                installedAt: isset($meta['installed_at']) ? (string) $meta['installed_at'] : null,
                enabledAt: $meta['enabled_at'] ?? null,
                extra: array_merge($extra, [
                    'display_name' => trim((string) $manifest['name']),
                    'path' => $targetDirectory,
                    'source' => 'official_support_center',
                    'plugin_code' => $normalizedCode,
                ]),
            ));

            $this->discoveryManager->scan();
            $this->runPluginMigrationsIfPresent($targetDirectory, $staged['alias']);
            $this->runtimeCacheCleaner->clearAfterLifecycleChange($staged['alias']);

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

            $this->operationLogger->logCompleted(
                HostPluginOperationLogger::OPERATION_UPDATE,
                $normalizedCode,
                (string) __('dberp.plugin_center.update.success'),
                ['from_version' => $previousVersion, 'to_version' => trim((string) $manifest['version'])],
            );
        } catch (HostPluginOperationException|HostPluginInstallException|SupportCenterException $exception) {
            $this->attemptRestoreFromBackup($backupDirectory, $normalizedCode);
            $message = $exception instanceof HostPluginOperationException
                ? $exception->userFacingMessage()
                : ($exception instanceof HostPluginInstallException
                    ? $exception->userFacingMessage()
                    : $exception->userFacingMessage());

            $preserved = $backupDirectory !== null && is_dir($backupDirectory);

            if ($preserved && ! ($exception instanceof HostPluginOperationException && $exception->preservedPreviousVersion())) {
                $message = (string) __('dberp.plugin_center.update.failed_preserved');
            }

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

            if ($exception instanceof HostPluginOperationException) {
                throw $preserved && ! $exception->preservedPreviousVersion()
                    ? HostPluginOperationException::failed($message, $exception->errorCode(), true, $exception)
                    : $exception;
            }

            throw HostPluginOperationException::failed($message, $exception->errorCode(), $preserved, $exception);
        } catch (Throwable $exception) {
            $this->attemptRestoreFromBackup($backupDirectory, $normalizedCode);

            Log::error('dberp.plugin_center.update_failed', [
                'plugin_code' => $normalizedCode,
                'exception_class' => $exception::class,
                'exception_message' => $exception->getMessage(),
            ]);

            $preserved = $backupDirectory !== null && is_dir($backupDirectory);

            $this->operationLogger->logFailed(
                HostPluginOperationLogger::OPERATION_UPDATE,
                $normalizedCode,
                $preserved
                    ? (string) __('dberp.plugin_center.update.failed_preserved')
                    : (string) __('dberp.plugin_center.update.failed'),
                'update_failed',
                exception: $exception,
            );

            throw HostPluginOperationException::failed(
                $preserved
                    ? (string) __('dberp.plugin_center.update.failed_preserved')
                    : (string) __('dberp.plugin_center.update.failed'),
                'update_failed',
                $preserved,
                $exception,
            );
        } finally {
            $this->operationLock->release($normalizedCode);

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

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

    private function resolveTargetVersion(string $pluginCode, string $installedVersion, ?string $targetVersion): string
    {
        $explicit = is_string($targetVersion) ? trim($targetVersion) : '';

        if ($explicit !== '') {
            return $explicit;
        }

        $check = $this->updateCheckService->check($pluginCode, $installedVersion !== '' ? $installedVersion : null);

        if ($check->state !== HostPluginUpdateCheckResult::STATE_UPDATE_AVAILABLE || $check->latestVersion === null) {
            throw HostPluginOperationException::blocked(
                (string) __('dberp.plugin_center.update.no_update_available'),
                'no_update_available',
            );
        }

        return $check->latestVersion;
    }

    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 assertLocalPermission(): void
    {
        $user = Auth::user();

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

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

        throw HostPluginOperationException::blocked(
            $this->entitlementGate->updateReason(),
            'entitlement_denied',
        );
    }

    private function normalizeStatusAfterUpdate(string $previousStatus): string
    {
        return in_array($previousStatus, [
            PluginStatus::Enabled->value,
            PluginStatus::Disabled->value,
            PluginStatus::Installed->value,
            'failed',
        ], true) ? $previousStatus : PluginStatus::Installed->value;
    }

    private function createBackupDirectory(string $alias): string
    {
        $backupRoot = HostPluginPackageStorage::baseDirectory().DIRECTORY_SEPARATOR.'backups';

        if (! is_dir($backupRoot)) {
            mkdir($backupRoot, 0755, true);
        }

        return $backupRoot.DIRECTORY_SEPARATOR.$alias.'-'.now()->format('YmdHis');
    }

    private function attemptRestoreFromBackup(?string $backupDirectory, string $pluginCode): void
    {
        if ($backupDirectory === null || ! is_dir($backupDirectory)) {
            return;
        }

        $record = CorePlugin::query()->where('identifier', $pluginCode)->first();

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

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

        if ($path === '') {
            $path = $this->officialPluginGuard->officialRootPath().DIRECTORY_SEPARATOR.$pluginCode;
        }

        try {
            if (is_dir($path)) {
                File::deleteDirectory($path);
            }

            File::copyDirectory($backupDirectory, $path);
            $this->discoveryManager->scan();

            $this->operationLogger->logCompleted(
                HostPluginOperationLogger::OPERATION_UPDATE_RESTORE,
                $pluginCode,
                (string) __('dberp.plugin_center.update.restore_completed'),
            );
        } catch (Throwable $exception) {
            Log::error('dberp.plugin_center.update_restore_failed', [
                'plugin_code' => $pluginCode,
                'exception_class' => $exception::class,
            ]);

            $this->operationLogger->logFailed(
                HostPluginOperationLogger::OPERATION_UPDATE_RESTORE,
                $pluginCode,
                (string) __('dberp.plugin_center.update.restore_failed'),
                'restore_failed',
                exception: $exception,
            );
        }
    }

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

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

        $this->operationLogger->logStarted(
            HostPluginOperationLogger::OPERATION_UPDATE_MIGRATION,
            $alias,
        );

        try {
            Artisan::call('migrate', [
                '--path' => $this->relativeMigrationPath($migrationsPath),
                '--force' => true,
            ]);

            $this->operationLogger->logCompleted(
                HostPluginOperationLogger::OPERATION_UPDATE_MIGRATION,
                $alias,
            );
        } catch (Throwable $exception) {
            $this->operationLogger->logFailed(
                HostPluginOperationLogger::OPERATION_UPDATE_MIGRATION,
                $alias,
                (string) __('dberp.plugin_center.update.migration_failed'),
                'migration_failed',
                exception: $exception,
            );

            Log::warning('dberp.plugin_center.update_migration_failed', [
                'alias' => $alias,
                'exception_class' => $exception::class,
            ]);
        }
    }

    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;
    }
}
