<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\StoreServerResourceRequest;
use App\Http\Requests\UpdateServerResourceRequest;
use App\Models\AccessLog;
use App\Models\BastionAccount;
use App\Models\OpsProtocol;
use App\Models\ServerResource;
use App\Models\User;
use App\Models\UserServerPermission;
use hg\apidoc\annotation as Apidoc;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
use Illuminate\Validation\ValidationException;
use Spatie\Permission\Models\Permission;

#[Apidoc\Title('服务器资源管理')]
class ServerResourceController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth:api');
        $this->middleware('permission:platform.servers.view|resource.servers.use,api')->only(['index', 'show']);
        $this->middleware('permission:platform.servers.manage,api')->only(['store', 'update', 'destroy', 'syncUserPermissions', 'userPermissions']);
    }

    #[Apidoc\Title('资源列表'), Apidoc\Method('GET'), Apidoc\Url('/servers')]
    public function index(): JsonResponse
    {
        $this->syncAllResourcePermissions();
        $query = ServerResource::query()->with('parent')->latest();
        $user = auth('api')->user();

        if ($user && ! $user->can('platform.servers.view')) {
            $pivotResourceIds = $user->serverResources()
                ->where(function ($pivotQuery) {
                    $pivotQuery->where('can_ssh', true)
                        ->orWhere('can_sftp', true)
                        ->orWhere('can_rdp', true);
                })
                ->pluck('server_resources.id')
                ->values();

            $permissionResourceIds = $this->resolveResourceIdsFromPermissions($user);
            $resourceIds = $pivotResourceIds
                ->merge($permissionResourceIds)
                ->map(fn ($id): int => (int) $id)
                ->unique()
                ->values();

            $parentIds = ServerResource::query()
                ->whereIn('id', $resourceIds)
                ->pluck('parent_id')
                ->filter()
                ->values();

            $query->where(function ($scope) use ($resourceIds, $parentIds) {
                $scope->whereIn('id', $resourceIds)->orWhereIn('id', $parentIds);
            });
        }

        return response()->json(['code' => 0, 'message' => 'ok', 'data' => $query->paginate(500)]);
    }

    private function resolveResourceIdsFromPermissions(User $user): Collection
    {
        $allPermissions = $user->getAllPermissions();
        $resourceIds = collect();
        foreach ($allPermissions as $permission) {
            $permissionName = (string) $permission->name;
            if (! str_starts_with($permissionName, 'resource.servers.use.')) {
                continue;
            }

            $description = (string) ($permission->description ?? '');
            if (preg_match('/资源ID[:：]\s*(\d+)/u', $description, $matches) === 1) {
                $resourceIds->push((int) $matches[1]);

                continue;
            }

            $nameParts = explode('.', $permissionName);
            if (count($nameParts) < 6) {
                continue;
            }

            $serverName = trim((string) ($nameParts[3] ?? ''));
            $resourceName = trim((string) ($nameParts[4] ?? ''));
            if ($serverName === '' || $resourceName === '') {
                continue;
            }

            $matchedIds = ServerResource::query()
                ->from('server_resources as resource')
                ->join('server_resources as server', 'server.id', '=', 'resource.parent_id')
                ->where('server.name', $serverName)
                ->where('resource.name', $resourceName)
                ->pluck('resource.id')
                ->map(fn ($id): int => (int) $id)
                ->all();
            if (! empty($matchedIds)) {
                $resourceIds = $resourceIds->merge($matchedIds);
            }
        }

        return $resourceIds->unique()->values();
    }

    #[Apidoc\Title('创建资源'), Apidoc\Method('POST'), Apidoc\Url('/servers')]
    public function store(StoreServerResourceRequest $request): JsonResponse
    {
        $data = $request->validated();
        $isResource = ! empty($data['parent_id']);
        $targetParentId = $isResource ? (int) $data['parent_id'] : null;

        $this->ensureUniqueNameUnderParent((string) $data['name'], $targetParentId, null);

        if ($isResource) {
            $parent = ServerResource::query()->findOrFail($targetParentId);
            $data['internal_ip'] = $data['internal_ip'] ?? $parent->internal_ip;
            $data['protocols'] = [$this->resolveResourceProtocol((string) ($data['protocol'] ?? ''), null)];
            $data['display_name'] = $data['display_name'] ?? $data['name'];
            $data['asset_id'] = $parent->asset_id;

            if (empty($data['asset_id'])) {
                throw ValidationException::withMessages([
                    'parent_id' => ['所属服务器未配置 asset_id，请先配置服务器的 asset_id。'],
                ]);
            }

            if (empty($data['account_id'])) {
                throw ValidationException::withMessages([
                    'account_id' => ['资源必须填写 account_id。'],
                ]);
            }
        } else {
            $data['protocols'] = [];
            $data['account_id'] = null;
            $data['display_name'] = $data['display_name'] ?? $data['name'];
        }

        unset($data['protocol']);

        $server = ServerResource::query()->create($data);
        $this->syncResourcePermission($server->load('parent'));
        $this->auditLog($request, $isResource ? 'resource_create' : 'server_create', ['server_resource_id' => $server->id]);

        return response()->json(['code' => 0, 'message' => 'ok', 'data' => $server], 201);
    }

    #[Apidoc\Title('资源详情'), Apidoc\Method('GET'), Apidoc\Url('/servers/{id}')]
    public function show(int $id): JsonResponse
    {
        return response()->json(['code' => 0, 'message' => 'ok', 'data' => ServerResource::query()->with('parent')->findOrFail($id)]);
    }

    #[Apidoc\Title('更新资源'), Apidoc\Method('PUT'), Apidoc\Url('/servers/{id}')]
    public function update(UpdateServerResourceRequest $request, int $id): JsonResponse
    {
        $server = ServerResource::query()->findOrFail($id);
        $server->load('children');
        $data = $request->validated();
        $isResource = ! empty($data['parent_id']) || ! empty($server->parent_id);
        $targetParentId = $isResource ? (int) ($data['parent_id'] ?? $server->parent_id) : null;

        $this->ensureUniqueNameUnderParent((string) $data['name'], $targetParentId, (int) $server->id);

        if ($isResource) {
            $parent = ServerResource::query()->findOrFail($targetParentId);
            $data['internal_ip'] = $data['internal_ip'] ?? $parent->internal_ip;
            $data['protocols'] = [$this->resolveResourceProtocol((string) ($data['protocol'] ?? ''), (string) ($server->protocols[0] ?? ''))];
            $data['display_name'] = $data['display_name'] ?? ($server->display_name ?: $data['name']);
            $data['asset_id'] = $parent->asset_id;

            if (empty($data['asset_id'])) {
                throw ValidationException::withMessages([
                    'parent_id' => ['所属服务器未配置 asset_id，请先配置服务器的 asset_id。'],
                ]);
            }

            if (empty($data['account_id'])) {
                throw ValidationException::withMessages([
                    'account_id' => ['资源必须填写 account_id。'],
                ]);
            }
        } else {
            $data['protocols'] = [];
            $data['account_id'] = null;
            $data['display_name'] = $data['display_name'] ?? ($server->display_name ?: $data['name']);
        }

        unset($data['protocol']);
        $server->update($data);
        $server->refresh()->load(['parent', 'children.parent']);
        $this->syncResourcePermission($server);
        if (! $server->parent_id) {
            ServerResource::query()
                ->where('parent_id', $server->id)
                ->update(['asset_id' => $server->asset_id]);
            foreach ($server->children as $childResource) {
                $this->syncResourcePermission($childResource->load('parent'));
            }
        }

        $this->auditLog($request, $isResource ? 'resource_update' : 'server_update', ['server_resource_id' => $server->id]);

        return response()->json(['code' => 0, 'message' => 'ok', 'data' => $server]);
    }

    #[Apidoc\Title('删除资源'), Apidoc\Method('DELETE'), Apidoc\Url('/servers/{id}')]
    public function destroy(Request $request, int $id): JsonResponse
    {
        $server = ServerResource::query()->with('children')->findOrFail($id);
        $this->auditLog($request, $server->parent_id ? 'resource_delete' : 'server_delete', ['server_resource_id' => $server->id]);
        if (! $server->parent_id) {
            $descendantIds = $this->collectDescendantResourceIds((int) $server->id);

            foreach ($descendantIds as $descendantId) {
                $this->deleteResourcePermission($descendantId);
            }

            ServerResource::query()
                ->whereIn('id', $descendantIds)
                ->delete();
        }
        $this->deleteResourcePermission((int) $server->id);
        $server->delete();

        return response()->json(['code' => 0, 'message' => 'ok', 'data' => null]);
    }

    #[Apidoc\Title('资源用户权限'), Apidoc\Method('GET'), Apidoc\Url('/servers/{id}/user-permissions')]
    public function userPermissions(Request $request, int $id): JsonResponse
    {
        $server = ServerResource::query()->findOrFail($id);
        $assignedOnly = $request->boolean('assigned_only', false);

        $users = User::query()->select(['id', 'nickname', 'email', 'phone'])->with(['serverResources' => function ($query) use ($id) {
            $query->where('server_resource_id', $id);
        }])->orderBy('id')->get()->map(function (User $user) {
            $pivot = $user->serverResources->first()?->pivot;

            return [
                'id' => $user->id,
                'nickname' => $user->nickname,
                'email' => $user->email,
                'phone' => $user->phone,
                'can_ssh' => (bool) ($pivot->can_ssh ?? false),
                'can_sftp' => (bool) ($pivot->can_sftp ?? false),
                'can_rdp' => (bool) ($pivot->can_rdp ?? false),
            ];
        })->filter(function (array $userItem) use ($assignedOnly) {
            if (! $assignedOnly) {
                return true;
            }

            return (bool) ($userItem['can_ssh'] || $userItem['can_sftp'] || $userItem['can_rdp']);
        })->values();

        return response()->json(['code' => 0, 'message' => 'ok', 'data' => ['server' => $server, 'users' => $users]]);
    }

    #[Apidoc\Title('同步资源用户权限'), Apidoc\Method('PUT'), Apidoc\Url('/servers/{id}/user-permissions')]
    public function syncUserPermissions(Request $request, int $id): JsonResponse
    {
        $validated = $request->validate([
            'users' => ['required', 'array'],
            'users.*.id' => ['required', 'integer', 'exists:users,id'],
            'users.*.can_ssh' => ['required', 'boolean'],
            'users.*.can_sftp' => ['required', 'boolean'],
            'users.*.can_rdp' => ['required', 'boolean'],
            'partial' => ['sometimes', 'boolean'],
        ]);

        $server = ServerResource::query()->with('parent')->findOrFail($id);
        $syncData = [];

        foreach ($validated['users'] as $userItem) {
            $syncData[(int) $userItem['id']] = [
                'can_ssh' => (bool) $userItem['can_ssh'],
                'can_sftp' => (bool) $userItem['can_sftp'],
                'can_rdp' => (bool) $userItem['can_rdp'],
            ];
        }

        $partial = (bool) ($validated['partial'] ?? false);
        if ($partial) {
            foreach ($syncData as $userId => $permissionItem) {
                $server->users()->updateExistingPivot($userId, $permissionItem);
            }
        } else {
            $server->users()->sync($syncData);
        }

        $permission = $this->syncResourcePermission($server);
        $this->syncDirectPermissionsByPivot($server, $permission, $syncData);

        $this->auditLog($request, 'resource_user_permissions_update', ['server_resource_id' => $server->id]);

        return response()->json(['code' => 0, 'message' => 'ok', 'data' => null]);
    }

    #[Apidoc\Title('使用服务器资源'), Apidoc\Method('POST'), Apidoc\Url('/servers/{id}/use')]
    public function useResource(Request $request, int $id): JsonResponse
    {
        $validated = $request->validate([
            'account_name' => ['required', 'string', 'max:255'],
            'password' => ['required', 'string', 'max:255'],
            'protocol' => ['required', 'string', 'max:64'],
        ], [
            'account_name.required' => '请输入访问用户名。',
            'password.required' => '请输入访问密码。',
        ]);

        $resource = ServerResource::query()->with('parent')->findOrFail($id);
        if (! $resource->parent_id) {
            throw ValidationException::withMessages([
                'id' => ['请选择具体资源后再发起访问。'],
            ]);
        }

        if (! $resource->is_active) {
            throw ValidationException::withMessages([
                'id' => ['该资源已停用，无法访问。'],
            ]);
        }

        $assetId = (int) ($resource->asset_id ?: ($resource->parent?->asset_id ?: 0));
        $accountId = (int) ($resource->account_id ?: 0);

        if ($assetId <= 0 || $accountId <= 0) {
            throw ValidationException::withMessages([
                'id' => ['该资源缺少 asset_id 或 account_id，无法访问。'],
            ]);
        }

        $requestedProtocol = (string) $validated['protocol'];
        $resourceProtocols = collect($resource->protocols ?? [])
            ->map(fn ($item): string => (string) $item)
            ->filter()
            ->mapWithKeys(fn (string $item): array => [mb_strtolower($item) => $item])
            ->toArray();
        $protocol = $resourceProtocols[mb_strtolower($requestedProtocol)] ?? '';
        if ($protocol === '') {
            throw ValidationException::withMessages([
                'protocol' => ['该资源不支持所选协议。'],
            ]);
        }

        /** @var User|null $user */
        $user = auth('api')->user();
        if (! $user || ! $this->canUseResource($user, $resource, $protocol)) {
            return response()->json([
                'code' => 403,
                'message' => '无权限使用该资源',
                'data' => null,
            ], 403);
        }

        $bastionAccount = BastionAccount::query()
            ->where('is_active', true)
            ->whereNotNull('usm')
            ->where('usm', '!=', '')
            ->whereNotNull('usm_authentication')
            ->where('usm_authentication', '!=', '')
            ->inRandomOrder()
            ->first();

        if (! $bastionAccount) {
            return response()->json([
                'code' => 422,
                'message' => '当前没有可用的堡垒机授权账号，请先刷新账号 Token',
                'data' => null,
            ], 422);
        }

        $baseUrl = (string) config('services.bastion_access.base_url', 'https://172.16.254.2');
        $endpoint = (string) config('services.bastion_access.sso_endpoint', '/usmapi/v1/operation/custom/sso');
        $timeout = (int) config('services.bastion_access.timeout', 30);
        $verifySsl = (bool) config('services.bastion_access.verify_ssl', false);
        $protocolId = $this->resolveProtocolId($protocol);
        $accountName = (string) ($validated['account_name'] ?? '');
        $password = (string) ($validated['password'] ?? '');

        try {
            $response = Http::baseUrl($baseUrl)
                ->acceptJson()
                ->timeout($timeout)
                ->retry(2, 300, throw: false)
                ->withOptions(['verify' => $verifySsl])
                ->withHeaders([
                    'Cookie' => sprintf(
                        'USM=%s; USM-AUTHENTICATION=%s',
                        (string) $bastionAccount->usm,
                        (string) $bastionAccount->usm_authentication
                    ),
                ])
                ->post($endpoint, [
                    'accounts' => [[
                        'account_name' => $accountName,
                        'protocol_id' => $protocolId,
                        'asset_id' => $assetId,
                        'account_id' => $accountId,
                        'rule_id' => 0,
                        'password' => $password,
                    ]],
                    'op_type' => 'bs',
                    'description' => '',
                    'second_verify_token' => '',
                    'use_taibao_password' => 0,
                    'use_om_password_cache' => 0,
                ]);
        } catch (ConnectionException|RequestException $exception) {
            return response()->json([
                'code' => 502,
                'message' => '堡垒机接口调用失败：'.$exception->getMessage(),
                'data' => null,
            ], 502);
        }

        if (! $response->successful()) {
            return response()->json([
                'code' => 502,
                'message' => '堡垒机接口返回异常',
                'data' => [
                    'status' => $response->status(),
                    'response' => $response->json(),
                ],
            ], 502);
        }

        $result = $response->json();
        if (strtoupper((string) data_get($result, 'code')) !== 'SUCCESS') {
            return response()->json([
                'code' => 502,
                'message' => '堡垒机接口返回失败：'.((string) data_get($result, 'msg', '未知错误')),
                'data' => ['response' => $result],
            ], 502);
        }

        $ssoUrl = (string) data_get($result, 'data.items.0.url', '');
        if ($ssoUrl === '') {
            return response()->json([
                'code' => 502,
                'message' => '堡垒机未返回可用的 SSO 地址',
                'data' => ['response' => $result],
            ], 502);
        }

        $tempPassword = null;
        if ((bool) $resource->allow_copy_temp_password) {
            $tempPassword = $this->extractSsoTokenFromUrl($ssoUrl);
        }

        AccessLog::query()->create([
            'user_id' => $user->id,
            'server_resource_id' => $resource->id,
            'bastion_account_id' => $bastionAccount->id,
            'protocol' => $protocol,
            'action' => 'resource_use',
            'requested_at' => now(),
            'metadata' => [
                'resource_name' => $resource->display_name ?: $resource->name,
                'account_name' => $accountName,
                'has_password' => $password !== '',
                'protocol_id' => $protocolId,
                'bastion_response_code' => data_get($result, 'code'),
            ],
        ]);

        $this->auditLog($request, 'resource_use', [
            'server_resource_id' => $resource->id,
            'bastion_account_id' => $bastionAccount->id,
            'metadata' => [
                'protocol' => $protocol,
                'protocol_id' => $protocolId,
                'account_name' => $accountName,
            ],
        ]);

        return response()->json([
            'code' => 0,
            'message' => 'ok',
            'data' => [
                'url' => $ssoUrl,
                'protocol' => $protocol,
                'resource_id' => $resource->id,
                'resource_name' => $resource->display_name ?: $resource->name,
                'bastion_account_id' => $bastionAccount->id,
                'client_type' => (string) data_get($result, 'data.client_type', ''),
                'response' => $result,
                'allow_copy_temp_password' => (bool) $resource->allow_copy_temp_password,
                'temp_password' => $tempPassword,
            ],
        ]);
    }

    private function extractSsoTokenFromUrl(string $ssoUrl): ?string
    {
        if (! str_starts_with($ssoUrl, 'sso://')) {
            return null;
        }

        $encoded = trim(substr($ssoUrl, strlen('sso://')));
        $encoded = rtrim($encoded, '/');
        if ($encoded === '') {
            return null;
        }

        $decoded = base64_decode($encoded, true);
        if ($decoded === false || $decoded === '') {
            return null;
        }

        $payload = json_decode($decoded, true);
        if (! is_array($payload)) {
            return null;
        }

        $token = data_get($payload, 'NODE_COMMON.SSOToken');
        if (! is_string($token) || trim($token) === '') {
            return null;
        }

        return trim($token);
    }

    private function syncDirectPermissionsByPivot(ServerResource $resource, Permission $permission, array $syncData): void
    {
        if (! $resource->parent_id) {
            return;
        }

        foreach ($syncData as $userId => $permissionItem) {
            $canUseResource = (bool) ($permissionItem['can_ssh'] || $permissionItem['can_sftp'] || $permissionItem['can_rdp']);
            $user = User::query()->find((int) $userId);

            if (! $user) {
                continue;
            }

            if ($canUseResource) {
                if (! $user->hasDirectPermission($permission->name)) {
                    $user->givePermissionTo($permission);
                }
            } else {
                $user->revokePermissionTo($permission->name);
            }
        }
    }

    public static function buildResourcePermissionName(ServerResource $resource): string
    {
        $serverName = $resource->parent_id ? (string) ($resource->parent?->name ?: 'unknown-server') : (string) ($resource->name ?: 'unknown-server');
        $resourceName = (string) ($resource->name ?: 'unknown-resource');

        return sprintf(
            'resource.servers.use.%s.%s',
            trim($serverName),
            trim($resourceName)
        );
    }

    public static function resourcePermissionDescription(ServerResource $resource): string
    {
        $resource->loadMissing('parent');
        if (! $resource->parent_id) {
            $serverLabel = trim((string) ($resource->display_name ?: $resource->name ?: '未命名服务器'));

            return sprintf('服务器资源访问权限（%s，资源ID: %d）', $serverLabel, (int) $resource->id);
        }

        $serverLabel = trim((string) ($resource->parent?->display_name ?: $resource->parent?->name ?: '未命名服务器'));
        $resourceLabel = trim((string) ($resource->display_name ?: $resource->name ?: '未命名资源'));

        return sprintf('服务器资源访问权限（%s-%s，资源ID: %d）', $serverLabel, $resourceLabel, (int) $resource->id);
    }

    private function syncResourcePermission(ServerResource $resource): Permission
    {
        $description = self::resourcePermissionDescription($resource);
        $permission = Permission::query()
            ->where('guard_name', 'api')
            ->where('description', $description)
            ->first();
        $basePermissionName = self::buildResourcePermissionName($resource);

        if (! $permission) {
            $permission = Permission::query()->firstOrCreate(
                [
                    'guard_name' => 'api',
                    'name' => $this->resolvePermissionNameConflict($basePermissionName, null, (int) $resource->id),
                ],
                [
                    'category' => '资源使用',
                    'description' => $description,
                ]
            );
        }

        $permission->update([
            'name' => $this->resolvePermissionNameConflict($basePermissionName, (int) $permission->id, (int) $resource->id),
            'category' => '资源使用',
            'description' => $description,
        ]);

        return $permission;
    }

    private function deleteResourcePermission(int $resourceId): void
    {
        Permission::query()
            ->where('guard_name', 'api')
            ->where('description', 'like', '%资源ID: '.$resourceId.'%')
            ->delete();

        UserServerPermission::query()
            ->where('server_resource_id', $resourceId)
            ->delete();
    }

    private function ensureUniqueNameUnderParent(string $name, ?int $parentId, ?int $ignoreId): void
    {
        $query = ServerResource::query()->where('name', $name);

        if ($parentId === null) {
            $query->whereNull('parent_id');
        } else {
            $query->where('parent_id', $parentId);
        }

        if ($ignoreId !== null) {
            $query->where('id', '!=', $ignoreId);
        }

        if ($query->exists()) {
            throw ValidationException::withMessages([
                'name' => ['同一服务器层级下名称必须唯一。'],
            ]);
        }
    }

    private function resolvePermissionNameConflict(string $baseName, ?int $ignorePermissionId, int $resourceId): string
    {
        $query = Permission::query()
            ->where('guard_name', 'api')
            ->where('name', $baseName);

        if ($ignorePermissionId !== null) {
            $query->where('id', '!=', $ignorePermissionId);
        }

        if (! $query->exists()) {
            return $baseName;
        }

        return $baseName.'.'.$resourceId;
    }

    private function syncAllResourcePermissions(): void
    {
        $resources = ServerResource::query()->with('parent')->get();

        foreach ($resources as $resource) {
            $this->syncResourcePermission($resource);
        }
    }

    private function canUseResource(User $user, ServerResource $resource, string $protocol): bool
    {
        if ($user->can('platform.servers.view')) {
            return true;
        }

        $resourcePermission = Permission::query()
            ->where('guard_name', 'api')
            ->where('description', self::resourcePermissionDescription($resource))
            ->first();
        if ($resourcePermission && $user->hasPermissionTo($resourcePermission->name)) {
            return true;
        }

        $pivot = $resource->users()
            ->where('users.id', $user->id)
            ->first()?->pivot;
        if (! $pivot) {
            return false;
        }

        return match ($protocol) {
            'SFTP' => (bool) $pivot->can_sftp,
            'RDP' => (bool) $pivot->can_rdp,
            default => (bool) $pivot->can_ssh,
        };
    }

    private function resolveProtocolId(string $protocol): int
    {
        $managed = OpsProtocol::query()
            ->where('name', $protocol)
            ->first();
        if ($managed) {
            return (int) ($managed->bastion_protocol_id ?: 2);
        }

        return match (strtoupper($protocol)) {
            'SFTP' => (int) config('services.bastion_access.protocol_ids.sftp', 4),
            'RDP' => (int) config('services.bastion_access.protocol_ids.rdp', 3),
            default => (int) config('services.bastion_access.protocol_ids.ssh', 2),
        };
    }

    /**
     * @return int[]
     */
    private function collectDescendantResourceIds(int $parentId): array
    {
        $descendantIds = [];
        $queue = [$parentId];

        while (! empty($queue)) {
            $children = ServerResource::query()
                ->whereIn('parent_id', $queue)
                ->pluck('id')
                ->map(fn (int $id): int => (int) $id)
                ->all();

            if (empty($children)) {
                break;
            }

            $descendantIds = array_values(array_unique([...$descendantIds, ...$children]));
            $queue = $children;
        }

        return $descendantIds;
    }

    private function resolveResourceProtocol(string $requestedProtocol, ?string $fallbackProtocol): string
    {
        $normalizedRequested = trim($requestedProtocol);
        if ($normalizedRequested !== '') {
            $exists = OpsProtocol::query()->where('name', $normalizedRequested)->exists();
            if (! $exists) {
                throw ValidationException::withMessages([
                    'protocol' => ['协议不存在，请先在运维协议中配置。'],
                ]);
            }

            return $normalizedRequested;
        }

        $normalizedFallback = trim((string) $fallbackProtocol);
        if ($normalizedFallback !== '') {
            return $normalizedFallback;
        }

        $firstProtocol = (string) OpsProtocol::query()
            ->where('is_active', true)
            ->orderBy('sort')
            ->orderBy('id')
            ->value('name');

        if ($firstProtocol === '') {
            throw ValidationException::withMessages([
                'protocol' => ['请先在运维协议中新增并启用至少一个协议。'],
            ]);
        }

        return $firstProtocol;
    }
}
